[
  {
    "path": ".dockerignore",
    "content": "# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# Virtual environments\n.venv/\nvenv/\nENV/\nenv/\n\n# Testing\n.pytest_cache/\n.coverage\nhtmlcov/\n.tox/\n\n# IDEs\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n\n# Git\n.git/\n.gitignore\n\n# Docker development files (not part of the SDK package)\nDockerfile\ndocker-compose.yml\n.dockerignore\nMakefile\n\n# OS\n.DS_Store\nThumbs.db\n\n# Documentation\ndocs/_build/\n\n# Local dev files\n*.log\n.env\n.env.*\n!.env.example\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Create a report to help us improve Memori\ntitle: \"[Bug] \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report! Please provide as much detail as possible.\n\n  - type: checkboxes\n    id: checks\n    attributes:\n      label: First Check\n      description: Please ensure you have done the following before submitting your issue.\n      options:\n        - label: I added a very descriptive title to this issue.\n          required: true\n        - label: I searched existing issues and documentation.\n          required: true\n\n  - type: input\n    id: version\n    attributes:\n      label: Memori Version\n      description: What version of the sdk are you using?\n      placeholder: e.g., 3.0.0\n    validations:\n      required: true\n\n  - type: input\n    id: environment\n    attributes:\n      label: OS / Python Version\n      description: e.g., Windows 10 / Python 3.10, or Ubuntu 22.04 / Python 3.11\n      placeholder: macOS / Python 3.12\n    validations:\n      required: true\n\n  - type: dropdown\n    id: llm_provider\n    attributes:\n      label: LLM Provider\n      description: Which LLM provider are you using?\n      options:\n        - OpenAI\n        - Anthropic\n        - Gemini\n        - Bedrock\n        - Grok (xAI)\n        - Ollama / Local\n        - Other\n    validations:\n      required: true\n\n  - type: input\n    id: llm_details\n    attributes:\n      label: LLM Model & Version\n      description: Please specify the model name/version.\n      placeholder: e.g., gpt-4o or claude-3-5-sonnet\n    validations:\n      required: true\n\n  - type: dropdown\n    id: database\n    attributes:\n      label: Database\n      description: Which database integration are you using?\n      options:\n        - Postgres\n        - MySQL\n        - MongoDB\n        - Oracle\n        - SQLite\n        - Neon\n        - Supabase\n        - Other\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: Please describe the bug clearly. What happened? What did you expect to happen?\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Minimal Reproducible Example\n      description: Please provide a code snippet that reproduces the issue.\n      render: python\n      placeholder: |\n        from memori import Memori\n        # Your code here...\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Log Output / Stack Trace\n      description: Please copy and paste any relevant log output or error messages here.\n      render: shell\n\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Participation\n      options:\n        - label: I am willing to submit a pull request for this issue.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest an idea or improvement for Memori\ntitle: \"[Feature] \"\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your interest in improving Memori! Please describe your idea in detail.\n\n  - type: textarea\n    id: problem\n    attributes:\n      label: Is your feature request related to a problem?\n      description: A clear and concise description of what the problem is.\n      placeholder: Ex. I'm always frustrated when I try to integrate with...\n    validations:\n      required: true\n\n  - type: textarea\n    id: solution\n    attributes:\n      label: The Solution\n      description: Describe the solution you'd like.\n      placeholder: |\n        I would like a new method `memori.recall_batch()` that accepts...\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives Considered\n      description: Describe any alternative solutions or features you've considered.\n      placeholder: I thought about using a loop, but it's too slow for my use case...\n\n  - type: dropdown\n    id: context\n    attributes:\n      label: Affected Components\n      description: Which parts of Memori would this feature touch?\n      multiple: true\n      options:\n        - LLM Provider / Adapter\n        - Vector Store / Memory\n        - Database Schema\n        - CLI\n        - Documentation\n        - Other\n\n  - type: checkboxes\n    id: help\n    attributes:\n      label: Participation\n      description: Would you be willing to help implement this?\n      options:\n        - label: I am willing to submit a pull request for this feature.\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'memori/**' \n  pull_request:\n    branches: [main]\n    paths:\n      - 'memori/**' \n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n\n      - name: Set up Python\n        run: uv python install 3.12\n\n      - name: Install dependencies\n        run: uv sync --dev\n\n      - name: Run ruff linting\n        run: uv run ruff check .\n\n      - name: Run ruff format check\n        run: uv run ruff format --check .\n\n  security:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n\n      - name: Set up Python\n        run: uv python install 3.12\n\n      - name: Install dependencies\n        run: uv sync --dev\n\n      - name: Run Bandit security checks\n        run: uv run bandit -r memori -ll -ii\n\n      - name: Run pip-audit for dependency vulnerabilities\n        run: uv run pip-audit --require-hashes --disable-pip\n        continue-on-error: true\n\n  type-check:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n\n      - name: Set up Python\n        run: uv python install 3.12\n\n      - name: Install dependencies\n        run: uv sync --dev\n\n      - name: Run type checking with ty\n        run: uvx ty check --exclude 'tests/llm/clients/**/*.py' --exclude 'tests/integration/**/*.py' --exclude 'benchmarks/**/*.py' --exclude 'examples/**/*.py'\n\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n\n      - name: Set up Python ${{ matrix.python-version }}\n        run: uv python install ${{ matrix.python-version }}\n\n      - name: Install dependencies\n        run: uv sync --dev\n\n      - name: Run pytest with coverage\n        run: uv run pytest --ignore=tests/benchmarks\n\n      - name: Upload coverage to Codecov\n        if: matrix.python-version == '3.12'\n        uses: codecov/codecov-action@v5\n        with:\n          files: ./coverage.xml\n          fail_ci_if_error: false\n"
  },
  {
    "path": ".github/workflows/integration.yml",
    "content": "name: Integration Tests\n\non:\n  workflow_dispatch:\n  workflow_call:\n    secrets:\n      OPENAI_API_KEY:\n        required: true\n      ANTHROPIC_API_KEY:\n        required: false\n      GOOGLE_API_KEY:\n        required: false\n      XAI_API_KEY:\n        required: false\n      AWS_ACCESS_KEY_ID:\n        required: false\n      AWS_SECRET_ACCESS_KEY:\n        required: false\n      MEMORI_API_KEY:\n        required: false\n\npermissions:\n  contents: read\n\njobs:\n  integration-tests:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n\n    services:\n      postgres:\n        image: postgres:15\n        env:\n          POSTGRES_USER: memori\n          POSTGRES_PASSWORD: memori\n          POSTGRES_DB: memori_test\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n      mysql:\n        image: mysql:8\n        env:\n          MYSQL_ROOT_PASSWORD: memori\n          MYSQL_DATABASE: memori_test\n          MYSQL_USER: memori\n          MYSQL_PASSWORD: memori\n        ports:\n          - 3306:3306\n        options: >-\n          --health-cmd \"mysqladmin ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n\n      mongodb:\n        image: mongo:6\n        env:\n          MONGO_INITDB_ROOT_USERNAME: memori\n          MONGO_INITDB_ROOT_PASSWORD: memori\n        ports:\n          - 27017:27017\n\n    env:\n      MEMORI_TEST_MODE: \"1\"\n      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n      GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}\n      XAI_API_KEY: ${{ secrets.XAI_API_KEY }}\n      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n      AWS_REGION: us-east-1\n      MEMORI_API_KEY: ${{ secrets.MEMORI_API_KEY }}\n      SQLITE_DATABASE_URL: sqlite:///test_memori.db\n      POSTGRES_DATABASE_URL: postgresql://memori:memori@localhost:5432/memori_test\n      MYSQL_DATABASE_URL: mysql+pymysql://memori:memori@localhost:3306/memori_test\n      MONGODB_URL: mongodb://memori:memori@localhost:27017/memori_test?authSource=admin\n\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          enable-cache: true\n\n      - name: Set up Python\n        run: uv python install 3.12\n\n      - name: Install dependencies\n        run: uv sync --dev\n\n      - name: Run AA Payload Unit Tests (no API keys needed)\n        run: |\n          echo \"Running AA payload unit tests...\"\n          uv run pytest tests/memory/augmentation/test_aa_payload_unit.py -v --tb=short\n\n      - name: Run AA Integration Tests (staging API)\n        if: env.OPENAI_API_KEY\n        run: |\n          echo \"Running AA integration tests against staging...\"\n          uv run pytest tests/integration/test_aa_payload.py -v -m integration --tb=short\n\n      - name: Run OpenAI Integration Tests\n        if: env.OPENAI_API_KEY\n        run: |\n          echo \"Running OpenAI integration tests...\"\n          uv run pytest tests/integration/providers/test_openai.py -v -m integration --tb=short\n\n      - name: Run Anthropic Integration Tests\n        if: env.ANTHROPIC_API_KEY\n        run: |\n          echo \"Running Anthropic integration tests...\"\n          uv run pytest tests/integration/providers/test_anthropic.py -v -m integration --tb=short\n\n      - name: Run Google Integration Tests\n        if: env.GOOGLE_API_KEY\n        run: |\n          echo \"Running Google integration tests...\"\n          uv run pytest tests/integration/providers/test_google.py -v -m integration --tb=short\n\n      - name: Run xAI Integration Tests\n        if: env.XAI_API_KEY\n        run: |\n          echo \"Running xAI integration tests...\"\n          uv run pytest tests/integration/providers/test_xai.py -v -m integration --tb=short\n\n      - name: Run Bedrock Integration Tests\n        if: env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY\n        run: |\n          echo \"Running Bedrock integration tests...\"\n          uv run pytest tests/integration/providers/test_bedrock.py -v -m integration --tb=short\n\n      - name: Run cloud OpenAI Integration Tests\n        if: env.OPENAI_API_KEY && env.MEMORI_API_KEY\n        run: |\n          echo \"Running cloud OpenAI integration tests...\"\n          uv run pytest tests/integration/cloud/test_cloud_openai.py -v -m integration --tb=short\n\n      - name: Run cloud Anthropic Integration Tests\n        if: env.ANTHROPIC_API_KEY && env.MEMORI_API_KEY\n        run: |\n          echo \"Running cloud Anthropic integration tests...\"\n          uv run pytest tests/integration/cloud/test_cloud_anthropic.py -v -m integration --tb=short\n\n      - name: Run cloud Google Integration Tests\n        if: env.GOOGLE_API_KEY && env.MEMORI_API_KEY\n        run: |\n          echo \"Running cloud Google integration tests...\"\n          uv run pytest tests/integration/cloud/test_cloud_gemini.py -v -m integration --tb=short\n\n      - name: Run cloud xAI Integration Tests\n        if: env.XAI_API_KEY && env.MEMORI_API_KEY\n        run: |\n          echo \"Running cloud xAI integration tests...\"\n          uv run pytest tests/integration/cloud/test_cloud_xai.py -v -m integration --tb=short\n\n      - name: Run cloud Bedrock Integration Tests\n        if: env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY && env.MEMORI_API_KEY\n        run: |\n          echo \"Running cloud Bedrock integration tests...\"\n          uv run pytest tests/integration/cloud/test_cloud_bedrock.py -v -m integration --tb=short\n\n      - name: Run Database Integration Tests\n        if: env.OPENAI_API_KEY\n        run: |\n          echo \"Running database integration tests...\"\n          uv run pytest tests/integration/databases/ -v -m integration --tb=short\n\n      - name: Integration Test Summary\n        if: always()\n        run: |\n          echo \"==========================================\"\n          echo \"Integration Test Summary\"\n          echo \"==========================================\"\n          echo \"MEMORI_TEST_MODE: $MEMORI_TEST_MODE (staging)\"\n          echo \"\"\n          echo \"Tests run:\"\n          echo \"  - AA Payload Unit Tests: Yes (no API keys needed)\"\n          if [ \"$OPENAI_API_KEY\" ]; then echo \"  - AA Integration Tests: Yes\"; else echo \"  - AA Integration Tests: Skipped (no key)\"; fi\n          if [ \"$OPENAI_API_KEY\" ]; then echo \"  - Database Integration Tests: Yes\"; else echo \"  - Database Integration Tests: Skipped (no key)\"; fi\n          echo \"\"\n          echo \"Databases tested:\"\n          echo \"  - SQLite: Yes\"\n          echo \"  - PostgreSQL: Yes\"\n          echo \"  - MySQL: Yes\"\n          echo \"  - MongoDB: Yes\"\n          echo \"\"\n          echo \"Providers tested:\"\n          if [ \"$OPENAI_API_KEY\" ]; then echo \"  - OpenAI: Yes\"; else echo \"  - OpenAI: Skipped (no key)\"; fi\n          if [ \"$ANTHROPIC_API_KEY\" ]; then echo \"  - Anthropic: Yes\"; else echo \"  - Anthropic: Skipped (no key)\"; fi\n          if [ \"$GOOGLE_API_KEY\" ]; then echo \"  - Google: Yes\"; else echo \"  - Google: Skipped (no key)\"; fi\n          if [ \"$XAI_API_KEY\" ]; then echo \"  - xAI: Yes\"; else echo \"  - xAI: Skipped (no key)\"; fi\n          if [ \"$AWS_ACCESS_KEY_ID\" ]; then echo \"  - Bedrock: Yes\"; else echo \"  - Bedrock: Skipped (no key)\"; fi\n          echo \"\"\n          echo \"Cloud (requires MEMORI_API_KEY) tests:\"\n          if [ \"$MEMORI_API_KEY\" ] && [ \"$OPENAI_API_KEY\" ]; then echo \"  - Cloud OpenAI: Yes\"; else echo \"  - Cloud OpenAI: Skipped (no key)\"; fi\n          if [ \"$MEMORI_API_KEY\" ] && [ \"$ANTHROPIC_API_KEY\" ]; then echo \"  - Cloud Anthropic: Yes\"; else echo \"  - Cloud Anthropic: Skipped (no key)\"; fi\n          if [ \"$MEMORI_API_KEY\" ] && [ \"$GOOGLE_API_KEY\" ]; then echo \"  - Cloud Google: Yes\"; else echo \"  - Cloud Google: Skipped (no key)\"; fi\n          if [ \"$MEMORI_API_KEY\" ] && [ \"$XAI_API_KEY\" ]; then echo \"  - Cloud xAI: Yes\"; else echo \"  - Cloud xAI: Skipped (no key)\"; fi\n          if [ \"$MEMORI_API_KEY\" ] && [ \"$AWS_ACCESS_KEY_ID\" ]; then echo \"  - Cloud Bedrock: Yes\"; else echo \"  - Cloud Bedrock: Skipped (no key)\"; fi\n          echo \"==========================================\"\n"
  },
  {
    "path": ".github/workflows/publish-npm.yml",
    "content": "name: Publish to npm (TS)\n\non:\n  workflow_dispatch: \n\npermissions:\n  contents: read\n  id-token: write\n\njobs:\n  publish:\n    runs-on: ubuntu-latest  \n    \n    defaults:\n      run:\n        working-directory: ./memori-ts\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: \"24\"\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Generate version.ts\n        run: |\n          VERSION=$(node -p \"require('./package.json').version\")\n          echo \"export const SDK_VERSION = '${VERSION}';\" > src/version.ts\n\n      - name: Build project\n        run: npm run build --if-present\n        \n      - name: Run tests\n        run: npm test --if-present\n\n      - name: Publish Package\n        run: npm publish --access public --tag latest"
  },
  {
    "path": ".github/workflows/publish-openclaw-plugin.yml",
    "content": "name: Publish OpenClaw to NPM\n\non:\n  workflow_dispatch: \n\npermissions:\n  contents: read\n  id-token: write\n\njobs:\n  publish:\n    runs-on: ubuntu-latest  \n    \n    defaults:\n      run:\n        working-directory: ./integrations/openclaw\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: \"24\"\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Generate version.ts\n        run: |\n          VERSION=$(node -p \"require('./package.json').version\")\n          echo \"export const SDK_VERSION = '${VERSION}';\" > src/version.ts\n\n      - name: Build project\n        run: npm run build --if-present\n\n      - name: Publish Package\n        run: npm publish --access public --tag latest"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish to PyPI\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  id-token: write\n\nconcurrency:\n  group: publish-pypi-${{ github.ref }}\n  cancel-in-progress: false\n\njobs:\n  # Run integration tests before publishing\n  integration-tests:\n    uses: ./.github/workflows/integration.yml\n    secrets:\n      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n      GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}\n      XAI_API_KEY: ${{ secrets.XAI_API_KEY }}\n      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n\n  deploy:\n    needs: integration-tests\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: '3.x'\n          cache: 'pip'\n\n      - name: Install build tools\n        run: pip install --upgrade pip build twine toml\n\n      - name: Build memori package\n        run: python -m build --outdir dist/memori\n\n      - name: Verify memori distribution\n        run: twine check dist/memori/*\n\n      - name: Publish memori to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          packages-dir: dist/memori/\n          user: __token__\n          password: ${{ secrets.PYPI_API_TOKEN }}\n\n      - name: Update package name to memorisdk\n        run: |\n          python -c \"\n            import toml\n            with open('pyproject.toml', 'r') as f:\n                config = toml.load(f)\n            config['project']['name'] = 'memorisdk'\n            with open('pyproject.toml', 'w') as f:\n                toml.dump(config, f)\n          \"\n\n      - name: Build memorisdk package\n        run: python -m build --outdir dist/memorisdk\n\n      - name: Verify memorisdk distribution\n        run: twine check dist/memorisdk/*\n\n      - name: Publish memorisdk to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          packages-dir: dist/memorisdk/\n          user: __token__\n          password: ${{ secrets.PYPI_SDK_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/ts-ci.yml",
    "content": "name: CI Code Check (TS)\n\non:\n  push:\n    branches: [ \"main\" ]\n    paths:\n      - 'memori-ts/**' \n  pull_request:\n    branches: [ \"main\" ]\n    paths:\n      - 'memori-ts/**'\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [18, 20, 22, 24]\n    \n    defaults:\n      run:\n        working-directory: ./memori-ts \n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n\n      - name: Setup Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Generate version.ts\n        run: |\n          VERSION=$(node -p \"require('./package.json').version\")\n          echo \"export const SDK_VERSION = '${VERSION}';\" > src/version.ts\n\n      - name: Run Linter\n        run: npm run lint\n\n      - name: Run Tests\n        run: npm test\n\n      - name: Verify Build\n        run: npm run build"
  },
  {
    "path": ".github/workflows/ts-openclaw-ci.yml",
    "content": "name: CI Code Check (TS)\n\non:\n  push:\n    branches: [ \"main\" ]\n    paths:\n      - 'integrations/openclaw/**' \n  pull_request:\n    branches: [ \"main\" ]\n    paths:\n      - 'integrations/openclaw/**'\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [22, 24]\n    \n    defaults:\n      run:\n        working-directory: ./integrations/openclaw\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n\n      - name: Setup Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Generate version.ts\n        run: |\n          VERSION=$(node -p \"require('./package.json').version\")\n          echo \"export const SDK_VERSION = '${VERSION}';\" > src/version.ts\n\n      - name: Run Linter\n        run: npm run lint\n\n      - name: Verify Build\n        run: npm run build"
  },
  {
    "path": ".gitignore",
    "content": "# Python\n*.pyc\n*.pyo\n*.pyd\n__pycache__\n*.so\n*.egg\n*.egg-info\ndist/\n/build/\n*.swp\n\n# Testing\n.pytest_cache\n.coverage\ncoverage.xml\nhtmlcov/\n.tox\n\n# IDEs\n.DS_Store\n.idea\n.vscode\n*.swo\n\n# Virtual environments\nvenv/\nenv/\n.venv/\n\n# Environment variables\n.env\n\n# SQLite databases\n*.db\n*.db-journal\n\n# UV\nuv.lock\n\n# Python packaging\n*.egg-info/\ndist/\n/build/\n\n# Sensitive files (do not commit)\ngoogle-credentials.json\n*.pem\n*.key\n\nAGENTS.md\n\ntests/examples/*\n\n# Benchmarking results\ntests/benchmarks/results/\nresults/\n*.json\n*.csv\n!pyproject.toml\n!package.json\n!composer.json\n\n# Node / TypeScript\nnode_modules/\ndist/\n*.tsbuildinfo\ncoverage\n!package-lock.json\n!tsconfig.json\n!tsconfig.build.json\n!.prettierrc.json\n!openclaw.plugin.json"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n- repo: https://github.com/pre-commit/pre-commit-hooks\n  rev: v4.5.0\n  hooks:\n  - id: check-yaml\n  - id: end-of-file-fixer\n  - id: trailing-whitespace\n  - id: debug-statements\n\n- repo: https://github.com/astral-sh/ruff-pre-commit\n  rev: v0.15.1\n  hooks:\n  - id: ruff-check\n    args: [--fix]\n  - id: ruff-format\n\n- repo: local\n  hooks:\n  # - id: ty\n  #   name: ty type checker\n  #   entry: uvx ty check\n  #   language: system\n  #   types: [python]\n  #   pass_filenames: false\n  #   always_run: true\n\n  - id: pytest\n    name: pytest\n    entry: uv run pytest --ignore=tests/benchmarks --ignore=tests/integration\n    language: system\n    pass_filenames: false\n    always_run: true\n\n  - id: ts-format\n    name: format (memori-ts)\n    entry: bash -c 'cd memori-ts && npm run format'\n    language: system\n    pass_filenames: false\n    files: ^memori-ts/\n\n  - id: ts-lint\n    name: lint (memori-ts)\n    entry: bash -c 'cd memori-ts && npm run lint:fix'\n    language: system\n    pass_filenames: false\n    files: ^memori-ts/\n\n  - id: ts-typecheck\n    name: typecheck (memori-ts)\n    entry: bash -c 'cd memori-ts && npm run typecheck'\n    language: system\n    pass_filenames: false\n    files: ^memori-ts/\n\n  - id: ts-test\n    name: test (memori-ts)\n    entry: bash -c 'cd memori-ts && npm run test'\n    language: system\n    pass_filenames: false\n    files: ^memori-ts/"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to the Memori Python SDK will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Fixed\n\n- Fixed multi-turn conversation ingestion for AzureOpenAI and OpenAI clients. Previously, only the first conversation turn was being recorded. Now `conversation_id` is resolved early in the request lifecycle, ensuring all conversation turns are properly ingested into the same conversation. (Fixes #83)\n\n[3.0.0]: https://github.com/MemoriLabs/Memori/releases/tag/v3.0.0\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "[![Memori Labs](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/banner.png)](https://memorilabs.ai/)\n\n# Contributing to Memori Python SDK\n\nThank you for your interest in contributing to Memori!\n\n## Development Setup\n\nWe use `uv` for fast dependency management and Docker for integration testing. You can develop locally or use our Docker environment.\n\n### Prerequisites\n\n- Python 3.10+ (3.12 recommended)\n- [uv](https://github.com/astral-sh/uv) - Fast Python package installer\n- Docker and Docker Compose (for integration tests)\n- Make\n\n### Quick Start (Local Development)\n\n```bash\n# Install uv if you haven't already\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# Clone the repository\ngit clone https://github.com/MemoriLabs/Memori.git\ncd Memori\n\n# Install dependencies\nuv sync\n\n# Install pre-commit hooks\nuv run pre-commit install\n\n# Run unit tests\nuv run pytest\n```\n\n### Quick Start (Docker)\n\n```bash\n# Copy the example environment file\ncp .env.example .env\n\n# Edit .env and add your API keys (optional for unit tests)\n# Required for integration tests: OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY\n\n# Start the environment\nmake dev-up\n```\n\nThis will:\n- Build the Docker container with Python 3.12\n- Install all dependencies with uv\n- Start PostgreSQL, MySQL, and MongoDB for integration tests\n- Start Mongo Express (web UI for MongoDB at http://localhost:8081)\n\n### Development Commands\n\n#### Local Development\n```bash\n# Run unit tests\nuv run pytest\n\n# Format code\nuv run ruff format .\n\n# Check linting\nuv run ruff check .\n\n# Run with coverage\nuv run pytest --cov=memori\n\n# Run security scans\nuv run bandit -r memori -ll -ii\nuv run pip-audit --require-hashes --disable-pip || true\n```\n\n#### Docker Development\n```bash\n# Enter the development container\nmake dev-shell\n\n# Run unit tests (fast, no external dependencies)\nmake test\n\n# Initialize database schemas\nmake init-postgres  # PostgreSQL\nmake init-mysql     # MySQL\nmake init-oceanbase # OceanBase\nmake init-mongodb   # MongoDB\nmake init-sqlite    # SQLite\n\n# Run a specific integration test script\nmake run-integration FILE=tests/llm/clients/oss/openai/async.py\n\n# Format code\nmake format\n\n# Check linting\nmake lint\n\n# Run security scans\nmake security\n\n# Stop the environment\nmake dev-down\n\n# Clean up everything (containers, volumes, cache)\nmake clean\n```\n\n## Testing\n\nWe use `pytest` with coverage reporting and `pytest-mock` for mocking.\n\n### Unit Tests\nUnit tests use mocks and run without external dependencies:\n```bash\n# Local\nuv run pytest\n\n# Docker\nmake test\n```\n\n### Integration Tests\nIntegration tests require:\n- Database instances (PostgreSQL, MySQL, MongoDB, or SQLite)\n- LLM API keys (OpenAI, Anthropic, Google)\n\n```bash\n# Set API keys in .env first\n# OPENAI_API_KEY=sk-...\n# ANTHROPIC_API_KEY=sk-ant-...\n# GOOGLE_API_KEY=...\n\n# Initialize database schema\nmake init-postgres  # or init-mysql, init-oceanbase, init-mongodb, init-sqlite\n\n# Run integration test scripts\nmake run-integration FILE=tests/llm/clients/oss/openai/sync.py\n```\n\n### Test Coverage\n\nWe maintain high test coverage. Coverage reports are generated automatically:\n- Terminal output (summary)\n- HTML report in `htmlcov/`\n- XML report in `coverage.xml`\n\nView HTML coverage:\n```bash\nopen htmlcov/index.html  # macOS\nxdg-open htmlcov/index.html  # Linux\n```\n\n## Project Structure\n\n```\nmemori/              # SDK source code\n  llm/               # LLM provider integrations (OpenAI, Anthropic, Google, etc.)\n  memory/            # Memory system and augmentation\n  storage/           # Storage adapters (PostgreSQL, MySQL, MongoDB, SQLite, etc.)\n  api/               # API client for Memori Advanced Augmentation\n  __init__.py        # Main Memori class and public API\n  py.typed           # PEP 561 type hint marker\ntests/               # Test files\n  build/             # Database initialization scripts\n  llm/               # LLM provider tests (unit & integration)\n  memory/            # Memory system tests\n  storage/           # Storage adapter tests\nconftest.py          # Pytest fixtures\npyproject.toml       # Project metadata and dependencies\nuv.lock              # Locked dependency versions\nCHANGELOG.md         # Version history\n```\n\n## Code Quality\n\nWe use [Ruff](https://docs.astral.sh/ruff/) for linting and formatting (configured in `pyproject.toml`):\n\n```bash\n# Format code\nuv run ruff format .     # or: make format\n\n# Check linting\nuv run ruff check .      # or: make lint\n\n# Auto-fix issues\nuv run ruff check --fix .\n\n# Run security scans (Bandit + pip-audit)\nuv run bandit -r memori -ll -ii\nuv run pip-audit --require-hashes --disable-pip || true\n```\n\n### Pre-commit Hooks\n\nWe use pre-commit to automatically format and lint code:\n\n```bash\n# Install hooks (one-time setup)\nuv run pre-commit install\n\n# Run manually\nuv run pre-commit run --all-files\n```\n\n### Code Standards\n\n- Follow PEP 8 standards\n- Line length: 88 characters (Black-compatible)\n- Python 3.10+ syntax (use modern type hints)\n- All public APIs must have type hints\n- Lean, simple code preferred over complex solutions (KISS, YAGNI)\n- Minimize unnecessary comments - code should be self-documenting\n\n## Pull Request Guidelines\n\n1. **Fork and branch**: Create a feature branch from `main`\n2. **Write tests**: Add/update tests for your changes\n3. **Pass all checks**: Ensure tests, linting, and formatting pass\n4. **Update docs**: Update README or docs if adding features\n5. **Changelog**: Add entry to CHANGELOG.md under \"Unreleased\"\n6. **Atomic commits**: Keep commits focused and well-described\n\n## Supported Integrations\n\n### LLM Providers\n- OpenAI (sync/async, streaming)\n- Anthropic Claude (sync/async, streaming)\n- Google Gemini (sync/async, streaming)\n- AWS Bedrock\n\n### Frameworks\n- Agno\n- LangChain\n\n### Database Adapters\n- PostgreSQL (via psycopg2, psycopg3)\n- MySQL / MariaDB (via pymysql)\n- MongoDB (via pymongo)\n- Oracle (via cx_Oracle, python-oracledb)\n- SQLite (stdlib)\n- CockroachDB\n- Neon, Supabase (PostgreSQL-compatible)\n- Django ORM\n- DB-API 2.0 compatible connections\n\n## CLI Commands\n\nMemori provides CLI commands for managing your account and quota:\n\n```bash\n# Check your API quota\npython3 -m memori quota\n\n# Sign up for Memori Advanced Augmentation\npython3 -m memori sign-up <email_address>\n```\n\nThese commands help you:\n- Monitor your memory quota and usage\n- Sign up for increased limits (always free for developers)\n- Obtain API keys for Advanced Augmentation features\n\n## Development Notes\n\n- Docker files (Dockerfile, docker-compose.yml, Makefile) are for development only\n- They are NOT included in the PyPI package\n- The SDK has minimal runtime dependencies - fully self-contained\n- Development dependencies (LLM clients, database drivers) are in `[dependency-groups]`\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Use Python 3.12 as base image\nFROM python:3.12-slim\n\n# Set working directory\nWORKDIR /app\n\n# Install system dependencies\nRUN apt-get update && apt-get install -y \\\n    git \\\n    curl \\\n    build-essential \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Install uv for faster dependency management\nRUN pip install uv\n\n# Copy dependency files\nCOPY pyproject.toml uv.lock* ./\n\n# Install dependencies (including dev dependencies)\nRUN uv sync --all-extras\n\n# Copy the rest of the application\nCOPY . .\n\n# Install pre-commit hooks\nRUN pip install pre-commit && pre-commit install || true\n\n# Add venv to PATH so all tools are available\nENV PATH=\"/app/.venv/bin:$PATH\"\n\n# Default command opens a bash shell\nCMD [\"/bin/bash\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction,\nand distribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all\nother entities that control, are controlled by, or are under common\ncontrol with that entity. For the purposes of this definition,\n\"control\" means (i) the power, direct or indirect, to cause the\ndirection or management of such entity, whether by contract or\notherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity\nexercising permissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications,\nincluding but not limited to software source code, documentation\nsource, and configuration files.\n\n\"Object\" form shall mean any form resulting from mechanical\ntransformation or translation of a Source form, including but\nnot limited to compiled object code, generated documentation,\nand conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or\nObject form, made available under the License, as indicated by a\ncopyright notice that is included in or attached to the work\n(which shall not include communications that are reasonably\nconsidered separate from, or merely to link to (or bind by name)\nthe interfaces of, the Work and separate works which communicate\nwith the Work solely through the Work's public interfaces).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object\nform, that is based upon (or derived from) the Work and for which the\neditorial revisions, annotations, elaborations, or other modifications\nrepresent, as a whole, an original work of authorship. For the purposes\nof this License, Derivative Works shall not include works that remain\nseparable from, or merely link (or bind by name) to the interfaces of,\nthe Work and separate works which communicate with the Work solely\nthrough the Work's public interfaces.\n\n\"Contribution\" shall mean any work of authorship, including\nthe original version of the Work and any modifications or additions\nto that Work or Derivative Works thereof, that is intentionally\nsubmitted to Licensor for inclusion in the Work by the copyright owner\nor by an individual or Legal Entity authorized to submit on behalf of\nthe copyright owner. For the purposes of this definition, \"submitted\"\nmeans any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control\nsystems, and issue tracking systems that are managed by, or on behalf\nof, the Licensor for the purpose of discussing and improving the Work,\nbut excluding communication that is conspicuously marked or otherwise\ndesignated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity\non behalf of whom a Contribution has been received by Licensor and\nsubsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\nthis License, each Contributor hereby grants to You a perpetual,\nworldwide, non-exclusive, no-charge, royalty-free, irrevocable\ncopyright license to use, reproduce, modify, distribute, and prepare\nDerivative Works of the Work, and to publicly display and perform the\nWork and such Derivative Works in all media and formats whether now\nknown or hereafter devised.\n\n3. Grant of Patent License. Subject to the terms and conditions of\nthis License, each Contributor hereby grants to You a perpetual,\nworldwide, non-exclusive, no-charge, royalty-free, irrevocable\n(except as stated in this section) patent license to make, have made,\nuse, offer to sell, sell, import, and otherwise transfer the Work,\nwhere such license applies only to those patent claims licensable\nby such Contributor that are necessarily infringed by their\nContribution(s) alone or by combination of their Contribution(s)\nwith the Work to which such Contribution(s) was submitted. If You\ninstitute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work\nor a Contribution incorporated within the Work constitutes direct\nor contributory patent infringement, then any patent licenses\ngranted to You under this License for that Work shall terminate\nas of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\nWork or Derivative Works thereof in any medium, with or without\nmodifications, and in Source or Object form, provided that You\nmeet the following conditions:\n\n(a) You must give any other recipients of the Work or\nDerivative Works a copy of this License; and\n\n(b) You must cause any modified files to carry prominent notices\nstating that You changed the files; and\n\n(c) You must retain, in the Source form of any Derivative Works\nthat You distribute, all copyright, trademark, patent,\nattribution and other notices from the Source form of the Work,\nexcluding those notices that do not pertain to any part of\nthe Derivative Works; and\n\n(d) If the Work includes a \"NOTICE\" text file as part of its\ndistribution, then any Derivative Works that You distribute must\ninclude a readable copy of the attribution notices contained\nwithin such NOTICE file, excluding those notices that do not\npertain to any part of the Derivative Works, in at least one\nof the following places: within a NOTICE text file distributed\nas part of the Derivative Works; within the Source form or\ndocumentation, if provided along with the Derivative Works; or,\nwithin a display generated by the Derivative Works, if and\nwherever such third-party notices normally appear. The contents\nof the NOTICE file are for informational purposes only and\ndo not modify the License. You may add Your own attribution\nnotices within Derivative Works that You distribute, alongside\nor as an addendum to the NOTICE text from the Work, provided\nthat such additional attribution notices cannot be construed\nas modifying the License.\n\nYou may add Your own copyright notice to Your modifications and\nmay provide additional or different license terms and conditions\nfor use, reproduction, or distribution of Your modifications, or\nfor any such Derivative Works as a whole, provided Your use,\nreproduction, and distribution of the Work otherwise complies with\nthe conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\nany Contribution intentionally submitted for inclusion in the Work\nby You to the Licensor shall be under the terms and conditions of\nthis License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify\nthe terms of any separate license agreement you may have executed\nwith Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\nnames, trademarks, service marks, or product names of the Licensor,\nexcept as required for reasonable and customary use in describing the\norigin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\nagreed to in writing, Licensor provides the Work (and each\nContributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\nimplied, including, without limitation, any warranties or conditions\nof TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\nPARTICULAR PURPOSE. You are solely responsible for determining the\nappropriateness of using or redistributing the Work and assume any\nrisks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\nwhether in tort (including negligence), contract, or otherwise,\nunless required by applicable law (such as deliberate and grossly\nnegligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special,\nincidental, or consequential damages of any character arising as a\nresult of this License or out of the use or inability to use the\nWork (including but not limited to damages for loss of goodwill,\nwork stoppage, computer failure or malfunction, or any and all\nother commercial damages or losses), even if such Contributor\nhas been advised of the possibility of such damages.\n\n9. Accepting Warranty or Support. You may choose to offer, and to\ncharge a fee for, warranty, support, indemnity or other liability\nobligations and/or rights consistent with this License. However, in\naccepting such obligations, You may act only on Your own behalf and on\nYour sole responsibility, not on behalf of any other Contributor, and\nonly if You agree to indemnify, defend, and hold each Contributor\nharmless for any liability incurred by, or claims asserted against,\nsuch Contributor by reason of your accepting any such warranty or support.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\nTo apply the Apache License to your work, attach the following\nboilerplate notice, with the fields enclosed by brackets \"[]\"\nreplaced with your own identifying information. (Don't include\nthe brackets!)  The text should be enclosed in the appropriate\ncomment syntax for the file format. We also recommend that a\nfile or class name and description of purpose be included on the\nsame \"printed page\" as the copyright notice for easier\nidentification within third-party archives.\n\nCopyright [2025] [Memori Team]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "# Include essential files\ninclude README.md\ninclude LICENSE\n\n# Exclude all development and Docker files\nexclude Dockerfile\nexclude docker-compose.yml\nexclude .dockerignore\nexclude Makefile\nexclude .env.example\nexclude conftest.py\nexclude CONTRIBUTING.md\nexclude .pre-commit-config.yaml\nexclude uv.lock\nexclude google-credentials.json\nexclude .coverage\nexclude coverage.xml\nexclude CHANGELOG.md\nexclude insert_facts.py\nexclude memori_test.db\n\n# Exclude test directories\nprune tests\n\n# Exclude documentation build artifacts\nprune docs\nprune htmlcov\n\n# Exclude TypeScript SDK (not shipped to PyPI)\nprune memori-ts\nprune integrations\n\n# Exclude benchmarks (not shipped with the SDK)\nprune benchmarks\n\n# Exclude version control and CI\nprune .git\nprune .github\nexclude .gitignore\n\n# Exclude IDE and editor files\nprune .vscode\nprune .venv\nprune .pytest_cache\nprune .ruff_cache\nglobal-exclude .DS_Store\nglobal-exclude *.pyc\nglobal-exclude *.pyo\nglobal-exclude __pycache__\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: help dev-up dev-down dev-shell dev-build dev-clean test lint format clean run-unit run-integration run-integration-provider run-integration-cloud\n\nhelp: ## Show this help message\n\t@echo 'Usage: make [target]'\n\t@echo ''\n\t@echo 'Available targets:'\n\t@awk 'BEGIN {FS = \":.*?## \"} /^[a-zA-Z_-]+:.*?## / {printf \"  %-20s %s\\n\", $$1, $$2}' $(MAKEFILE_LIST)\n\ndev-up: ## Start development environment (builds and runs containers)\n\tdocker compose up -d --build\n\t@echo \"\"\n\t@echo \"✓ Development environment is ready!\"\n\t@echo \"  Run 'make dev-shell' to enter the development container\"\n\t@echo \"  Run 'make test' to run tests\"\n\ndev-down: ## Stop development environment\n\tdocker compose down\n\ndev-shell: ## Open a shell in the development container\n\tdocker compose exec dev /bin/bash\n\ninit-db: ## Initialize database schema for integration tests\n\tdocker compose exec -e PYTHONPATH=/app dev python tests/database/init_db.py\n\ninit-postgres: ## Initialize PostgreSQL schema\n\tdocker compose exec -e PYTHONPATH=/app dev python tests/build/postgresql.py\n\ninit-mysql: ## Initialize MySQL schema\n\tdocker compose exec -e PYTHONPATH=/app dev python tests/build/mysql.py\n\ninit-oceanbase: ## Initialize OceanBase schema\n\tdocker compose exec -e PYTHONPATH=/app dev python tests/build/oceanbase.py\n\ninit-oracle: ## Initialize Oracle schema\n\tdocker compose exec -e PYTHONPATH=/app dev python tests/build/oracle.py\n\ninit-mongodb: ## Initialize MongoDB schema\n\tdocker compose exec -e PYTHONPATH=/app dev python tests/build/mongodb.py\n\ninit-sqlite: ## Initialize SQLite schema\n\tdocker compose exec -e PYTHONPATH=/app dev python tests/build/sqlite.py\n\ndev-build: ## Rebuild the development container\n\tdocker compose build --no-cache\n\ndev-clean: ## Complete teardown: stop containers, remove images, prune build cache\n\tdocker compose down -v\n\tdocker builder prune -f\n\tdocker compose rm -f\n\t@echo \"✓ Docker environment cleaned (containers, volumes, and build cache removed)\"\n\ntest: ## Run tests in the container\n\tdocker compose exec dev pytest\n\nrun-unit: ## Run unit tests (no API keys needed)\n\t@echo \"Running unit tests...\"\n\tuv run pytest tests/ --ignore=tests/integration --ignore=tests/benchmarks -v --tb=short\n\nrun-integration: ## Run all integration tests (requires API keys)\n\t@echo \"Running all integration tests with MEMORI_TEST_MODE=1...\"\n\tMEMORI_TEST_MODE=1 uv run pytest tests/integration/ -v -m integration --tb=short\n\nrun-integration-provider: ## Run specific provider tests (e.g., make run-integration-provider P=openai)\n\t@echo \"Running $(P) integration tests...\"\n\tMEMORI_TEST_MODE=1 uv run pytest tests/integration/providers/test_$(P).py -v -m integration --tb=short\n\nrun-integration-cloud: ## Run cloud integration tests (production API, requires MEMORI_API_KEY)\n\t@echo \"Running cloud integration tests...\"\n\tuv run pytest tests/integration/cloud/ -v -m integration --tb=short\n\nlint: ## Run linting (format check)\n\tdocker compose exec dev uv run ruff check .\n\nsecurity: ## Run security scans (Bandit + pip-audit)\n\tdocker compose exec dev uv run bandit -r memori -ll -ii\n\tdocker compose exec dev uv run pip-audit --require-hashes --disable-pip || true\n\nformat: ## Format code\n\tdocker compose exec dev uv run ruff format .\n\nclean: ## Clean up containers, volumes, and Python cache files\n\tdocker compose down -v\n\tfind . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true\n\tfind . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true\n\tfind . -type d -name \"*.egg-info\" -exec rm -rf {} + 2>/dev/null || true\n"
  },
  {
    "path": "README.md",
    "content": "[![Memori Labs](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/banner.png)](https://memorilabs.ai/)\n\n<p align=\"center\">\n  <strong>The memory fabric for enterprise AI</strong>\n</p>\n\n<p align=\"center\">\n  <i>Memori plugs into the software and infrastructure you already use. It is LLM, datastore and framework agnostic and seamlessly integrates into the architecture you've already designed.</i>\n</p>\n\n<p align=\"center\">\n  <strong>→ <a href=\"https://memorilabs.ai/docs/memori-cloud/\">Memori Cloud</a></strong> — Zero config. Get an API key and start building in minutes.\n</p>\n<p align=\"center\">\n  <a href=\"https://trendshift.io/repositories/15418\">\n    <img src=\"https://trendshift.io/_next/image?url=https%3A%2F%2Ftrendshift.io%2Fapi%2Fbadge%2Frepositories%2F15418&w=640&q=75\" alt=\"Memori%2fLabs%2FMemori | Trendshif\">\n  </a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://badge.fury.io/py/memori\">\n    <img src=\"https://badge.fury.io/py/memori.svg\" alt=\"PyPI version\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@memorilabs/memori\">\n    <img src=\"https://img.shields.io/npm/v/@memorilabs/memori.svg\" alt=\"NPM version\">\n  </a>\n  <a href=\"https://pepy.tech/projects/memori\">\n    <img src=\"https://static.pepy.tech/badge/memori\" alt=\"Downloads\">\n  </a>\n  <a href=\"https://opensource.org/license/apache-2-0\">\n    <img src=\"https://img.shields.io/badge/license-Apache%202.0-blue\" alt=\"License\">\n  </a>\n  <a href=\"https://discord.gg/abD4eGym6v\">\n    <img src=\"https://img.shields.io/discord/1042405378304004156?logo=discord\" alt=\"Discord\">\n  </a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/MemoriLabs/Memori/stargazers\">\n    <img src=\"https://img.shields.io/badge/⭐%20Give%20a%20Star-Support%20the%20project-orange?style=for-the-badge\" alt=\"Give a Star\">\n  </a>\n</p>\n\n---\n\n## Getting Started\n\n### Installation\n\n<details open>\n<summary><b>TypeScript SDK</b></summary>\n\n```bash\nnpm install @memorilabs/memori\n```\n</details>\n\n<details>\n<summary><b>Python SDK</b></summary>\n\n```bash\npip install memori\n```\n</details>\n\n### Quickstart\n\nSign up at [app.memorilabs.ai](https://app.memorilabs.ai), get a Memori API key, and start building. Full docs: [memorilabs.ai/docs/memori-cloud/](https://memorilabs.ai/docs/memori-cloud/).\n\nSet `MEMORI_API_KEY` and your LLM API key (e.g. `OPENAI_API_KEY`), then:\n\n<details open>\n<summary><b>TypeScript SDK</b></summary>\n\n```typescript\nimport { OpenAI } from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\n// Requires MEMORI_API_KEY and OPENAI_API_KEY in your environment\nconst client = new OpenAI();\nconst mem = new Memori().llm\n  .register(client)\n  .attribution('user_123', 'support_agent');\n\nasync function main() {\n  await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: 'My favorite color is blue.' }],\n  });\n  // Conversations are persisted and recalled automatically in the background.\n\n  const response = await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: \"What's my favorite color?\" }],\n  });\n  // Memori recalls that your favorite color is blue.\n}\n```\n</details>\n\n<details>\n<summary><b>Python SDK</b></summary>\n\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\n# Requires MEMORI_API_KEY and OPENAI_API_KEY in your environment\nclient = OpenAI()\nmem = Memori().llm.register(client)\n\nmem.attribution(entity_id=\"user_123\", process_id=\"support_agent\")\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"My favorite color is blue.\"}]\n)\n# Conversations are persisted and recalled automatically.\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}]\n)\n# Memori recalls that your favorite color is blue.\n```\n</details>\n\n## Explore the Memories\n\nUse the [Dashboard](https://app.memorilabs.ai) — Memories, Analytics, Playground, and API Keys.\n\n> [!TIP]\n> Want to use your own database? Check out docs for Memori BYODB here:\n> [https://memorilabs.ai/docs/memori-byodb/](https://memorilabs.ai/docs/memori-byodb/).\n\n## LoCoMo Benchmark\n\nMemori was evaluated on the LoCoMo benchmark for long-conversation memory and achieved **81.95% overall accuracy** while using an average of **1,294 tokens per query**. That is just **4.97% of the full-context footprint**, showing that structured memory can preserve reasoning quality without forcing large prompts into every request.\n\nCompared with other retrieval-based memory systems, Memori outperformed Zep, LangMem, and Mem0 while reducing prompt size by roughly **67% vs. Zep** and lowering context cost by more than **20x vs. full-context prompting**.\n\nRead the [benchmark overview](docs/memori-cloud/benchmark/overview.mdx), see the [results](docs/memori-cloud/benchmark/results.mdx), or download the [paper](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-locomo-benchmark.pdf).\n\n![\"Memori's average accuracy along with the standard deviation\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-locomo-benchmark.webp)\n\n## OpenClaw (Persistent Memory for Your Gateway)\n\nBy default, OpenClaw agents forget everything between sessions. The Memori plugin fixes that. It captures durable facts and preferences after each conversation, then injects the most relevant context back into future prompts automatically.\n\nNo changes to your agent code or prompts are required. The plugin hooks into OpenClaw's lifecycle, so you get structured memory, Intelligent Recall, and Advanced Augmentation with a drop-in plugin.\n\n```bash\nopenclaw plugins install @memorilabs/openclaw-memori\nopenclaw plugins enable openclaw-memori\n\nopenclaw config set plugins.entries.openclaw-memori.config.apiKey \"YOUR_MEMORI_API_KEY\"\nopenclaw config set plugins.entries.openclaw-memori.config.entityId \"your-app-user-id\"\n\nopenclaw gateway restart\n```\n\nFor setup and configuration, see the [OpenClaw Quickstart](docs/memori-cloud/openclaw/quickstart.mdx). For architecture and lifecycle details, see the [OpenClaw Overview](docs/memori-cloud/openclaw/overview.mdx).\n\n## MCP (Connect Your Agent in One Command)\n\nYour agent forgets everything between sessions. Memori fixes that. It remembers your stack, your conventions, and how you like things done so you stop repeating yourself.\n\nWorks for solo developers and teams. Your agent learns coding patterns, reviewer preferences, and project conventions over time. For teams, that means shared context that new engineers pick up on day one instead of absorbing tribal knowledge over months.\n\nIf you use Claude Code, Cursor, Codex, Warp, or Antigravity, you can connect Memori with no SDK integration needed:\n\n```bash\nclaude mcp add --transport http memori https://api.memorilabs.ai/mcp/ \\\n  --header \"X-Memori-API-Key: ${MEMORI_API_KEY}\" \\\n  --header \"X-Memori-Entity-Id: your_username\" \\\n  --header \"X-Memori-Process-Id: claude-code\"\n```\n\nFor Cursor, Codex, Warp, and other clients, see the [MCP client setup guide](docs/memori-cloud/mcp/client-setup.mdx).\n\n## Attribution\n\nTo get the most out of Memori, you want to attribute your LLM interactions to an entity (think person, place or thing; like a user) and a process (think your agent, LLM interaction or program).\n\nIf you do not provide any attribution, Memori cannot make memories for you.\n\n<details open>\n<summary><b>TypeScript SDK</b></summary>\n\n```typescript\nmem.attribution(\"12345\", \"my-ai-bot\");\n```\n</details>\n\n<details>\n<summary><b>Python SDK</b></summary>\n\n```python\nmem.attribution(entity_id=\"12345\", process_id=\"my-ai-bot\")\n```\n</details>\n\n## Session Management\n\nMemori uses sessions to group your LLM interactions together. For example, if you have an agent that executes multiple steps you want those to be recorded in a single session.\n\nBy default, Memori handles setting the session for you but you can start a new session or override the session by executing the following:\n\n<details open>\n<summary><b>TypeScript SDK</b></summary>\n\n```typescript\nmem.resetSession();\n// or\nmem.setSession(sessionId);\n```\n</details>\n\n<details>\n<summary><b>Python SDK</b></summary>\n\n```python\nmem.new_session()\n# or\nmem.set_session(session_id)\n```\n</details>\n\n## Supported LLMs\n\n- Anthropic\n- Bedrock\n- DeepSeek\n- Gemini\n- Grok (xAI)\n- OpenAI (Chat Completions & Responses API)\n\n_(unstreamed, streamed, synchronous and asynchronous)_\n\n## Supported Frameworks\n\n- Agno\n- LangChain\n- Pydantic AI\n\n## Supported Platforms\n\n- DeepSeek\n- Nebius AI Studio\n\n## Examples\n\nFor more examples and demos, check out the [Memori Cookbook](https://github.com/MemoriLabs/memori-cookbook).\n\n## Memori Advanced Augmentation\n\nMemories are tracked at several different levels:\n\n- **entity**: think person, place, or thing; like a user\n- **process**: think your agent, LLM interaction or program\n- **session**: the current interactions between the entity, process and the LLM\n\n[Memori's Advanced Augmentation](docs/memori-cloud/concepts/advanced-augmentation.mdx) enhances memories at each of these levels with:\n\n- attributes\n- events\n- facts\n- people\n- preferences\n- relationships\n- rules\n- skills\n\nMemori knows who your user is, what tasks your agent handles and creates unparalleled context between the two. Augmentation occurs in the background incurring no latency.\n\nBy default, Memori Advanced Augmentation is available without an account but rate limited. When you need increased limits, [sign up for Memori Advanced Augmentation](https://app.memorilabs.ai/signup) or use the Memori CLI:\n\n```bash\n# Install the CLI via pip to manage your account\npython -m memori sign-up <email_address>\n```\n\nMemori Advanced Augmentation is always free for developers!\n\nOnce you've obtained an API key, set the following environment variable (used by both Python and TypeScript SDKs):\n\n```bash\nexport MEMORI_API_KEY=[api_key]\n```\n\n## Managing Your Quota\n\nAt any time, you can check your quota using the Memori CLI (works for both SDKs):\n\n```bash\npython -m memori quota\n```\n\nOr by checking your account at [https://app.memorilabs.ai/](https://app.memorilabs.ai/). If you have reached your IP address quota, sign up and get an API key for increased limits.\n\nIf your API key exceeds its quota limits we will email you and let you know.\n\n## Command Line Interface (CLI)\n\nThe Memori CLI is the unified tool for managing your account, keys, and quotas across all SDKs. To use it, execute the following from the command line:\n\n```bash\n# Requires Python installed\npython -m memori\n```\n\nThis will display a menu of the available options. For more information about what you can do with the Memori CLI, please reference [Command Line Interface](docs/memori-byodb/concepts/cli-quickstart.mdx).\n\n## Contributing\n\nWe welcome contributions from the community! Please see our [Contributing Guidelines](https://github.com/MemoriLabs/Memori/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up your development environment\n- Code style and standards\n- Submitting pull requests\n- Reporting issues\n\n---\n\n## Support\n\n- **Memori Cloud Documentation**: [memorilabs.ai/docs/memori-cloud/](https://memorilabs.ai/docs/memori-cloud/)\n- **Memori BYODB Documentation**: [https://memorilabs.ai/docs/memori-byodb/](https://memorilabs.ai/docs/memori-byodb/)\n- **Discord**: [https://discord.gg/abD4eGym6v](https://discord.gg/abD4eGym6v)\n- **Issues**: [GitHub Issues](https://github.com/MemoriLabs/Memori/issues)\n\n---\n\n## License\n\nApache 2.0 - see [LICENSE](https://github.com/MemoriLabs/Memori/blob/main/LICENSE)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "[![Memori Labs](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/banner.png)](https://memorilabs.ai/)\n\n## Security Policy\n\nIf you believe you have discovered a security vulnerability in Memori, please **do not** open a public GitHub issue.\n\nInstead, responsibly disclose it by:\n- Going to the **Security** tab of this repository\n- Clicking **\"Report a vulnerability\"**\n\nPlease provide as much detail as possible in your private report, including:\n- A clear description of the issue\n- Steps to reproduce (a minimal reproducible example is greatly appreciated)\n- Potential impact and any proof-of-concept if available\n\nThis allows us to investigate and address the issue quickly and safely before public disclosure.\n\nThank you for helping keep Memori secure!\n"
  },
  {
    "path": "benchmarks/README.md",
    "content": "## Benchmarks\n\nThis directory contains **benchmark harnesses** that are intentionally **not** part of the\ndefault `pytest` unit test suite.\n\n### Performance / latency (pytest-benchmark)\n\nPerformance benchmarks (including **end-to-end recall latency**) live in `benchmarks/perf/`.\n\nRun locally (example):\n\n```bash\nuv run pytest -m benchmark --benchmark-only benchmarks/perf/test_recall_benchmarks.py -v\n```\n\nFor EC2 / VPC-adjacent database benchmarking, see `benchmarks/perf/README.md` and the helper\nscripts in `benchmarks/perf/`.\n\n### LoCoMo (retrieval evaluation)\n\nLoCoMo is a benchmark dataset by Snap Research for long conversation memory.\n\nIn Memori, we treat LoCoMo as a **retrieval evaluation** problem: given a question, does\nMemori retrieve the right supporting context (evidence)?\n\n#### Dataset\n\nLoCoMo is not vendored in this repo. Download the dataset JSON locally, then point the\nharness at that file path.\n\nUpstream: `https://github.com/snap-research/locomo`\n\n#### Preprocess (recommended for Memori)\n\nThe upstream LoCoMo format is a **third-person** dialogue between two speakers, and some\nconversations include multimodal fields (e.g., image URLs + captions) that Memori does not\ncurrently handle well.\n\nTo make evaluation more representative of Memori usage, we provide a small preprocessing step\nthat:\n\n- Skips any conversation that contains multimodal turn fields (`img_url`, `blip_caption`, `query`)\n- Rewrites speakers so `conversation.speaker_b` becomes `assistant` and the other speaker becomes `user`\n\nRun:\n\n```bash\nuv run python benchmarks/locomo/preprocess.py \\\n  --in benchmarks/locomo10.json \\\n  --out benchmarks/locomo10_memori.json\n```\n\n#### What gets written (artifacts)\n\nEach run writes:\n\n- `predictions.jsonl`: one row per QA question (retrieved top-k + hit@k/MRR metrics)\n- `summary.json`: aggregated metrics (overall + by category)\n- `locomo.sqlite`: SQLite DB used by Memori storage during the run\n- `locomo_provenance.sqlite`: (AA mode only) benchmark-only mapping of `fact_id → dia_id` for scoring\n\n#### Modes (ingestion)\n\nLoCoMo ingestion always uses **Advanced Augmentation**:\n\n- Stores turns as `conversation_message`s and runs Memori **Advanced Augmentation** to produce\n  derived `entity_fact`s (closest to real usage).\n- Because LoCoMo evidence is turn-level, we write a **benchmark-only provenance DB**\n  (`locomo_provenance.sqlite`) that maps each derived fact back to the LoCoMo `dia_id` turn(s),\n  then score hit@k/MRR against evidence.\n- **Requires**: `MEMORI_API_KEY`.\n- **Note**: may be non-deterministic (API + model changes).\n\n#### Quickstart (advanced_augmentation, seeds + scores)\n\nPrerequisite:\n\n- `MEMORI_API_KEY` set (Advanced Augmentation API access)\n- LoCoMo harness forces staging routing (`MEMORI_TEST_MODE=1`)\n\nRun:\n\n```bash\nexport MEMORI_API_KEY=\"...\"\n# Optional: increase AA request timeout (default is 30s)\nexport MEMORI_AUGMENTATION_TIMEOUT_SECONDS=120\n\nuv run python benchmarks/locomo/run.py \\\n  --dataset benchmarks/locomo10.json \\\n  --out results/locomo/aa_run \\\n  --aa-batch per_pair\n```\n\n#### Score-only (reuse an existing DB, no AA calls)\n\nIf you already seeded a SQLite DB (and, for AA runs, a provenance DB), you can skip ingestion and\nrun retrieval+scoring directly from the existing DB:\n\n```bash\nuv run python benchmarks/locomo/run.py \\\n  --dataset benchmarks/locomo10.json \\\n  --out results/locomo/score_only \\\n  --sqlite-db results/locomo/aa_run/locomo.sqlite \\\n  --provenance-db results/locomo/aa_run/locomo_provenance.sqlite \\\n  --reuse-db\n```\n\nIf the DB contains multiple prior LoCoMo runs, pass `--run-id` to choose which one to score.\n\n#### Useful knobs (AA mode)\n\n- **Batching**:\n  - `--aa-batch per_pair` (one AA request per user+assistant pair)\n\n- **Dry-run** (inspect payload; no network call):\n  - `--aa-dry-run` writes `aa_payload_preview.json` and prints the payload + URL.\n\n- **Metadata** (only if your AA endpoint requires it; defaults are provided):\n  - `--meta-llm-provider`\n  - `--meta-llm-version`\n  - `--meta-llm-sdk-version`\n  - `--meta-framework-provider`\n  - `--meta-platform-provider`\n\n- **Timeout**:\n  - AA HTTP timeout is configured via `MEMORI_AUGMENTATION_TIMEOUT_SECONDS`.\n"
  },
  {
    "path": "benchmarks/locomo/_run_impl.py",
    "content": "from __future__ import annotations\n\nimport datetime\nimport json\nimport os\nimport sqlite3\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom uuid import uuid4\n\nfrom benchmarks.locomo._types import LoCoMoSample\nfrom benchmarks.locomo.loader import load_locomo_json\nfrom benchmarks.locomo.provenance import (\n    FactAttribution,\n    ProvenanceStore,\n    attribute_facts_to_turn_ids,\n)\nfrom benchmarks.locomo.retrieval import (\n    build_turn_facts,\n    evidence_to_turn_ids,\n)\nfrom benchmarks.locomo.scoring import hit_at_k_groups, mrr_groups\nfrom memori import Memori\nfrom memori.embeddings import embed_texts\nfrom memori.memory.augmentation.input import AugmentationInput\nfrom memori.memory.recall import Recall\n\nCATEGORY_LABELS: dict[str, str] = {\n    # LoCoMo category IDs are numeric in the dataset JSON.\n    # Mapping here matches the upstream LoCoMo taxonomy:\n    # 1=multi-hop, 2=temporal, 3=open-domain knowledge, 4=single-hop, 5=adversarial.\n    \"1\": \"multi-hop\",\n    \"2\": \"temporal\",\n    \"3\": \"open-domain\",\n    \"4\": \"single-hop\",\n    \"5\": \"adversarial\",\n    \"unknown\": \"unknown\",\n}\n\n\n@dataclass(frozen=True, slots=True)\nclass RunConfig:\n    dataset: str\n    out: str\n    sqlite_db: str = \"\"\n    provenance_db: str = \"\"\n    reuse_db: bool = False\n    run_id: str = \"\"\n    k: int = 5\n    aa_timeout: float = 180.0\n    aa_batch: str = \"per_pair\"\n    aa_dry_run: bool = False\n    aa_max_requests: int = 0\n    meta_llm_provider: str = \"openai\"\n    meta_llm_version: str = \"gpt-4.1-mini\"\n    meta_llm_sdk_version: str = \"unknown\"\n    meta_framework_provider: str = \"memori\"\n    meta_platform_provider: str = \"benchmark\"\n    aa_provenance_top_n: int = 1\n    aa_provenance_min_score: float = 0.25\n    aa_provenance_mode: str = \"similarity\"\n    rebuild_provenance: bool = False\n    allow_prod_aa: bool = False\n    max_samples: int = 0\n    max_questions: int = 0\n    only_sample_id: str = \"\"\n    max_sessions: int = 0\n    verbose: bool = False\n    log_every_questions: int = 0\n    seed_only: bool = False\n\n\n@dataclass(frozen=True, slots=True)\nclass PairRequest:\n    messages: list[dict[str, str]]\n    pair_turn_ids: tuple[str, str]\n\n\ndef run_locomo(cfg: RunConfig) -> dict:\n    out_dir = Path(cfg.out)\n    out_dir.mkdir(parents=True, exist_ok=True)\n    predictions_path = out_dir / \"predictions.jsonl\"\n    summary_path = out_dir / \"summary.json\"\n\n    sqlite_path = (\n        Path(cfg.sqlite_db).expanduser()\n        if cfg.sqlite_db\n        else (out_dir / \"locomo.sqlite\")\n    )\n    provenance_path = (\n        Path(cfg.provenance_db).expanduser()\n        if cfg.provenance_db\n        else (out_dir / \"locomo_provenance.sqlite\")\n    )\n\n    samples = load_locomo_json(cfg.dataset)\n    if cfg.only_sample_id:\n        samples = [s for s in samples if s.sample_id == cfg.only_sample_id]\n    if cfg.max_samples and cfg.max_samples > 0:\n        samples = samples[: cfg.max_samples]\n\n    run_id = _resolve_run_id(\n        cfg=cfg,\n        out_dir=out_dir,\n        sqlite_path=sqlite_path,\n        provenance_path=provenance_path,\n    )\n    ts = datetime.datetime.now(datetime.timezone.utc).isoformat()\n\n    mem = _init_memori(sqlite_path)\n    recall = Recall(mem.config)\n    prov_store = ProvenanceStore(provenance_path)\n\n    if (\n        not cfg.reuse_db\n        and not cfg.aa_dry_run\n        and not cfg.allow_prod_aa\n        and os.environ.get(\"MEMORI_TEST_MODE\") != \"1\"\n    ):\n        raise RuntimeError(\n            \"Advanced Augmentation LoCoMo runs must target staging. \"\n            \"Set MEMORI_TEST_MODE=1 (recommended) or pass --allow-prod-aa to override.\"\n        )\n\n    if cfg.verbose:\n        from memori._network import Api\n\n        url = Api(mem.config).url(\"sdk/augmentation\")\n        print(\n            f\"[locomo][aa] resolved_api_url={url} MEMORI_TEST_MODE={os.environ.get('MEMORI_TEST_MODE')!r}\"\n        )\n\n    totals = _Totals()\n\n    with predictions_path.open(\"w\", encoding=\"utf-8\") as f:\n        if cfg.verbose:\n            print(\n                f\"[locomo] start ingest=advanced_augmentation aa_batch={cfg.aa_batch} \"\n                f\"samples={len(samples)} k={cfg.k} seed_only={cfg.seed_only}\"\n            )\n        for sample in samples:\n            if cfg.max_sessions and cfg.max_sessions > 0:\n                sample = LoCoMoSample(\n                    sample_id=sample.sample_id,\n                    sessions=sample.sessions[: cfg.max_sessions],\n                    qa=sample.qa,\n                )\n            entity_external_id = f\"locomo:{run_id}:{sample.sample_id}\"\n            if cfg.reuse_db:\n                entity_db_id = _get_entity_id_sqlite(\n                    sqlite_path=sqlite_path,\n                    entity_external_id=entity_external_id,\n                )\n                if entity_db_id is None:\n                    raise RuntimeError(\n                        f\"--reuse-db was set but entity was not found in sqlite DB: external_id={entity_external_id}. \"\n                        \"Run ingestion first (omit --reuse-db) or pass the correct --run-id.\"\n                    )\n            else:\n                mem.attribution(\n                    entity_id=entity_external_id, process_id=\"locomo-benchmark\"\n                )\n                entity_db_id = mem.config.storage.driver.entity.create(\n                    entity_external_id\n                )\n\n            turn_facts, turn_index = build_turn_facts(sample)\n\n            if cfg.reuse_db:\n                fact_count = _count_entity_facts_sqlite(\n                    sqlite_path=sqlite_path, entity_db_id=entity_db_id\n                )\n                if fact_count <= 0:\n                    raise RuntimeError(\n                        f\"--reuse-db was set but no facts were found for entity external_id={entity_external_id}. \"\n                        \"Run ingestion first (omit --reuse-db) or pass the correct --run-id.\"\n                    )\n                mem.config.recall_embeddings_limit = fact_count\n                if cfg.rebuild_provenance and not cfg.seed_only:\n                    _build_aa_provenance(\n                        mem=mem,\n                        prov_store=prov_store,\n                        run_id=run_id,\n                        sample_id=sample.sample_id,\n                        entity_db_id=entity_db_id,\n                        turn_facts=turn_facts,\n                        top_n=cfg.aa_provenance_top_n,\n                        min_score=cfg.aa_provenance_min_score,\n                    )\n                elif not cfg.seed_only and not prov_store.has_any(\n                    run_id=run_id, sample_id=sample.sample_id\n                ):\n                    raise RuntimeError(\n                        f\"--reuse-db was set but no provenance rows were found for run_id={run_id} sample_id={sample.sample_id}. \"\n                        \"Seed AA ingestion once (omit --reuse-db) to generate locomo_provenance.sqlite, \"\n                        \"or point --provenance-db at an existing provenance DB, \"\n                        \"or pass --rebuild-provenance to recompute provenance offline.\"\n                    )\n                if cfg.verbose:\n                    print(\n                        f\"[locomo][reuse] sample={sample.sample_id} facts={fact_count}\"\n                    )\n            else:\n                _configure_aa_meta(mem, cfg)\n                if cfg.aa_dry_run:\n                    _write_aa_payload_preview(\n                        out_dir=out_dir,\n                        mem=mem,\n                        sample=sample,\n                        cfg=cfg,\n                        entity_external_id=entity_external_id,\n                        process_id=\"locomo-benchmark\",\n                    )\n                    # Exit before any network call.\n                    return {\n                        \"run_id\": run_id,\n                        \"timestamp_utc\": ts,\n                        \"dataset_path\": str(Path(cfg.dataset).resolve()),\n                        \"sqlite_db_path\": str(sqlite_path),\n                        \"provenance_db_path\": str(provenance_path),\n                        \"ingest\": \"advanced_augmentation\",\n                        \"sample_count\": len(samples),\n                        \"question_count\": 0,\n                        \"questions_by_category\": {},\n                        \"metrics_overall\": {},\n                        \"metrics_by_category\": {},\n                        \"phase\": 2,\n                        \"note\": \"AA dry-run: printed/wrote payload preview and exited before network calls.\",\n                    }\n                conv_id = _ingest_with_advanced_augmentation(\n                    mem=mem,\n                    prov_store=prov_store,\n                    run_id=run_id,\n                    entity_external_id=entity_external_id,\n                    entity_db_id=entity_db_id,\n                    sample=sample,\n                    cfg=cfg,\n                )\n                if conv_id is not None:\n                    summary_val = _read_conversation_summary(mem, conv_id) or \"\"\n                    print(\n                        f\"[locomo][aa] sample={sample.sample_id} \"\n                        f\"conversation_id={conv_id} summary_is_set={bool(summary_val)}\"\n                    )\n                    if summary_val:\n                        print(f\"[locomo][aa] summary={summary_val}\")\n\n                if cfg.seed_only:\n                    continue\n\n                if cfg.verbose:\n                    fact_count = len(\n                        mem.config.storage.driver.entity_fact.get_embeddings(\n                            entity_db_id, limit=100000\n                        )\n                    )\n                    print(\n                        f\"[locomo][seed] sample={sample.sample_id} mode=advanced_augmentation \"\n                        f\"turns={len(turn_facts)} facts_written={fact_count}\"\n                    )\n                if cfg.aa_provenance_mode == \"similarity\":\n                    _build_aa_provenance(\n                        mem=mem,\n                        prov_store=prov_store,\n                        run_id=run_id,\n                        sample_id=sample.sample_id,\n                        entity_db_id=entity_db_id,\n                        turn_facts=turn_facts,\n                        top_n=cfg.aa_provenance_top_n,\n                        min_score=cfg.aa_provenance_min_score,\n                    )\n                # Keep recall bounded to the facts created for this entity.\n                entity_fact_driver = mem.config.storage.driver.entity_fact\n                mem.config.recall_embeddings_limit = (\n                    len(entity_fact_driver.get_embeddings(entity_db_id, limit=100000))\n                    or 1\n                )\n\n            qa = list(sample.qa)\n            if cfg.max_questions and cfg.max_questions > 0:\n                qa = qa[: cfg.max_questions]\n\n            available_turn_ids = {t.turn_id for t in turn_facts}\n\n            for q in qa:\n                if (\n                    cfg.verbose\n                    and cfg.log_every_questions\n                    and totals.question_count % cfg.log_every_questions == 0\n                ):\n                    print(\n                        f\"[locomo][score] questions={totals.question_count} \"\n                        f\"sample={sample.sample_id}\"\n                    )\n\n                relevant = evidence_to_turn_ids(q.evidence, turn_index=turn_index)\n                if not relevant:\n                    if cfg.verbose:\n                        print(\n                            f\"[locomo][skip] sample={sample.sample_id} \"\n                            f\"question_id={q.question_id} reason=no_evidence\"\n                        )\n                    continue\n                missing = sorted(\n                    tid for tid in relevant if tid not in available_turn_ids\n                )\n                if missing:\n                    if cfg.verbose:\n                        preview = \", \".join(missing[:5])\n                        more = (\n                            \"\" if len(missing) <= 5 else f\" (+{len(missing) - 5} more)\"\n                        )\n                        print(\n                            f\"[locomo][skip] sample={sample.sample_id} \"\n                            f\"question_id={q.question_id} reason=missing_evidence \"\n                            f\"missing={preview}{more}\"\n                        )\n                    continue\n\n                totals.count_question(q.category)\n                results = recall.search_facts(\n                    query=q.question,\n                    limit=max(cfg.k, 1),\n                    entity_id=entity_db_id,\n                )\n\n                # Scoring in AA mode needs a mapping from fact_id -> dia_id (LoCoMo turn id).\n                # A single fact can plausibly map to multiple turns; we score using any-match\n                # semantics per retrieved rank.\n                retrieved_ids: list[str] = []\n                retrieved_groups: list[set[str]] = []\n                top_k = _format_top_k(\n                    results=results[: max(cfg.k, 1)],\n                    prov_store=prov_store,\n                    run_id=run_id,\n                    sample_id=sample.sample_id,\n                    retrieved_ids_out=retrieved_ids,\n                    retrieved_groups_out=retrieved_groups,\n                    provenance_limit=max(int(cfg.aa_provenance_top_n), 1),\n                )\n\n                metrics = {\n                    \"hit@1\": hit_at_k_groups(relevant, retrieved_groups, 1),\n                    \"hit@3\": hit_at_k_groups(relevant, retrieved_groups, 3),\n                    \"hit@5\": hit_at_k_groups(relevant, retrieved_groups, 5),\n                    \"hit@10\": hit_at_k_groups(relevant, retrieved_groups, 10),\n                    \"hit@20\": hit_at_k_groups(relevant, retrieved_groups, 20),\n                    \"hit@30\": hit_at_k_groups(relevant, retrieved_groups, 30),\n                    \"mrr\": mrr_groups(relevant, retrieved_groups),\n                }\n                totals.add_metrics(category=q.category or \"unknown\", metrics=metrics)\n\n                row = {\n                    \"run_id\": run_id,\n                    \"timestamp_utc\": ts,\n                    \"sample_id\": sample.sample_id,\n                    \"question_id\": q.question_id,\n                    \"category\": q.category,\n                    \"question\": q.question,\n                    \"answers\": list(q.answers),\n                    \"evidence\": q.evidence,\n                    \"retrieval\": {\n                        \"status\": \"ok\",\n                        \"ingest\": \"advanced_augmentation\",\n                        \"k\": cfg.k,\n                        \"relevant_turn_ids\": sorted(relevant),\n                        \"top_k\": top_k,\n                        \"metrics\": metrics,\n                    },\n                }\n                f.write(json.dumps(row, ensure_ascii=False) + \"\\n\")\n\n    summary = totals.to_summary(\n        run_id=run_id,\n        timestamp_utc=ts,\n        dataset_path=str(Path(cfg.dataset).resolve()),\n        sqlite_db_path=str(sqlite_path),\n        provenance_db_path=str(provenance_path),\n        ingest=\"advanced_augmentation\",\n        sample_count=len(samples),\n    )\n    summary_path.write_text(json.dumps(summary, indent=2), encoding=\"utf-8\")\n    return summary\n\n\ndef _resolve_run_id(\n    *, cfg: RunConfig, out_dir: Path, sqlite_path: Path, provenance_path: Path\n) -> str:\n    if cfg.run_id:\n        return cfg.run_id\n    if not cfg.reuse_db:\n        return str(uuid4())\n\n    summary_path = out_dir / \"summary.json\"\n    if summary_path.exists():\n        try:\n            obj = json.loads(summary_path.read_text(encoding=\"utf-8\"))\n            rid = obj.get(\"run_id\")\n            if isinstance(rid, str) and rid:\n                return rid\n        except Exception:\n            pass\n\n    run_ids: set[str] = set()\n    if provenance_path.exists():\n        run_ids |= _distinct_run_ids_from_provenance_sqlite(provenance_path)\n\n    if sqlite_path.exists():\n        run_ids |= _distinct_run_ids_from_memori_sqlite(sqlite_path)\n\n    if len(run_ids) == 1:\n        return next(iter(run_ids))\n    if not run_ids:\n        raise RuntimeError(\n            \"--reuse-db was set but no prior LoCoMo run_id was found in the DB(s). \"\n            \"Pass --run-id or run ingestion once without --reuse-db.\"\n        )\n    raise RuntimeError(\n        \"--reuse-db was set but multiple run_ids were found in the DB(s). \"\n        f\"Pass --run-id to choose one. found={sorted(run_ids)}\"\n    )\n\n\ndef _distinct_run_ids_from_provenance_sqlite(path: Path) -> set[str]:\n    with sqlite3.connect(str(path), check_same_thread=False) as conn:\n        try:\n            cur = conn.execute(\n                \"SELECT DISTINCT run_id FROM bench_locomo_fact_provenance\"\n            )\n        except sqlite3.OperationalError:\n            return set()\n        return {r[0] for r in cur.fetchall() if r and isinstance(r[0], str) and r[0]}\n\n\ndef _distinct_run_ids_from_memori_sqlite(path: Path) -> set[str]:\n    with sqlite3.connect(str(path), check_same_thread=False) as conn:\n        try:\n            cur = conn.execute(\n                \"SELECT external_id FROM memori_entity WHERE external_id LIKE 'locomo:%'\"\n            )\n        except sqlite3.OperationalError:\n            return set()\n        out: set[str] = set()\n        for row in cur.fetchall():\n            if not row:\n                continue\n            external_id = row[0]\n            if not isinstance(external_id, str) or not external_id.startswith(\n                \"locomo:\"\n            ):\n                continue\n            parts = external_id.split(\":\")\n            if len(parts) >= 3 and parts[1]:\n                out.add(parts[1])\n        return out\n\n\ndef _get_entity_id_sqlite(*, sqlite_path: Path, entity_external_id: str) -> int | None:\n    with sqlite3.connect(str(sqlite_path), check_same_thread=False) as conn:\n        cur = conn.execute(\n            \"SELECT id FROM memori_entity WHERE external_id = ?\",\n            (entity_external_id,),\n        )\n        row = cur.fetchone()\n        if not row:\n            return None\n        try:\n            return int(row[0])\n        except (TypeError, ValueError):\n            return None\n\n\ndef _count_entity_facts_sqlite(*, sqlite_path: Path, entity_db_id: int) -> int:\n    with sqlite3.connect(str(sqlite_path), check_same_thread=False) as conn:\n        cur = conn.execute(\n            \"SELECT COUNT(*) FROM memori_entity_fact WHERE entity_id = ?\",\n            (int(entity_db_id),),\n        )\n        row = cur.fetchone()\n        if not row:\n            return 0\n        try:\n            return int(row[0])\n        except (TypeError, ValueError):\n            return 0\n\n\ndef _init_memori(sqlite_path: Path) -> Memori:\n    def _conn():\n        return sqlite3.connect(str(sqlite_path), check_same_thread=False)\n\n    mem = Memori(conn=_conn)\n    mem.config.storage.build()\n    return mem\n\n\ndef _configure_aa_meta(mem: Memori, cfg: RunConfig) -> None:\n    mem.config.framework.provider = cfg.meta_framework_provider\n    mem.config.platform.provider = cfg.meta_platform_provider\n    mem.config.llm.provider = cfg.meta_llm_provider\n    mem.config.llm.version = cfg.meta_llm_version\n    mem.config.llm.provider_sdk_version = cfg.meta_llm_sdk_version\n\n\ndef _ingest_with_advanced_augmentation(\n    *,\n    mem: Memori,\n    prov_store: ProvenanceStore,\n    run_id: str,\n    entity_external_id: str,\n    entity_db_id: int,\n    sample,\n    cfg: RunConfig,\n) -> int | None:\n    if cfg.aa_batch == \"per_pair\":\n        return _aa_enqueue_pairs_sequential(\n            mem=mem,\n            prov_store=prov_store,\n            run_id=run_id,\n            entity_external_id=entity_external_id,\n            entity_db_id=entity_db_id,\n            sample_id=sample.sample_id,\n            sample=sample,\n            timeout=cfg.aa_timeout,\n            max_requests=int(cfg.aa_max_requests or 0),\n            verbose=bool(cfg.verbose),\n        )\n\n    raise ValueError(f\"unknown aa batch: {cfg.aa_batch}\")\n\n\ndef _aa_enqueue_pairs_sequential(\n    *,\n    mem: Memori,\n    prov_store: ProvenanceStore,\n    run_id: str,\n    entity_external_id: str,\n    entity_db_id: int,\n    sample_id: str,\n    sample,\n    timeout: float,\n    max_requests: int = 0,\n    verbose: bool = False,\n) -> int | None:\n    all_msgs, all_turn_ids = _build_aa_messages_and_turn_ids_for_sample(sample)\n    conv_id = _create_conversation_and_persist_messages(mem, all_msgs)\n\n    reqs = _build_per_pair_requests(all_msgs, all_turn_ids)\n    max_n = int(max_requests or 0)\n    if max_n > 0:\n        reqs = reqs[:max_n]\n\n    entity_fact_driver = mem.config.storage.driver.entity_fact\n    known_fact_ids = {\n        int(r[\"id\"])\n        for r in entity_fact_driver.get_embeddings(int(entity_db_id), limit=100000)\n    }\n\n    for idx, req in enumerate(reqs):\n        _enqueue_aa(mem, conv_id, entity_external_id, req.messages)\n        mem.augmentation.wait(timeout=timeout)\n\n        after = {\n            int(r[\"id\"])\n            for r in entity_fact_driver.get_embeddings(int(entity_db_id), limit=100000)\n        }\n        new_fact_ids = sorted(after - known_fact_ids)\n        known_fact_ids = after\n\n        if new_fact_ids:\n            rows: list[FactAttribution] = []\n            for fid in new_fact_ids:\n                for dia_id in req.pair_turn_ids:\n                    if dia_id:\n                        rows.append(\n                            FactAttribution(fact_id=fid, dia_id=dia_id, score=1.0)\n                        )\n            prov_store.upsert_many(rows, run_id=run_id, sample_id=sample_id)\n\n        if verbose:\n            summary_val = _read_conversation_summary(mem, int(conv_id)) or \"\"\n            print(\n                f\"[locomo][aa][pair] idx={idx} new_facts={len(new_fact_ids)} \"\n                f\"pair_turn_ids={req.pair_turn_ids} summary_is_set={bool(summary_val)}\"\n            )\n\n    return int(conv_id)\n\n\ndef _read_conversation_summary(mem: Memori, conversation_id: int) -> str | None:\n    try:\n        obj = mem.config.storage.driver.conversation.read(int(conversation_id))\n    except Exception:\n        return None\n    if not isinstance(obj, dict):\n        return None\n    summary = obj.get(\"summary\")\n    return summary if isinstance(summary, str) and summary.strip() else None\n\n\ndef _create_conversation_and_persist_messages(\n    mem: Memori, msgs: list[dict[str, str]]\n) -> int:\n    mem.new_session()\n    conv_id = mem.config.storage.driver.conversation.create(\n        str(mem.config.session_id), mem.config.session_timeout_minutes\n    )\n    for m in msgs:\n        mem.config.storage.driver.conversation.message.create(\n            conv_id, m[\"role\"], \"text\", m[\"content\"]\n        )\n    if mem.config.storage.adapter is not None:\n        mem.config.storage.adapter.commit()\n    return int(conv_id)\n\n\ndef _enqueue_aa(\n    mem: Memori, conv_id: int, entity_external_id: str, msgs: list[dict[str, str]]\n) -> None:\n    from memori.memory.augmentation._message import ConversationMessage\n\n    mem.augmentation.enqueue(\n        AugmentationInput(\n            conversation_id=str(conv_id),\n            entity_id=entity_external_id,\n            process_id=\"locomo-benchmark\",\n            conversation_messages=[\n                ConversationMessage(role=m[\"role\"], content=m[\"content\"]) for m in msgs\n            ],\n            system_prompt=None,\n        )\n    )\n\n\ndef _build_aa_messages_for_sample(sample) -> list[dict[str, str]]:\n    msgs, _turn_ids = _build_aa_messages_and_turn_ids_for_sample(sample)\n    return msgs\n\n\ndef _build_aa_messages_for_session(sample, session) -> list[dict[str, str]]:\n    speaker_to_role = _speaker_to_role(sample)\n    msgs, _turn_ids = _build_aa_messages_and_turn_ids_for_turns(\n        sample, session, speaker_to_role\n    )\n    return msgs\n\n\ndef _build_aa_messages_for_turns(\n    sample, session, speaker_to_role: dict[str, str]\n) -> list[dict[str, str]]:\n    msgs, _turn_ids = _build_aa_messages_and_turn_ids_for_turns(\n        sample, session, speaker_to_role\n    )\n    return msgs\n\n\ndef _build_aa_messages_and_turn_ids_for_sample(\n    sample,\n) -> tuple[list[dict[str, str]], list[str]]:\n    speaker_to_role = _speaker_to_role(sample)\n    msgs: list[dict[str, str]] = []\n    turn_ids: list[str] = []\n    for session in sample.sessions:\n        m, t = _build_aa_messages_and_turn_ids_for_turns(\n            sample, session, speaker_to_role\n        )\n        msgs.extend(m)\n        turn_ids.extend(t)\n    return msgs, turn_ids\n\n\ndef _build_aa_messages_and_turn_ids_for_turns(\n    sample, session, speaker_to_role: dict[str, str]\n) -> tuple[list[dict[str, str]], list[str]]:\n    msgs: list[dict[str, str]] = []\n    turn_ids: list[str] = []\n    session_id = session.session_id or \"\"\n    for t_idx, turn in enumerate(session.turns):\n        speaker = (turn.speaker or \"\").strip()\n        role = speaker_to_role.get(speaker, \"assistant\")\n        turn_id = (\n            turn.turn_id or \"\"\n        ).strip() or f\"{sample.sample_id}:{session_id}:{t_idx}\"\n        content = _format_turn_content(\n            turn_id=turn_id,\n            speaker=speaker,\n            text=turn.text,\n            session_date_time=session.date_time,\n        )\n        msgs.append({\"role\": role, \"content\": content})\n        turn_ids.append(turn_id)\n    return msgs, turn_ids\n\n\ndef _format_turn_content(\n    *, turn_id: str, speaker: str, text: str, session_date_time: str | None\n) -> str:\n    # Mirror real-world Memori payloads: content is the plain message text only.\n    # Keep turn_id/speaker/session_time out of the message body (benchmark-only metadata).\n    return str(text).strip()\n\n\ndef _speaker_to_role(sample) -> dict[str, str]:\n    \"\"\"\n    Heuristic mapping: first unique speaker => user, second => assistant, others => assistant.\n    \"\"\"\n    out: dict[str, str] = {}\n    ordered: list[str] = []\n    for session in sample.sessions:\n        for turn in session.turns:\n            speaker = (turn.speaker or \"\").strip()\n            if not speaker or speaker in out:\n                continue\n            ordered.append(speaker)\n            out[speaker] = \"user\" if len(ordered) == 1 else \"assistant\"\n    return out\n\n\ndef _build_aa_provenance(\n    *,\n    mem: Memori,\n    prov_store: ProvenanceStore,\n    run_id: str,\n    sample_id: str,\n    entity_db_id: int,\n    turn_facts,\n    top_n: int,\n    min_score: float,\n) -> None:\n    turn_ids = [t.turn_id for t in turn_facts]\n    turn_texts = [t.content for t in turn_facts]\n    turn_embs = embed_texts(\n        turn_texts,\n        model=mem.config.embeddings.model,\n    )\n\n    entity_fact_driver = mem.config.storage.driver.entity_fact\n    rows = entity_fact_driver.get_embeddings(entity_db_id, limit=100000)\n    fact_ids = [int(r[\"id\"]) for r in rows]\n    facts = entity_fact_driver.get_facts_by_ids(fact_ids)\n    content_by_id = {int(r[\"id\"]): r[\"content\"] for r in facts}\n\n    fact_ids_aligned = [i for i in fact_ids if i in content_by_id]\n    fact_texts = [content_by_id[i] for i in fact_ids_aligned]\n    fact_embs = embed_texts(\n        fact_texts,\n        model=mem.config.embeddings.model,\n    )\n\n    # Clear any prior mappings for this run/sample to avoid mixing old/new strategies.\n    prov_store.delete_sample(run_id=run_id, sample_id=sample_id)\n\n    mapping = attribute_facts_to_turn_ids(\n        turn_ids=turn_ids,\n        turn_embeddings=turn_embs,\n        turn_texts=turn_texts,\n        fact_ids=fact_ids_aligned,\n        fact_embeddings=fact_embs,\n        fact_texts=fact_texts,\n        top_n=top_n,\n        min_score=min_score,\n    )\n\n    prov_rows: list[FactAttribution] = []\n    for fid, mapped in mapping.items():\n        for dia_id, score in mapped:\n            prov_rows.append(FactAttribution(fact_id=fid, dia_id=dia_id, score=score))\n    prov_store.upsert_many(prov_rows, run_id=run_id, sample_id=sample_id)\n\n\ndef _write_aa_payload_preview(\n    *,\n    out_dir: Path,\n    mem: Memori,\n    sample,\n    cfg: RunConfig,\n    entity_external_id: str,\n    process_id: str,\n) -> None:\n    \"\"\"\n    Build and print the exact AA request payload (no network call).\n\n    This is useful for debugging staging/prod routing, payload structure, and metadata.\n    \"\"\"\n    import hashlib\n\n    from memori._network import Api\n    from memori.memory.augmentation.augmentations.memori._augmentation import (\n        AdvancedAugmentation,\n    )\n\n    dialect = (\n        mem.config.storage.adapter.get_dialect()\n        if mem.config.storage and mem.config.storage.adapter\n        else \"unknown\"\n    )\n\n    aug = AdvancedAugmentation(config=mem.config, enabled=True)\n    url = Api(mem.config).url(\"sdk/augmentation\")\n\n    all_msgs, all_turn_ids = _build_aa_messages_and_turn_ids_for_sample(sample)\n    requests: list[dict[str, object]] = []\n    if cfg.aa_batch != \"per_pair\":\n        raise ValueError(f\"unknown aa batch: {cfg.aa_batch}\")\n    for i, req in enumerate(_build_per_pair_requests(all_msgs, all_turn_ids)):\n        payload = aug._build_api_payload(  # noqa: SLF001 - benchmark-only debug path\n            req.messages,\n            \"\",\n            None,\n            dialect,\n            entity_external_id,\n            process_id,\n        )\n        requests.append(\n            {\n                \"request_index\": i,\n                \"pair_turn_ids\": list(req.pair_turn_ids),\n                \"messages\": req.messages,\n                \"payload\": payload,\n            }\n        )\n\n    api_key = mem.config.api_key or \"\"\n    api_key_preview = \"\"\n    api_key_sha256 = \"\"\n    if api_key:\n        api_key_preview = (\n            f\"{api_key[:4]}…{api_key[-4:]}\" if len(api_key) >= 8 else \"set\"\n        )\n        api_key_sha256 = hashlib.sha256(api_key.encode(\"utf-8\")).hexdigest()\n\n    preview = {\n        \"url\": url,\n        \"ingest\": \"advanced_augmentation\",\n        \"aa_batch\": cfg.aa_batch,\n        \"entity_id\": entity_external_id,\n        \"process_id\": process_id,\n        \"memori_api_key\": {\n            \"is_set\": bool(api_key),\n            \"length\": len(api_key),\n            \"preview\": api_key_preview,\n            \"sha256\": api_key_sha256,\n        },\n        \"requests\": requests,\n    }\n\n    out_path = out_dir / \"aa_payload_preview.json\"\n    out_path.write_text(json.dumps(preview, indent=2), encoding=\"utf-8\")\n\n    print(f\"[locomo][aa-dry-run] url={url}\")\n    print(f\"[locomo][aa-dry-run] wrote={out_path}\")\n    # Avoid flooding stdout for large datasets.\n    max_print = 5\n    preview_print = dict(preview)\n    preview_print[\"requests_total\"] = len(requests)\n    preview_print[\"requests_printed\"] = min(max_print, len(requests))\n    preview_print[\"requests\"] = requests[:max_print]\n    print(json.dumps(preview_print, indent=2))\n\n\ndef _build_per_pair_requests(\n    msgs: list[dict[str, str]],\n    turn_ids: list[str],\n) -> list[PairRequest]:\n    \"\"\"\n    Build per-pair AA requests that mirror SDK behavior.\n\n    Each request includes *all* prior messages (oldest -> newest) plus the next\n    strict user->assistant pair at the end.\n\n    Any messages that can't be paired as a strict consecutive user+assistant pair\n    are skipped as pair boundaries, but still remain in the context for later requests.\n    \"\"\"\n    if len(msgs) != len(turn_ids):\n        raise ValueError(\"msgs and turn_ids must be aligned\")\n\n    out: list[PairRequest] = []\n    i = 0\n    while i < len(msgs) - 1:\n        role0 = (msgs[i].get(\"role\") or \"\").strip()\n        role1 = (msgs[i + 1].get(\"role\") or \"\").strip()\n        if role0 == \"user\" and role1 == \"assistant\":\n            tid0 = (turn_ids[i] or \"\").strip()\n            tid1 = (turn_ids[i + 1] or \"\").strip()\n            out.append(\n                PairRequest(messages=list(msgs[: i + 2]), pair_turn_ids=(tid0, tid1))\n            )\n            i += 2\n            continue\n        i += 1\n    return out\n\n\ndef _format_top_k(\n    *,\n    results: list,\n    prov_store: ProvenanceStore,\n    run_id: str,\n    sample_id: str,\n    retrieved_ids_out: list[str],\n    retrieved_groups_out: list[set[str]],\n    provenance_limit: int = 1,\n) -> list[dict[str, object]]:\n    out: list[dict[str, object]] = []\n    for r in results:\n        if isinstance(r, dict):\n            content = r.get(\"content\", \"\")\n            fact_id = r.get(\"id\")\n            similarity = r.get(\"similarity\")\n        else:\n            # Assume FactSearchResult or similar object with attributes\n            content = getattr(r, \"content\", \"\")\n            fact_id = getattr(r, \"id\", None)\n            similarity = getattr(r, \"similarity\", None)\n\n        turn_ids: list[str] = []\n        if isinstance(fact_id, int):\n            dia_ids = prov_store.best_dia_ids_for_fact(\n                run_id=run_id,\n                sample_id=sample_id,\n                fact_id=fact_id,\n                limit=max(int(provenance_limit), 1),\n            )\n            turn_ids = [d for d in dia_ids if d]\n\n        # For backward compatibility, keep a single primary turn_id field (first).\n        turn_id = turn_ids[0] if turn_ids else \"\"\n        if turn_id:\n            retrieved_ids_out.append(turn_id)\n\n        retrieved_groups_out.append(set(turn_ids))\n\n        out.append(\n            {\n                \"turn_id\": turn_id,\n                \"turn_ids\": turn_ids,\n                \"fact_id\": fact_id,\n                \"similarity\": similarity,\n                \"content\": content,\n            }\n        )\n    return out\n\n\nclass _Totals:\n    def __init__(self) -> None:\n        self.question_count = 0\n        self.questions_by_category: dict[str, int] = {}\n        self.sums = {\n            \"hit@1\": 0.0,\n            \"hit@3\": 0.0,\n            \"hit@5\": 0.0,\n            \"hit@10\": 0.0,\n            \"hit@20\": 0.0,\n            \"hit@30\": 0.0,\n            \"mrr\": 0.0,\n        }\n        self.sums_by_cat: dict[str, dict[str, float]] = {}\n        self.counts_by_cat: dict[str, int] = {}\n\n    def count_question(self, category: str | None) -> None:\n        self.question_count += 1\n        cat = category or \"unknown\"\n        self.questions_by_category[cat] = self.questions_by_category.get(cat, 0) + 1\n\n    def add_metrics(self, *, category: str, metrics: dict[str, float]) -> None:\n        for key in self.sums:\n            self.sums[key] += float(metrics[key])\n        self.sums_by_cat.setdefault(\n            category,\n            {\n                \"hit@1\": 0.0,\n                \"hit@3\": 0.0,\n                \"hit@5\": 0.0,\n                \"hit@10\": 0.0,\n                \"hit@20\": 0.0,\n                \"hit@30\": 0.0,\n                \"mrr\": 0.0,\n            },\n        )\n        for key in self.sums_by_cat[category]:\n            self.sums_by_cat[category][key] += float(metrics[key])\n        self.counts_by_cat[category] = self.counts_by_cat.get(category, 0) + 1\n\n    def to_summary(\n        self,\n        *,\n        run_id: str,\n        timestamp_utc: str,\n        dataset_path: str,\n        sqlite_db_path: str,\n        provenance_db_path: str,\n        ingest: str,\n        sample_count: int,\n    ) -> dict:\n        denom = float(self.question_count) if self.question_count else 1.0\n        metrics_overall = {k: (self.sums[k] / denom) for k in self.sums}\n\n        metrics_by_category: dict[str, dict[str, float]] = {}\n        for cat, sums_cat in self.sums_by_cat.items():\n            denom_cat = float(self.counts_by_cat.get(cat, 0)) or 1.0\n            metrics_by_category[cat] = {k: (sums_cat[k] / denom_cat) for k in sums_cat}\n\n        questions_by_category_labeled = {\n            CATEGORY_LABELS.get(cat, cat): count\n            for cat, count in self.questions_by_category.items()\n        }\n        metrics_by_category_labeled = {\n            CATEGORY_LABELS.get(cat, cat): vals\n            for cat, vals in metrics_by_category.items()\n        }\n\n        return {\n            \"run_id\": run_id,\n            \"timestamp_utc\": timestamp_utc,\n            \"dataset_path\": dataset_path,\n            \"sqlite_db_path\": sqlite_db_path,\n            \"provenance_db_path\": provenance_db_path,\n            \"ingest\": ingest,\n            \"sample_count\": sample_count,\n            \"question_count\": self.question_count,\n            \"category_labels\": dict(CATEGORY_LABELS),\n            \"questions_by_category\": dict(\n                sorted(self.questions_by_category.items(), key=lambda kv: kv[0])\n            ),\n            \"questions_by_category_labeled\": dict(\n                sorted(questions_by_category_labeled.items(), key=lambda kv: kv[0])\n            ),\n            \"metrics_overall\": metrics_overall,\n            \"metrics_by_category\": dict(\n                sorted(metrics_by_category.items(), key=lambda kv: kv[0])\n            ),\n            \"metrics_by_category_labeled\": dict(\n                sorted(metrics_by_category_labeled.items(), key=lambda kv: kv[0])\n            ),\n            \"phase\": 2,\n        }\n"
  },
  {
    "path": "benchmarks/locomo/_types.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Any\n\n\n@dataclass(frozen=True, slots=True)\nclass LoCoMoTurn:\n    speaker: str\n    text: str\n    turn_id: str | None = None\n    timestamp: str | None = None\n\n\n@dataclass(frozen=True, slots=True)\nclass LoCoMoSession:\n    session_id: str | None\n    turns: tuple[LoCoMoTurn, ...]\n    date_time: str | None = None\n\n\n@dataclass(frozen=True, slots=True)\nclass LoCoMoQA:\n    question_id: str\n    question: str\n    answers: tuple[str, ...]\n    category: str | None = None\n    evidence: Any | None = None\n\n\n@dataclass(frozen=True, slots=True)\nclass LoCoMoSample:\n    sample_id: str\n    sessions: tuple[LoCoMoSession, ...]\n    qa: tuple[LoCoMoQA, ...]\n"
  },
  {
    "path": "benchmarks/locomo/loader.py",
    "content": "from __future__ import annotations\n\nimport json\nimport re\nfrom pathlib import Path\nfrom typing import Any\n\nfrom benchmarks.locomo._types import LoCoMoQA, LoCoMoSample, LoCoMoSession, LoCoMoTurn\n\n\ndef load_locomo_json(path: str | Path) -> list[LoCoMoSample]:\n    raw = Path(path).read_text(encoding=\"utf-8\")\n    data = json.loads(raw)\n\n    if not isinstance(data, list):\n        raise ValueError(\"LoCoMo JSON must be a list of samples\")\n\n    samples: list[LoCoMoSample] = []\n    for i, item in enumerate(data):\n        if not isinstance(item, dict):\n            raise ValueError(f\"Sample at index {i} must be an object\")\n\n        sample_id = _coerce_str(item.get(\"sample_id\")) or _coerce_str(item.get(\"id\"))\n        if not sample_id:\n            raise ValueError(f\"Sample at index {i} is missing 'sample_id'\")\n\n        sessions = _parse_conversation(item.get(\"conversation\"))\n        qa = _parse_qa(item.get(\"qa\"))\n        samples.append(LoCoMoSample(sample_id=sample_id, sessions=sessions, qa=qa))\n\n    return samples\n\n\ndef _parse_conversation(value: Any) -> tuple[LoCoMoSession, ...]:\n    if value is None:\n        return ()\n    if isinstance(value, dict):\n        return _parse_conversation_dict(value)\n    if not isinstance(value, list):\n        raise ValueError(\"'conversation' must be a list of sessions or an object\")\n\n    out: list[LoCoMoSession] = []\n    for idx, session in enumerate(value):\n        if not isinstance(session, dict):\n            raise ValueError(f\"conversation[{idx}] must be an object\")\n\n        session_id = _coerce_str(session.get(\"session_id\")) or _coerce_str(\n            session.get(\"id\")\n        )\n        turns_raw = (\n            session.get(\"dialogue\") or session.get(\"turns\") or session.get(\"messages\")\n        )\n        turns = _parse_turns(turns_raw, context=f\"conversation[{idx}]\")\n        out.append(LoCoMoSession(session_id=session_id, turns=turns, date_time=None))\n\n    return tuple(out)\n\n\n_SESSION_KEY_RE = re.compile(r\"^session_(?P<n>\\d+)$\")\n\n\ndef _parse_conversation_dict(value: dict[str, Any]) -> tuple[LoCoMoSession, ...]:\n    sessions: list[tuple[int, str]] = []\n    for key, v in value.items():\n        if not isinstance(key, str):\n            continue\n        m = _SESSION_KEY_RE.match(key)\n        if not m:\n            continue\n        if not isinstance(v, list):\n            continue\n        sessions.append((int(m.group(\"n\")), key))\n\n    sessions.sort(key=lambda t: t[0])\n\n    out: list[LoCoMoSession] = []\n    for n, key in sessions:\n        date_time = _coerce_str(value.get(f\"session_{n}_date_time\"))\n        turns = _parse_turns(value.get(key), context=f\"conversation[{key}]\")\n        out.append(LoCoMoSession(session_id=key, turns=turns, date_time=date_time))\n    return tuple(out)\n\n\ndef _parse_turns(value: Any, *, context: str) -> tuple[LoCoMoTurn, ...]:\n    if value is None:\n        return ()\n    if not isinstance(value, list):\n        raise ValueError(f\"{context}.dialogue must be a list of turns\")\n\n    out: list[LoCoMoTurn] = []\n    for idx, turn in enumerate(value):\n        if not isinstance(turn, dict):\n            raise ValueError(f\"{context}.dialogue[{idx}] must be an object\")\n\n        turn_id = _coerce_str(turn.get(\"dia_id\")) or _coerce_str(turn.get(\"turn_id\"))\n        speaker = (\n            _coerce_str(turn.get(\"speaker\")) or _coerce_str(turn.get(\"role\")) or \"\"\n        )\n        text = _coerce_str(turn.get(\"text\")) or _coerce_str(turn.get(\"content\")) or \"\"\n        if not text:\n            continue\n\n        timestamp = _coerce_str(turn.get(\"timestamp\"))\n        out.append(\n            LoCoMoTurn(turn_id=turn_id, speaker=speaker, text=text, timestamp=timestamp)\n        )\n\n    return tuple(out)\n\n\ndef _parse_qa(value: Any) -> tuple[LoCoMoQA, ...]:\n    if value is None:\n        return ()\n    if not isinstance(value, list):\n        raise ValueError(\"'qa' must be a list\")\n\n    out: list[LoCoMoQA] = []\n    for idx, qa in enumerate(value):\n        if not isinstance(qa, dict):\n            raise ValueError(f\"qa[{idx}] must be an object\")\n\n        qid = (\n            _coerce_str(qa.get(\"question_id\")) or _coerce_str(qa.get(\"id\")) or f\"q{idx}\"\n        )\n        question = _coerce_str(qa.get(\"question\")) or \"\"\n        if not question:\n            continue\n\n        answers_raw = qa.get(\"answer\") if \"answer\" in qa else qa.get(\"answers\")\n        answers = _coerce_answers(answers_raw)\n        category = _coerce_str(qa.get(\"category\")) or _coerce_str(qa.get(\"type\"))\n        evidence = (\n            qa.get(\"evidence\") if \"evidence\" in qa else qa.get(\"supporting_facts\")\n        )\n        out.append(\n            LoCoMoQA(\n                question_id=qid,\n                question=question,\n                answers=answers,\n                category=category,\n                evidence=evidence,\n            )\n        )\n\n    return tuple(out)\n\n\ndef _coerce_str(value: Any) -> str | None:\n    if value is None:\n        return None\n    if isinstance(value, str):\n        v = value.strip()\n        return v or None\n    return str(value).strip() or None\n\n\ndef _coerce_answers(value: Any) -> tuple[str, ...]:\n    if value is None:\n        return ()\n    if isinstance(value, str):\n        v = value.strip()\n        return (v,) if v else ()\n    if isinstance(value, list):\n        out: list[str] = []\n        for item in value:\n            s = _coerce_str(item)\n            if s:\n                out.append(s)\n        return tuple(out)\n    s = _coerce_str(value)\n    return (s,) if s else ()\n"
  },
  {
    "path": "benchmarks/locomo/preprocess.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport json\nimport re\nfrom collections import Counter\nfrom pathlib import Path\nfrom typing import Any\n\n_SESSION_KEY_RE = re.compile(r\"^session_(?P<n>\\d+)$\")\n_MULTIMODAL_KEYS = (\"img_url\", \"blip_caption\", \"query\")\n\n\ndef preprocess_locomo_json(in_path: str | Path, out_path: str | Path) -> dict[str, int]:\n    \"\"\"\n    Preprocess LoCoMo into a Memori-friendly role format.\n\n    - Removes multimodal turn fields (img_url, blip_caption, query).\n    - Rewrites turn speakers so conversation.speaker_b becomes \"assistant\" and all others \"user\".\n      If conversation.speaker_b is missing, the sample is kept and left unchanged.\n    \"\"\"\n    src = Path(in_path)\n    dst = Path(out_path)\n\n    data = json.loads(src.read_text(encoding=\"utf-8\"))\n    if not isinstance(data, list):\n        raise ValueError(\"LoCoMo JSON must be a list of samples\")\n\n    kept: list[dict[str, Any]] = []\n    dropped_reasons: Counter[str] = Counter()\n    removed_multimodal_turns = 0\n\n    for _idx, sample in enumerate(data):\n        if not isinstance(sample, dict):\n            dropped_reasons[\"invalid_sample\"] += 1\n            continue\n\n        conv = sample.get(\"conversation\")\n        removed_multimodal_turns += _strip_multimodal_fields_inplace(conv)\n\n        speaker_b = None\n        if isinstance(conv, dict):\n            speaker_b = _coerce_str(conv.get(\"speaker_b\"))\n\n        if speaker_b:\n            _rewrite_speakers_inplace(conv, assistant_speaker=speaker_b)\n        else:\n            # Keep unchanged when we can't confidently map assistant roles.\n            dropped_reasons[\"missing_speaker_b\"] += 0\n\n        kept.append(sample)\n\n    dst.parent.mkdir(parents=True, exist_ok=True)\n    dst.write_text(json.dumps(kept, ensure_ascii=False), encoding=\"utf-8\")\n\n    return {\n        \"samples_in\": len(data),\n        \"samples_out\": len(kept),\n        \"removed_multimodal_turns\": int(removed_multimodal_turns),\n        \"dropped_invalid_sample\": int(dropped_reasons.get(\"invalid_sample\", 0)),\n    }\n\n\ndef _strip_multimodal_fields_inplace(conversation: Any) -> int:\n    removed = 0\n    for turn in _iter_turn_dicts(conversation):\n        if not isinstance(turn, dict):\n            continue\n        before = len(turn)\n        for k in _MULTIMODAL_KEYS:\n            turn.pop(k, None)\n        removed += int(len(turn) != before)\n    return removed\n\n\ndef _rewrite_speakers_inplace(conversation: Any, *, assistant_speaker: str) -> None:\n    for turn in _iter_turn_dicts(conversation):\n        speaker = _coerce_str(turn.get(\"speaker\")) or \"\"\n        turn[\"speaker\"] = \"assistant\" if speaker == assistant_speaker else \"user\"\n\n\ndef _iter_turn_dicts(conversation: Any):\n    if conversation is None:\n        return\n\n    if isinstance(conversation, list):\n        for session in conversation:\n            if not isinstance(session, dict):\n                continue\n            turns = (\n                session.get(\"dialogue\")\n                or session.get(\"turns\")\n                or session.get(\"messages\")\n            )\n            if not isinstance(turns, list):\n                continue\n            for t in turns:\n                if isinstance(t, dict):\n                    yield t\n        return\n\n    if isinstance(conversation, dict):\n        sessions: list[tuple[int, str]] = []\n        for key, v in conversation.items():\n            if not isinstance(key, str):\n                continue\n            m = _SESSION_KEY_RE.match(key)\n            if not m:\n                continue\n            if isinstance(v, list):\n                sessions.append((int(m.group(\"n\")), key))\n        sessions.sort(key=lambda x: x[0])\n\n        for _, key in sessions:\n            v = conversation.get(key)\n            if not isinstance(v, list):\n                continue\n            for t in v:\n                if isinstance(t, dict):\n                    yield t\n        return\n\n\ndef _coerce_str(value: Any) -> str | None:\n    if value is None:\n        return None\n    if isinstance(value, str):\n        v = value.strip()\n        return v or None\n    return str(value).strip() or None\n\n\ndef main(argv: list[str] | None = None) -> int:\n    parser = argparse.ArgumentParser(\n        description=\"Preprocess LoCoMo: strip multimodal fields and map speaker_b -> assistant.\"\n    )\n    parser.add_argument(\n        \"--in\", dest=\"in_path\", required=True, help=\"Input LoCoMo JSON path.\"\n    )\n    parser.add_argument(\n        \"--out\", dest=\"out_path\", required=True, help=\"Output JSON path.\"\n    )\n    args = parser.parse_args(argv)\n\n    stats = preprocess_locomo_json(args.in_path, args.out_path)\n    print(\n        \"[locomo][preprocess] \"\n        f\"samples_in={stats['samples_in']} samples_out={stats['samples_out']} \"\n        f\"removed_multimodal_turns={stats['removed_multimodal_turns']} \"\n        f\"dropped_invalid_sample={stats['dropped_invalid_sample']}\"\n    )\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "benchmarks/locomo/provenance.py",
    "content": "from __future__ import annotations\n\nimport math\nimport re\nimport sqlite3\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, cast\n\nfrom memori.search import find_similar_embeddings\n\nif TYPE_CHECKING:\n    from memori.search._types import FactId\n\n\n@dataclass(frozen=True, slots=True)\nclass FactAttribution:\n    fact_id: int\n    dia_id: str\n    score: float\n\n\nclass ProvenanceStore:\n    def __init__(self, path: str | Path) -> None:\n        self.path = Path(path)\n        self.path.parent.mkdir(parents=True, exist_ok=True)\n        self._ensure_schema()\n\n    def _connect(self) -> sqlite3.Connection:\n        return sqlite3.connect(str(self.path), check_same_thread=False)\n\n    def _ensure_schema(self) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                \"\"\"\n                CREATE TABLE IF NOT EXISTS bench_locomo_fact_provenance(\n                    run_id TEXT NOT NULL,\n                    sample_id TEXT NOT NULL,\n                    fact_id INTEGER NOT NULL,\n                    dia_id TEXT NOT NULL,\n                    score REAL NOT NULL,\n                    PRIMARY KEY (run_id, sample_id, fact_id, dia_id)\n                )\n                \"\"\"\n            )\n            conn.execute(\n                \"\"\"\n                CREATE INDEX IF NOT EXISTS idx_bench_locomo_fact_prov_lookup\n                ON bench_locomo_fact_provenance(run_id, sample_id, fact_id, score DESC)\n                \"\"\"\n            )\n            conn.commit()\n\n    def upsert_many(\n        self, rows: list[FactAttribution], *, run_id: str, sample_id: str\n    ) -> None:\n        if not rows:\n            return\n        with self._connect() as conn:\n            conn.executemany(\n                \"\"\"\n                INSERT OR REPLACE INTO bench_locomo_fact_provenance(\n                    run_id, sample_id, fact_id, dia_id, score\n                ) VALUES (?, ?, ?, ?, ?)\n                \"\"\",\n                [\n                    (run_id, sample_id, r.fact_id, r.dia_id, float(r.score))\n                    for r in rows\n                ],\n            )\n            conn.commit()\n\n    def best_dia_ids_for_fact(\n        self, *, run_id: str, sample_id: str, fact_id: int, limit: int = 1\n    ) -> list[str]:\n        if limit <= 0:\n            return []\n        with self._connect() as conn:\n            cur = conn.execute(\n                \"\"\"\n                SELECT dia_id\n                  FROM bench_locomo_fact_provenance\n                 WHERE run_id = ? AND sample_id = ? AND fact_id = ?\n                 ORDER BY score DESC\n                 LIMIT ?\n                \"\"\",\n                (run_id, sample_id, fact_id, limit),\n            )\n            return [r[0] for r in cur.fetchall() if r and r[0]]\n\n    def has_any(self, *, run_id: str, sample_id: str) -> bool:\n        with self._connect() as conn:\n            cur = conn.execute(\n                \"\"\"\n                SELECT 1\n                  FROM bench_locomo_fact_provenance\n                 WHERE run_id = ? AND sample_id = ?\n                 LIMIT 1\n                \"\"\",\n                (run_id, sample_id),\n            )\n            return cur.fetchone() is not None\n\n    def delete_sample(self, *, run_id: str, sample_id: str) -> None:\n        with self._connect() as conn:\n            conn.execute(\n                \"\"\"\n                DELETE FROM bench_locomo_fact_provenance\n                 WHERE run_id = ? AND sample_id = ?\n                \"\"\",\n                (run_id, sample_id),\n            )\n            conn.commit()\n\n\ndef attribute_facts_to_turn_ids(\n    *,\n    turn_ids: list[str],\n    turn_embeddings: list[list[float]],\n    turn_texts: list[str] | None = None,\n    fact_ids: list[int],\n    fact_embeddings: list[list[float]],\n    fact_texts: list[str] | None = None,\n    top_n: int = 1,\n    min_score: float | None = None,\n) -> dict[int, list[tuple[str, float]]]:\n    \"\"\"\n    Map each fact to the most similar LoCoMo turn_id(s).\n\n    This is intentionally heuristic: it enables benchmark-only provenance without\n    changing Memori's product schema.\n    \"\"\"\n    if top_n <= 0:\n        return {}\n    if len(turn_ids) != len(turn_embeddings):\n        raise ValueError(\"turn_ids and turn_embeddings must be the same length\")\n    if len(fact_ids) != len(fact_embeddings):\n        raise ValueError(\"fact_ids and fact_embeddings must be the same length\")\n    if turn_texts is not None and len(turn_texts) != len(turn_ids):\n        raise ValueError(\"turn_texts and turn_ids must be the same length\")\n    if fact_texts is not None and len(fact_texts) != len(fact_ids):\n        raise ValueError(\"fact_texts and fact_ids must be the same length\")\n\n    embeddings: list[tuple[FactId, Any]] = cast(\n        \"list[tuple[FactId, Any]]\", list(enumerate(turn_embeddings))\n    )\n    out: dict[int, list[tuple[str, float]]] = {}\n    for i, (fact_id, qemb) in enumerate(zip(fact_ids, fact_embeddings, strict=True)):\n        # Use a larger semantic pool before reranking to improve coverage.\n        semantic_pool = max(top_n, min(len(turn_ids), max(top_n * 10, 50)))\n        similar = find_similar_embeddings(embeddings, qemb, limit=semantic_pool)\n\n        fact_text = (fact_texts[i] if fact_texts is not None else \"\") or \"\"\n        lexical_scores = (\n            _lexical_scores(query_text=fact_text, docs=turn_texts)\n            if turn_texts is not None\n            else None\n        )\n\n        mapped: list[tuple[str, float]] = []\n        scored: list[tuple[int, float]] = []\n        for raw_idx, score in similar:\n            # idx is always int here (from enumerate), cast for type checker\n            idx = cast(int, raw_idx)\n            if idx < 0 or idx >= len(turn_ids):\n                continue\n            lex = float(lexical_scores[idx]) if lexical_scores is not None else 0.0\n            combined = (0.8 * float(score)) + (0.2 * lex)\n            scored.append((idx, combined))\n\n        scored.sort(key=lambda t: t[1], reverse=True)\n        for idx, score in scored[:top_n]:\n            if min_score is not None and score < min_score:\n                continue\n            mapped.append((turn_ids[idx], float(score)))\n\n        # If the threshold filtered everything, prefer some attribution over none.\n        if not mapped and scored:\n            idx_best, score_best = scored[0]\n            mapped.append((turn_ids[idx_best], float(score_best)))\n\n        out[int(fact_id)] = mapped\n    return out\n\n\n_TOKEN_RE = re.compile(r\"[a-z0-9]+\")\n_STOPWORDS = {\n    \"a\",\n    \"an\",\n    \"and\",\n    \"are\",\n    \"as\",\n    \"at\",\n    \"be\",\n    \"by\",\n    \"did\",\n    \"do\",\n    \"does\",\n    \"for\",\n    \"from\",\n    \"had\",\n    \"has\",\n    \"have\",\n    \"how\",\n    \"i\",\n    \"in\",\n    \"is\",\n    \"it\",\n    \"of\",\n    \"on\",\n    \"or\",\n    \"that\",\n    \"the\",\n    \"their\",\n    \"then\",\n    \"there\",\n    \"to\",\n    \"was\",\n    \"were\",\n    \"what\",\n    \"when\",\n    \"where\",\n    \"which\",\n    \"who\",\n    \"why\",\n    \"with\",\n    \"you\",\n    \"your\",\n}\n\n\ndef _tokenize(text: str) -> list[str]:\n    tokens = [t for t in _TOKEN_RE.findall((text or \"\").lower()) if t]\n    return [t for t in tokens if t not in _STOPWORDS]\n\n\ndef _lexical_scores(*, query_text: str, docs: list[str]) -> list[float]:\n    q_tokens = _tokenize(query_text)\n    if not q_tokens or not docs:\n        return [0.0 for _ in docs]\n\n    doc_tokens = [set(_tokenize(d)) for d in docs]\n    n = float(len(docs)) or 1.0\n    df: dict[str, int] = {}\n    for t in set(q_tokens):\n        df[t] = sum(1 for toks in doc_tokens if t in toks)\n    idf = {t: (math.log((n + 1.0) / (float(df[t]) + 1.0)) + 1.0) for t in df}\n    denom = sum(idf.get(t, 0.0) for t in q_tokens) or 1.0\n\n    out: list[float] = []\n    for toks in doc_tokens:\n        num = sum(idf.get(t, 0.0) for t in q_tokens if t in toks)\n        out.append(float(num / denom))\n    return out\n"
  },
  {
    "path": "benchmarks/locomo/report.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport json\nfrom pathlib import Path\nfrom typing import Any\n\nfrom benchmarks.locomo._run_impl import CATEGORY_LABELS\n\n\ndef main(argv: list[str] | None = None) -> int:\n    parser = argparse.ArgumentParser(\n        description=\"Aggregate LoCoMo predictions.jsonl into summary.json\"\n    )\n    parser.add_argument(\n        \"--predictions\",\n        required=True,\n        help=\"Path to predictions.jsonl created by benchmarks/locomo/run.py\",\n    )\n    parser.add_argument(\n        \"--out\",\n        required=True,\n        help=\"Path to write summary.json\",\n    )\n    args = parser.parse_args(argv)\n\n    categories: dict[str, int] = {}\n    sample_ids: set[str] = set()\n    total = 0\n\n    run_id: str | None = None\n    timestamp_utc: str | None = None\n    sums = {\n        \"hit@1\": 0.0,\n        \"hit@3\": 0.0,\n        \"hit@5\": 0.0,\n        \"hit@10\": 0.0,\n        \"hit@20\": 0.0,\n        \"hit@30\": 0.0,\n        \"mrr\": 0.0,\n    }\n    sums_by_cat: dict[str, dict[str, float]] = {}\n    counts_by_cat: dict[str, int] = {}\n\n    for row in _read_jsonl(Path(args.predictions)):\n        total += 1\n        sample_id = str(row.get(\"sample_id\", \"\")).strip()\n        if sample_id:\n            sample_ids.add(sample_id)\n\n        cat = str(row.get(\"category\") or \"unknown\")\n        categories[cat] = categories.get(cat, 0) + 1\n\n        metrics = (\n            (row.get(\"retrieval\") or {}).get(\"metrics\")\n            if isinstance(row.get(\"retrieval\"), dict)\n            else None\n        )\n        if isinstance(metrics, dict):\n            for key in sums:\n                if key in metrics:\n                    sums[key] += float(metrics[key])\n            sums_by_cat.setdefault(\n                cat,\n                {\n                    \"hit@1\": 0.0,\n                    \"hit@3\": 0.0,\n                    \"hit@5\": 0.0,\n                    \"hit@10\": 0.0,\n                    \"hit@20\": 0.0,\n                    \"hit@30\": 0.0,\n                    \"mrr\": 0.0,\n                },\n            )\n            for key in sums_by_cat[cat]:\n                if key in metrics:\n                    sums_by_cat[cat][key] += float(metrics[key])\n            counts_by_cat[cat] = counts_by_cat.get(cat, 0) + 1\n\n        run_id = run_id or _coerce_str(row.get(\"run_id\"))\n        timestamp_utc = timestamp_utc or _coerce_str(row.get(\"timestamp_utc\"))\n\n    denom = float(total) if total else 1.0\n    metrics_overall = {k: (sums[k] / denom) for k in sums}\n    metrics_by_category: dict[str, dict[str, float]] = {}\n    for cat, sums_cat in sums_by_cat.items():\n        denom_cat = float(counts_by_cat.get(cat, 0)) or 1.0\n        metrics_by_category[cat] = {k: (sums_cat[k] / denom_cat) for k in sums_cat}\n\n    questions_by_category_labeled = {\n        CATEGORY_LABELS.get(cat, cat): count for cat, count in categories.items()\n    }\n    metrics_by_category_labeled = {\n        CATEGORY_LABELS.get(cat, cat): vals for cat, vals in metrics_by_category.items()\n    }\n\n    out = {\n        \"run_id\": run_id,\n        \"timestamp_utc\": timestamp_utc,\n        \"sample_count\": len(sample_ids),\n        \"question_count\": total,\n        \"category_labels\": dict(CATEGORY_LABELS),\n        \"questions_by_category\": dict(sorted(categories.items(), key=lambda kv: kv[0])),\n        \"questions_by_category_labeled\": dict(\n            sorted(questions_by_category_labeled.items(), key=lambda kv: kv[0])\n        ),\n        \"metrics_overall\": metrics_overall,\n        \"metrics_by_category\": dict(\n            sorted(metrics_by_category.items(), key=lambda kv: kv[0])\n        ),\n        \"metrics_by_category_labeled\": dict(\n            sorted(metrics_by_category_labeled.items(), key=lambda kv: kv[0])\n        ),\n    }\n    Path(args.out).write_text(json.dumps(out, indent=2), encoding=\"utf-8\")\n    return 0\n\n\ndef _read_jsonl(path: Path) -> list[dict[str, Any]]:\n    rows: list[dict[str, Any]] = []\n    for line in path.read_text(encoding=\"utf-8\").splitlines():\n        if not line.strip():\n            continue\n        rows.append(json.loads(line))\n    return rows\n\n\ndef _coerce_str(value: Any) -> str | None:\n    if value is None:\n        return None\n    if isinstance(value, str):\n        v = value.strip()\n        return v or None\n    return str(value).strip() or None\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "benchmarks/locomo/retrieval.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom benchmarks.locomo._types import LoCoMoSample\n\n\n@dataclass(frozen=True, slots=True)\nclass TurnFact:\n    turn_id: str\n    content: str\n\n\n_TURN_ID_RE = re.compile(r\"^\\[(?P<turn_id>[^\\]]+)\\]\\s*\")\n\n\ndef build_turn_facts(\n    sample: LoCoMoSample,\n) -> tuple[tuple[TurnFact, ...], dict[tuple[str, int], str]]:\n    facts: list[TurnFact] = []\n    index: dict[tuple[str, int], str] = {}\n\n    for s_idx, session in enumerate(sample.sessions):\n        session_id = session.session_id or f\"session-{s_idx}\"\n        for t_idx, turn in enumerate(session.turns):\n            turn_id = turn.turn_id or f\"{sample.sample_id}:{session_id}:{t_idx}\"\n            index[(session_id, t_idx)] = turn_id\n\n            speaker = (turn.speaker or \"\").strip()\n            prefix = f\"[{turn_id}]\"\n            body = f\"{speaker}: {turn.text}\" if speaker else turn.text\n            if session.date_time:\n                body = f\"{body} (session_time: {session.date_time})\"\n            if turn.timestamp:\n                body = f\"{body} (ts: {turn.timestamp})\"\n            facts.append(TurnFact(turn_id=turn_id, content=f\"{prefix} {body}\".strip()))\n\n    return tuple(facts), index\n\n\ndef extract_turn_id_from_content(content: str) -> str | None:\n    m = _TURN_ID_RE.match(content or \"\")\n    if not m:\n        return None\n    v = m.group(\"turn_id\").strip()\n    return v or None\n\n\ndef evidence_to_turn_ids(\n    evidence: Any, *, turn_index: dict[tuple[str, int], str]\n) -> set[str]:\n    if evidence is None:\n        return set()\n\n    out: set[str] = set()\n\n    if isinstance(evidence, dict):\n        _add_evidence_obj(evidence, out=out, turn_index=turn_index)\n        return out\n\n    if isinstance(evidence, list):\n        for item in evidence:\n            if isinstance(item, dict):\n                _add_evidence_obj(item, out=out, turn_index=turn_index)\n            elif isinstance(item, int):\n                # If evidence is a bare turn index, only safe when there's a single session.\n                _add_evidence_turn_index(item, out=out, turn_index=turn_index)\n            elif isinstance(item, str):\n                if item:\n                    out.add(item)\n        return out\n\n    if isinstance(evidence, int):\n        _add_evidence_turn_index(evidence, out=out, turn_index=turn_index)\n        return out\n\n    if isinstance(evidence, str) and evidence:\n        out.add(evidence)\n\n    return out\n\n\ndef _add_evidence_obj(\n    obj: dict[str, Any], *, out: set[str], turn_index: dict[tuple[str, int], str]\n) -> None:\n    session_id = _coerce_str(obj.get(\"session_id\")) or _coerce_str(obj.get(\"session\"))\n    turn_idx = _coerce_int(obj.get(\"turn_index\"))\n    if session_id is not None and turn_idx is not None:\n        tid = turn_index.get((session_id, turn_idx))\n        if tid:\n            out.add(tid)\n        return\n\n    turn_id = _coerce_str(obj.get(\"turn_id\")) or _coerce_str(obj.get(\"evidence_id\"))\n    if turn_id:\n        out.add(turn_id)\n\n\ndef _add_evidence_turn_index(\n    idx: int, *, out: set[str], turn_index: dict[tuple[str, int], str]\n) -> None:\n    sessions = {session_id for (session_id, _), _tid in turn_index.items()}\n    if len(sessions) != 1:\n        return\n    (session_id,) = sessions\n    tid = turn_index.get((session_id, idx))\n    if tid:\n        out.add(tid)\n\n\ndef _coerce_str(value: Any) -> str | None:\n    if value is None:\n        return None\n    if isinstance(value, str):\n        v = value.strip()\n        return v or None\n    return str(value).strip() or None\n\n\ndef _coerce_int(value: Any) -> int | None:\n    if value is None:\n        return None\n    if isinstance(value, int):\n        return value\n    try:\n        return int(str(value).strip())\n    except Exception:\n        return None\n"
  },
  {
    "path": "benchmarks/locomo/run.py",
    "content": "import argparse\nimport os\n\nfrom benchmarks.locomo._run_impl import RunConfig, run_locomo\n\n\ndef main(argv: list[str] | None = None) -> int:\n    parser = argparse.ArgumentParser(description=\"LoCoMo benchmark harness (Phase 2)\")\n    parser.add_argument(\n        \"--allow-prod-aa\",\n        action=\"store_true\",\n        help=\"(Deprecated) Allow Advanced Augmentation calls without MEMORI_TEST_MODE=1 (production).\",\n    )\n    parser.add_argument(\n        \"--dataset\",\n        required=True,\n        help=\"Path to a LoCoMo JSON file (downloaded locally).\",\n    )\n    parser.add_argument(\n        \"--out\",\n        required=True,\n        help=\"Output directory for artifacts (predictions.jsonl, summary.json).\",\n    )\n    parser.add_argument(\n        \"--sqlite-db\",\n        default=\"\",\n        help=\"SQLite DB file path used for the run (default: <out>/locomo.sqlite).\",\n    )\n    parser.add_argument(\n        \"--provenance-db\",\n        default=\"\",\n        help=\"Benchmark-only provenance DB (default: <out>/locomo_provenance.sqlite).\",\n    )\n    parser.add_argument(\n        \"--reuse-db\",\n        action=\"store_true\",\n        help=\"Skip ingestion and reuse the existing SQLite/provenance DB for retrieval+scoring.\",\n    )\n    parser.add_argument(\n        \"--run-id\",\n        default=\"\",\n        help=\"Run namespace used for entity external IDs/provenance (required when --reuse-db and multiple runs exist in the DB).\",\n    )\n    parser.add_argument(\n        \"--k\",\n        type=int,\n        default=5,\n        help=\"Retrieval top-k to store and score (default: 5).\",\n    )\n    parser.add_argument(\n        \"--aa-timeout\",\n        type=float,\n        default=180.0,\n        help=\"Timeout (seconds) to wait for Advanced Augmentation to finish per sample.\",\n    )\n    parser.add_argument(\n        \"--aa-batch\",\n        choices=[\"per_pair\"],\n        default=\"per_pair\",\n        help=\"How to batch messages when calling Advanced Augmentation (default: per_pair).\",\n    )\n    parser.add_argument(\n        \"--aa-dry-run\",\n        action=\"store_true\",\n        help=\"Print/write AA request payload and exit before making any network calls.\",\n    )\n    parser.add_argument(\n        \"--aa-max-requests\",\n        type=int,\n        default=0,\n        help=\"Limit the number of AA requests to enqueue during ingestion (0 = no limit).\",\n    )\n    parser.add_argument(\n        \"--seed-only\",\n        action=\"store_true\",\n        help=\"Run ingestion only (no retrieval/scoring). Useful for AA payload/summary validation.\",\n    )\n    parser.add_argument(\n        \"--meta-llm-provider\",\n        default=\"openai\",\n        help=\"Metadata only: LLM provider to report to Advanced Augmentation (default: openai).\",\n    )\n    parser.add_argument(\n        \"--meta-llm-version\",\n        default=\"gpt-4.1-mini\",\n        help=\"Metadata only: LLM model version to report (default: gpt-4.1-mini).\",\n    )\n    parser.add_argument(\n        \"--meta-llm-sdk-version\",\n        default=\"unknown\",\n        help=\"Metadata only: LLM SDK version to report (default: unknown).\",\n    )\n    parser.add_argument(\n        \"--meta-framework-provider\",\n        default=\"memori\",\n        help=\"Metadata only: framework provider to report (default: memori).\",\n    )\n    parser.add_argument(\n        \"--meta-platform-provider\",\n        default=\"benchmark\",\n        help=\"Metadata only: platform provider to report (default: benchmark).\",\n    )\n    parser.add_argument(\n        \"--aa-provenance-top-n\",\n        type=int,\n        default=1,\n        help=\"How many turn_ids to attribute to each augmented fact (default: 1).\",\n    )\n    parser.add_argument(\n        \"--aa-provenance-min-score\",\n        type=float,\n        default=0.25,\n        help=\"Min cosine similarity to accept a fact->turn attribution (default: 0.25).\",\n    )\n    parser.add_argument(\n        \"--aa-provenance-mode\",\n        choices=[\"similarity\"],\n        default=\"similarity\",\n        help=(\n            \"How to attribute AA facts back to LoCoMo turn IDs for scoring. \"\n            \"'similarity' maps facts to turns post-hoc using embedding/text similarity (default). \"\n        ),\n    )\n    parser.add_argument(\n        \"--rebuild-provenance\",\n        action=\"store_true\",\n        help=\"When --reuse-db: recompute provenance offline from the SQLite DB (no AA calls).\",\n    )\n    parser.add_argument(\n        \"--max-samples\",\n        type=int,\n        default=0,\n        help=\"Limit number of samples (0 = no limit).\",\n    )\n    parser.add_argument(\n        \"--only-sample-id\",\n        default=\"\",\n        help=\"Run only a single sample_id (exact match).\",\n    )\n    parser.add_argument(\n        \"--max-sessions\",\n        type=int,\n        default=0,\n        help=\"Limit number of sessions per sample (0 = no limit).\",\n    )\n    parser.add_argument(\n        \"--max-questions\",\n        type=int,\n        default=0,\n        help=\"Limit questions per sample (0 = no limit).\",\n    )\n    parser.add_argument(\n        \"--verbose\",\n        action=\"store_true\",\n        help=\"Print progress/logging about seeding and scoring.\",\n    )\n    parser.add_argument(\n        \"--log-every-questions\",\n        type=int,\n        default=0,\n        help=\"When --verbose, log progress every N questions (0 = disabled).\",\n    )\n    args = parser.parse_args(argv)\n    # LoCoMo benchmarks should never hit production AA. Force staging routing.\n    os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n    run_locomo(\n        RunConfig(\n            dataset=args.dataset,\n            out=args.out,\n            sqlite_db=args.sqlite_db,\n            provenance_db=args.provenance_db,\n            reuse_db=args.reuse_db,\n            run_id=args.run_id,\n            k=args.k,\n            aa_timeout=args.aa_timeout,\n            aa_batch=args.aa_batch,\n            aa_dry_run=args.aa_dry_run,\n            aa_max_requests=args.aa_max_requests,\n            meta_llm_provider=args.meta_llm_provider,\n            meta_llm_version=args.meta_llm_version,\n            meta_llm_sdk_version=args.meta_llm_sdk_version,\n            meta_framework_provider=args.meta_framework_provider,\n            meta_platform_provider=args.meta_platform_provider,\n            aa_provenance_top_n=args.aa_provenance_top_n,\n            aa_provenance_min_score=args.aa_provenance_min_score,\n            aa_provenance_mode=args.aa_provenance_mode,\n            rebuild_provenance=args.rebuild_provenance,\n            allow_prod_aa=args.allow_prod_aa,\n            max_samples=args.max_samples,\n            only_sample_id=args.only_sample_id,\n            max_sessions=args.max_sessions,\n            max_questions=args.max_questions,\n            seed_only=args.seed_only,\n            verbose=args.verbose,\n            log_every_questions=args.log_every_questions,\n        )\n    )\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "benchmarks/locomo/scoring.py",
    "content": "from __future__ import annotations\n\n\ndef hit_at_k(relevant: set[str], retrieved: list[str], k: int) -> float:\n    if not relevant or not retrieved or k <= 0:\n        return 0.0\n    top = retrieved[:k]\n    return 1.0 if any(item in relevant for item in top) else 0.0\n\n\ndef mrr(relevant: set[str], retrieved: list[str]) -> float:\n    if not relevant or not retrieved:\n        return 0.0\n    for i, item in enumerate(retrieved, start=1):\n        if item in relevant:\n            return 1.0 / float(i)\n    return 0.0\n\n\ndef hit_at_k_groups(relevant: set[str], retrieved: list[set[str]], k: int) -> float:\n    \"\"\"\n    Variant of hit@k where each retrieved rank can map to multiple IDs.\n\n    This is useful when a retrieved item (e.g., an AA fact) may plausibly originate\n    from multiple LoCoMo turns (dia_id).\n    \"\"\"\n    if not relevant or not retrieved or k <= 0:\n        return 0.0\n    for group in retrieved[:k]:\n        if group and (group & relevant):\n            return 1.0\n    return 0.0\n\n\ndef mrr_groups(relevant: set[str], retrieved: list[set[str]]) -> float:\n    if not relevant or not retrieved:\n        return 0.0\n    for i, group in enumerate(retrieved, start=1):\n        if group and (group & relevant):\n            return 1.0 / float(i)\n    return 0.0\n"
  },
  {
    "path": "benchmarks/perf/README.md",
    "content": "# Performance / latency benchmarks\n\nThis folder contains **performance benchmarks** for Memori recall, powered by `pytest-benchmark`.\n\n## Run locally\n\n```bash\nuv run pytest -m benchmark --benchmark-only benchmarks/perf/test_recall_benchmarks.py -v\n```\n\n## Run on EC2 (recommended for realistic DB latency)\n\nUse an EC2 instance in the same VPC as your database (RDS Postgres/MySQL).\n\n- Setup:\n\n```bash\nchmod +x benchmarks/perf/setup_ec2_benchmarks.sh\n./benchmarks/perf/setup_ec2_benchmarks.sh\n```\n\n- Run:\n\n```bash\nexport BENCHMARK_POSTGRES_URL=\"CHANGEME\"\nDB_TYPE=postgres TEST_TYPE=all ./benchmarks/perf/run_benchmarks_ec2.sh\n```\n\n### Environment variables\n\n- `DB_TYPE`: `postgres` (default) or `mysql`\n- `TEST_TYPE`: `all` (default), `end_to_end`, `db_retrieval`, `semantic_search`, `embedding`\n- `BENCHMARK_POSTGRES_URL`: Postgres connection string\n- `BENCHMARK_MYSQL_URL`: MySQL connection string\n\n## Outputs\n\nResults are saved to `./results`:\n\n- JSON: `results_{db}_{type}_{timestamp}.json`\n- CSV: `report_{db}_{type}_{timestamp}.csv`\n"
  },
  {
    "path": "benchmarks/perf/_results.py",
    "content": "from __future__ import annotations\n\nimport csv\nfrom pathlib import Path\nfrom typing import Any\n\n\ndef repo_root() -> Path:\n    return Path(__file__).resolve().parents[2]\n\n\ndef results_dir() -> Path:\n    path = repo_root() / \"results\"\n    path.mkdir(parents=True, exist_ok=True)\n    return path\n\n\ndef append_csv_row(path: str | Path, *, header: list[str], row: dict[str, Any]) -> None:\n    out_path = Path(path)\n    out_path.parent.mkdir(parents=True, exist_ok=True)\n    file_exists = out_path.exists()\n    with out_path.open(\"a\", newline=\"\") as f:\n        writer = csv.DictWriter(f, fieldnames=header)\n        if not file_exists:\n            writer.writeheader()\n        writer.writerow(row)\n"
  },
  {
    "path": "benchmarks/perf/conftest.py",
    "content": "\"\"\"Pytest fixtures for performance benchmarks.\"\"\"\n\nimport os\n\nimport pytest\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom benchmarks.perf.fixtures.sample_data import (\n    generate_facts_with_size,\n    generate_sample_queries,\n)\nfrom memori import Memori\nfrom memori.embeddings import embed_texts\n\n\n@pytest.fixture\ndef postgres_db_connection():\n    \"\"\"Create a PostgreSQL database connection factory for benchmarking (via AWS/Docker).\"\"\"\n    postgres_uri = os.environ.get(\n        \"BENCHMARK_POSTGRES_URL\",\n        # Matches docker-compose.yml default DB name\n        \"postgresql://memori:memori@localhost:5432/memori_test\",\n    )\n\n    from sqlalchemy import text\n\n    # Support SSL root certificate via environment variable (for AWS RDS)\n    connect_args = {}\n    sslrootcert = os.environ.get(\"BENCHMARK_POSTGRES_SSLROOTCERT\")\n    if sslrootcert:\n        connect_args[\"sslrootcert\"] = sslrootcert\n        # Ensure sslmode is set if using SSL cert\n        if \"sslmode\" not in postgres_uri:\n            # Add sslmode=require if not already in URI\n            separator = \"&\" if \"?\" in postgres_uri else \"?\"\n            postgres_uri = f\"{postgres_uri}{separator}sslmode=require\"\n\n    engine = create_engine(\n        postgres_uri,\n        pool_pre_ping=True,\n        pool_recycle=300,\n        connect_args=connect_args,\n    )\n\n    try:\n        with engine.connect() as conn:\n            conn.execute(text(\"SELECT 1\"))\n    except Exception as e:\n        pytest.skip(\n            f\"PostgreSQL not available at {postgres_uri}: {e}. \"\n            \"Set BENCHMARK_POSTGRES_URL to a database that exists.\"\n        )\n\n    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n    yield Session\n    engine.dispose()\n\n\n@pytest.fixture\ndef mysql_db_connection():\n    \"\"\"Create a MySQL database connection factory for benchmarking (via AWS/Docker).\"\"\"\n    mysql_uri = os.environ.get(\n        \"BENCHMARK_MYSQL_URL\",\n        \"mysql+pymysql://memori:memori@localhost:3306/memori_test\",\n    )\n\n    from sqlalchemy import text\n\n    engine = create_engine(\n        mysql_uri,\n        pool_pre_ping=True,\n        pool_recycle=300,\n    )\n\n    try:\n        with engine.connect() as conn:\n            conn.execute(text(\"SELECT 1\"))\n    except Exception as e:\n        pytest.skip(f\"MySQL not available at {mysql_uri}: {e}\")\n\n    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n    yield Session\n    engine.dispose()\n\n\n@pytest.fixture(\n    params=[\"postgres\", \"mysql\"],\n    ids=[\"postgres\", \"mysql\"],\n)\ndef db_connection(request):\n    \"\"\"Parameterized fixture for realistic database types (no SQLite).\"\"\"\n    db_type = request.param\n\n    if db_type == \"postgres\":\n        return request.getfixturevalue(\"postgres_db_connection\")\n    elif db_type == \"mysql\":\n        return request.getfixturevalue(\"mysql_db_connection\")\n\n    pytest.skip(f\"Unsupported benchmark database type: {db_type}\")\n\n\n@pytest.fixture\ndef memori_instance(db_connection, request):\n    \"\"\"Create a Memori instance with the specified database for benchmarking.\"\"\"\n    mem = Memori(conn=db_connection)\n    mem.config.storage.build()\n\n    db_type_param = None\n    for marker in request.node.iter_markers(\"parametrize\"):\n        if \"db_connection\" in marker.args[0]:\n            db_type_param = marker.args[1][0] if marker.args[1] else None\n            break\n\n    # Try to infer from connection\n    if not db_type_param:\n        try:\n            # SQLAlchemy sessionmaker is callable, so detect it first by presence of a bind.\n            bind = getattr(db_connection, \"kw\", {}).get(\"bind\", None)\n            if bind is not None:\n                db_type_param = bind.dialect.name\n            else:\n                db_type_param = \"unknown\"\n        except Exception:\n            db_type_param = \"unknown\"\n\n    mem._benchmark_db_type = db_type_param\n    return mem\n\n\n@pytest.fixture\ndef sample_queries():\n    \"\"\"Provide sample queries of varying lengths.\"\"\"\n    return generate_sample_queries()\n\n\n@pytest.fixture\ndef fact_content_size():\n    \"\"\"Fixture for fact content size.\n\n    Note: Embeddings are always 768 dimensions (3072 bytes binary) regardless of text size.\n    \"\"\"\n    return \"small\"\n\n\n@pytest.fixture(\n    params=[5, 50, 100, 300, 600, 1000],\n    ids=lambda x: f\"n{x}\",\n)\ndef entity_with_n_facts(memori_instance, fact_content_size, request):\n    \"\"\"Create an entity with N facts for benchmarking database retrieval.\"\"\"\n    fact_count = request.param\n    entity_id = f\"benchmark-entity-{fact_count}-{fact_content_size}\"\n    memori_instance.attribution(entity_id=entity_id, process_id=\"benchmark-process\")\n\n    facts = generate_facts_with_size(fact_count, fact_content_size)\n    fact_embeddings = embed_texts(\n        facts,\n        model=memori_instance.config.embeddings.model,\n    )\n\n    entity_db_id = memori_instance.config.storage.driver.entity.create(entity_id)\n    memori_instance.config.storage.driver.entity_fact.create(\n        entity_db_id, facts, fact_embeddings\n    )\n\n    db_type = getattr(memori_instance, \"_benchmark_db_type\", \"unknown\")\n\n    return {\n        \"entity_id\": entity_id,\n        \"entity_db_id\": entity_db_id,\n        \"fact_count\": fact_count,\n        \"content_size\": fact_content_size,\n        \"db_type\": db_type,\n        \"facts\": facts,\n    }\n"
  },
  {
    "path": "benchmarks/perf/fixtures/sample_data.py",
    "content": "\"\"\"Helper functions for generating sample test data for benchmarks.\"\"\"\n\nimport random\nimport string\n\nrandom.seed(42)\n\n\ndef generate_random_string(length: int = 10) -> str:\n    \"\"\"Generate a random string of specified length.\"\"\"\n    return \"\".join(random.choices(string.ascii_letters + string.digits, k=length))\n\n\ndef generate_sample_fact() -> str:\n    \"\"\"Generate a realistic sample fact for testing.\"\"\"\n    templates = [\n        \"User likes {item}\",\n        \"User lives in {location}\",\n        \"User works at {company}\",\n        \"User's favorite color is {color}\",\n        \"User prefers {preference}\",\n        \"User has {count} {item}\",\n        \"User enjoys {activity}\",\n        \"User's birthday is {date}\",\n    ]\n\n    items = [\"pizza\", \"coffee\", \"books\", \"music\", \"movies\", \"travel\", \"coding\"]\n    locations = [\"New York\", \"San Francisco\", \"London\", \"Tokyo\", \"Paris\"]\n    companies = [\"Tech Corp\", \"Startup Inc\", \"Big Company\", \"Small Business\"]\n    colors = [\"blue\", \"red\", \"green\", \"purple\", \"yellow\"]\n    preferences = [\"dark mode\", \"light mode\", \"minimalist design\", \"detailed UI\"]\n    activities = [\"reading\", \"hiking\", \"cooking\", \"gaming\", \"photography\"]\n    dates = [\"January 1st\", \"March 15th\", \"June 30th\", \"December 25th\"]\n\n    template = random.choice(templates)\n    fact = template.format(\n        item=random.choice(items),\n        location=random.choice(locations),\n        company=random.choice(companies),\n        color=random.choice(colors),\n        preference=random.choice(preferences),\n        count=random.randint(1, 10),\n        activity=random.choice(activities),\n        date=random.choice(dates),\n    )\n\n    return fact\n\n\ndef generate_sample_queries() -> dict[str, list[str]]:\n    \"\"\"Generate sample queries of varying lengths for benchmarking.\"\"\"\n    return {\n        \"short\": [\n            \"What do I like?\",\n            \"Where do I live?\",\n            \"My preferences?\",\n            \"Favorite color?\",\n            \"Birthday?\",\n        ],\n        \"medium\": [\n            \"What are my favorite things?\",\n            \"Tell me about where I live\",\n            \"What are my preferences for software?\",\n            \"What is my favorite color and why?\",\n            \"When is my birthday and how do I celebrate?\",\n        ],\n        \"long\": [\n            \"Can you tell me about all the things I like and enjoy doing?\",\n            \"I want to know more about where I currently live and work\",\n            \"What are all my preferences when it comes to software and design?\",\n            \"Please provide details about my favorite color and any related memories\",\n            \"I'd like to know when my birthday is and how I typically celebrate it\",\n        ],\n    }\n\n\ndef generate_facts(count: int) -> list[str]:\n    \"\"\"Generate a list of unique sample facts.\"\"\"\n    facts = []\n    seen = set()\n\n    while len(facts) < count:\n        fact = generate_sample_fact()\n        # Add unique identifier to ensure no duplicates\n        unique_fact = f\"{fact} (id: {len(facts)})\"\n\n        # Double-check uniqueness (shouldn't be needed with id, but safe)\n        if unique_fact not in seen:\n            facts.append(unique_fact)\n            seen.add(unique_fact)\n\n    return facts\n\n\ndef generate_facts_with_size(count: int, size: str = \"small\") -> list[str]:\n    \"\"\"Generate facts for benchmarking.\n\n    Args:\n        count: Number of facts to generate\n        size: Content size\n\n    Returns:\n        List of unique facts\n    \"\"\"\n    base_facts = generate_facts(count)\n\n    def with_id_suffix(text: str, idx: int, max_len: int) -> str:\n        suffix = f\" (id: {idx})\"\n        if max_len <= len(suffix):\n            return suffix[-max_len:]\n        return text[: max_len - len(suffix)] + suffix\n\n    return [with_id_suffix(fact, i, 60) for i, fact in enumerate(base_facts)]\n"
  },
  {
    "path": "benchmarks/perf/fixtures/sample_facts.py",
    "content": "# 1,000 synthetic user facts as semantic triples (subject, predicate, object)\n# Copy/paste into your project. Produces: user_data = {\"user\": \"John\", \"facts\": [(s,p,o), ...]}\n\n\ndef build_user_data() -> dict:\n    facts: list[tuple[str, str, str]] = []\n\n    def add(s: str, p: str, o: str) -> None:\n        facts.append((s, p, o))\n\n    # -----------------------------\n    # Core profile facts (diverse)\n    # -----------------------------\n    add(\"John\", \"type\", \"Person\")\n    add(\"John\", \"has_given_name\", \"John\")\n    add(\"John\", \"prefers_language\", \"English\")\n    add(\"John\", \"prefers_time_format\", \"12-hour\")\n    add(\"John\", \"prefers_temperature_unit\", \"Fahrenheit\")\n    add(\"John\", \"prefers_distance_unit\", \"miles\")\n    add(\"John\", \"has_home_city\", \"St_Louis\")\n    add(\"John\", \"has_home_state\", \"Missouri\")\n    add(\"John\", \"has_home_country\", \"United_States\")\n    add(\"John\", \"has_role\", \"Software_Engineer\")\n    add(\"John\", \"works_in\", \"Technology\")\n    add(\"John\", \"interested_in\", \"AI\")\n    add(\"John\", \"interested_in\", \"cloud_infrastructure\")\n    add(\"John\", \"interested_in\", \"personal_finance\")\n    add(\"John\", \"interested_in\", \"fitness\")\n    add(\"John\", \"interested_in\", \"cooking\")\n    add(\"John\", \"interested_in\", \"travel\")\n    add(\"John\", \"interested_in\", \"productivity\")\n    add(\"John\", \"interested_in\", \"home_improvement\")\n    add(\"John\", \"uses_llm_for\", \"writing_assistance\")\n    add(\"John\", \"uses_llm_for\", \"coding_help\")\n    add(\"John\", \"uses_llm_for\", \"travel_planning\")\n    add(\"John\", \"uses_llm_for\", \"learning_new_topics\")\n    add(\"John\", \"uses_llm_for\", \"recipe_ideas\")\n    add(\"John\", \"uses_llm_for\", \"career_advice\")\n    add(\"John\", \"uses_llm_for\", \"data_analysis\")\n    add(\"John\", \"uses_llm_for\", \"brainstorming\")\n    add(\"John\", \"prefers_response_style\", \"structured\")\n    add(\"John\", \"prefers_response_style\", \"actionable\")\n    add(\"John\", \"concerned_about\", \"latency\")\n    add(\"John\", \"concerned_about\", \"cost\")\n    add(\"John\", \"concerned_about\", \"privacy\")\n    add(\"John\", \"primary_os\", \"macOS\")\n    add(\"John\", \"primary_browser\", \"Chrome\")\n    add(\"John\", \"primary_editor\", \"VS_Code\")\n    add(\"John\", \"primary_shell\", \"zsh\")\n    add(\"John\", \"primary_email_provider\", \"Gmail\")\n    add(\"John\", \"primary_calendar\", \"Google_Calendar\")\n    add(\"John\", \"primary_messaging_app\", \"Slack\")\n    add(\"John\", \"primary_code_host\", \"GitHub\")\n    add(\"John\", \"prefers_document_format\", \"Markdown\")\n    add(\"John\", \"prefers_spreadsheet_tool\", \"Google_Sheets\")\n    add(\"John\", \"uses_password_manager\", \"1Password\")\n    add(\"John\", \"uses_cloud_storage\", \"Google_Drive\")\n    add(\"John\", \"uses_issue_tracker\", \"GitHub_Issues\")\n    add(\"John\", \"uses_ci_cd\", \"GitHub_Actions\")\n    add(\"John\", \"prefers_container_runtime\", \"Docker\")\n    add(\"John\", \"prefers_database\", \"PostgreSQL\")\n    add(\"John\", \"prefers_cache\", \"Redis\")\n    add(\"John\", \"prefers_language_for_backend\", \"Python\")\n    add(\"John\", \"knows_language\", \"JavaScript\")\n    add(\"John\", \"knows_language\", \"SQL\")\n    add(\"John\", \"knows_language\", \"Bash\")\n    add(\"John\", \"learning_language\", \"Rust\")\n    add(\"John\", \"prefers_testing_framework\", \"pytest\")\n    add(\"John\", \"prefers_package_manager\", \"uv\")\n    add(\"John\", \"uses_llm_provider\", \"OpenAI\")\n    add(\"John\", \"uses_llm_provider\", \"Anthropic\")\n    add(\"John\", \"uses_model_family\", \"GPT\")\n    add(\"John\", \"uses_model_family\", \"Claude\")\n    add(\"John\", \"has_hobby\", \"fantasy_football\")\n    add(\"John\", \"follows_sport\", \"NFL\")\n    add(\"John\", \"follows_sport\", \"NHL\")\n    add(\"John\", \"prefers_coffee_drink\", \"latte\")\n    add(\"John\", \"prefers_breakfast\", \"oatmeal\")\n    add(\"John\", \"prefers_lunch\", \"salad\")\n    add(\"John\", \"prefers_dinner\", \"grilled_chicken\")\n    add(\"John\", \"commutes_by\", \"car\")\n    add(\"John\", \"prefers_meeting_platform\", \"Zoom\")\n\n    # -----------------------------\n    # Family / household facts\n    # -----------------------------\n    family = [\n        (\"Spouse_01\", \"spouse\"),\n        (\"Child_01\", \"child\"),\n        (\"Child_02\", \"child\"),\n        (\"Parent_01\", \"parent\"),\n        (\"Parent_02\", \"parent\"),\n        (\"Sibling_01\", \"sibling\"),\n        (\"Sibling_02\", \"sibling\"),\n        (\"Pet_01\", \"pet\"),\n    ]\n    for i, (entity, rel) in enumerate(family, start=1):\n        add(entity, \"type\", \"Person\" if \"Pet\" not in entity else \"Animal\")\n        add(\"John\", f\"has_{rel}\", entity)\n        add(entity, \"has_first_name\", entity.split(\"_\")[0])\n        add(entity, \"located_in\", f\"City_{(i % 25) + 1:03d}\")\n        add(\n            entity,\n            \"preferred_contact_method\",\n            [\"text\", \"call\", \"email\"][i % 3] if \"Pet\" not in entity else \"n/a\",\n        )\n\n    # -----------------------------\n    # Friends (varied interests)\n    # -----------------------------\n    friend_interests = [\n        \"music\",\n        \"travel\",\n        \"tech\",\n        \"food\",\n        \"sports\",\n        \"finance\",\n        \"fitness\",\n        \"gaming\",\n        \"books\",\n        \"photography\",\n    ]\n    contact_methods = [\"email\", \"text\", \"slack\", \"signal\", \"call\"]\n    for i in range(1, 61):  # 60 friends\n        f = f\"Friend_{i:03d}\"\n        add(f, \"type\", \"Person\")\n        add(\"John\", \"has_friend\", f)\n        add(f, \"located_in\", f\"City_{(i % 80) + 1:03d}\")\n        add(f, \"interested_in\", friend_interests[i % len(friend_interests)])\n        add(f, \"preferred_contact_method\", contact_methods[i % len(contact_methods)])\n\n    # -----------------------------\n    # Coworkers (teams, roles, tools)\n    # -----------------------------\n    roles = [\n        \"Backend_Engineer\",\n        \"Frontend_Engineer\",\n        \"DevOps_Engineer\",\n        \"Product_Manager\",\n        \"Designer\",\n        \"Data_Engineer\",\n    ]\n    teams = [\"Platform\", \"Infra\", \"Product\", \"Data\", \"Security\", \"Growth\"]\n    tools = [\"Jira\", \"Linear\", \"Confluence\", \"Notion\", \"Figma\", \"Datadog\", \"Grafana\"]\n    for i in range(1, 41):  # 40 coworkers\n        c = f\"Coworker_{i:03d}\"\n        add(c, \"type\", \"Person\")\n        add(\"John\", \"works_with\", c)\n        add(c, \"has_role\", roles[i % len(roles)])\n        add(c, \"member_of_team\", teams[i % len(teams)])\n        add(c, \"uses_tool\", tools[i % len(tools)])\n\n    # -----------------------------\n    # Places: cities, venues, travel\n    # -----------------------------\n    for i in range(1, 81):  # 80 cities\n        city = f\"City_{i:03d}\"\n        add(city, \"type\", \"City\")\n        add(city, \"in_country\", \"United_States\" if i <= 60 else \"International\")\n        add(\"John\", \"has_visited\", city if i <= 45 else f\"Planned_{city}\")\n        add(\n            city,\n            \"has_timezone\",\n            \"America/Chicago\"\n            if i % 3 == 0\n            else \"America/New_York\"\n            if i % 3 == 1\n            else \"America/Los_Angeles\",\n        )\n\n    venue_types = [\n        \"Restaurant\",\n        \"Coffee_Shop\",\n        \"Gym\",\n        \"Airport\",\n        \"Hotel\",\n        \"Park\",\n        \"Museum\",\n        \"Stadium\",\n    ]\n    for i in range(1, 81):  # 80 venues\n        v = f\"Venue_{i:03d}\"\n        add(v, \"type\", venue_types[i % len(venue_types)])\n        add(v, \"located_in\", f\"City_{(i % 80) + 1:03d}\")\n        add(\"John\", \"likes_place\", v if i % 4 != 0 else f\"Neutral_{v}\")\n        add(v, \"has_price_tier\", [\"$\", \"$$\", \"$$$\"][i % 3])\n\n    # -----------------------------\n    # Devices, apps, services, accounts\n    # -----------------------------\n    devices = [\n        \"MacBook_Pro\",\n        \"iPhone\",\n        \"iPad\",\n        \"AirPods\",\n        \"Smart_TV\",\n        \"Router\",\n        \"NAS\",\n        \"Mechanical_Keyboard\",\n        \"Gaming_PC\",\n        \"Monitor_34inch\",\n        \"Standing_Desk\",\n        \"Ergonomic_Chair\",\n        \"Fitness_Tracker\",\n        \"Smart_Speaker\",\n        \"Kindle\",\n        \"External_SSD\",\n        \"Webcam\",\n        \"Microphone\",\n        \"Printer\",\n        \"Smart_Thermostat\",\n    ]\n    for d in devices:\n        add(d, \"type\", \"Device\")\n        add(\"John\", \"owns_device\", d)\n        add(d, \"used_for\", \"work\" if \"MacBook\" in d or \"Monitor\" in d else \"personal\")\n        add(d, \"has_status\", \"active\")\n\n    apps = [\n        \"Notion\",\n        \"Slack\",\n        \"Gmail\",\n        \"Google_Calendar\",\n        \"Spotify\",\n        \"YouTube\",\n        \"Netflix\",\n        \"Amazon\",\n        \"GitHub\",\n        \"Figma\",\n        \"Jira\",\n        \"Linear\",\n        \"Zoom\",\n        \"1Password\",\n        \"Google_Drive\",\n    ]\n    for i, app in enumerate(apps, start=1):\n        add(app, \"type\", \"App\")\n        add(\"John\", \"uses_app\", app)\n        add(app, \"has_version\", f\"{(i % 5) + 1}.{(i % 10)}.{(i % 20)}\")\n\n    return {\"user\": \"John\", \"facts\": facts}\n"
  },
  {
    "path": "benchmarks/perf/generate_percentile_report.py",
    "content": "\"\"\"Generate percentile report (p50/p95/p99) from benchmark JSON results.\"\"\"\n\nimport json\nimport sys\nfrom pathlib import Path\n\n\ndef calculate_percentile(data, percentile):\n    \"\"\"Calculate percentile from sorted data.\"\"\"\n    if not data:\n        return None\n    sorted_data = sorted(data)\n    index = (len(sorted_data) - 1) * percentile / 100\n    lower = int(index)\n    upper = lower + 1\n    weight = index - lower\n\n    if upper >= len(sorted_data):\n        return sorted_data[lower]\n\n    return sorted_data[lower] * (1 - weight) + sorted_data[upper] * weight\n\n\ndef extract_n_from_test_name(test_name):\n    \"\"\"Extract N (number of records) from test name.\"\"\"\n    import re\n\n    # Our parametrized ids use \"n{N}\" (e.g. \"[n1000-postgres-small]\")\n    match = re.search(r\"n(\\d+)\", test_name)\n    if match:\n        return int(match.group(1))\n\n    # Backwards-compatible: plain numeric parameter (e.g. \"[1000]\")\n    match = re.search(r\"\\[(\\d+)\\]\", test_name)\n    return int(match.group(1)) if match else None\n\n\ndef extract_db_type_from_test_name(test_name):\n    \"\"\"Extract database type from test name.\"\"\"\n    import re\n\n    # Look for database type in test name (postgres, mysql)\n    match = re.search(r\"\\[(postgres|mysql)[-\\]]\", test_name)\n    if not match:\n        match = re.search(r\"-(postgres|mysql)[-\\]]\", test_name)\n    if not match:\n        match = re.search(r\"(postgres|mysql)\", test_name)\n    return match.group(1) if match else \"unknown\"\n\n\ndef extract_content_size_from_test_name(test_name):\n    \"\"\"Extract content size from test name.\"\"\"\n    import re\n\n    match = re.search(r\"\\[small[-\\]]\", test_name)\n    if not match:\n        match = re.search(r\"-small[-\\]]\", test_name)\n    if not match:\n        match = re.search(r\"small\", test_name)\n    return match.group(0) if match else \"small\"\n\n\ndef extract_benchmark_id_from_test_name(test_name):\n    \"\"\"\n    Extract a stable benchmark identifier from pytest-benchmark's name field.\n    Examples:\n      \"test_benchmark_end_to_end_recall[n1000-sqlite-small]\" -> \"test_benchmark_end_to_end_recall\"\n      \"test_benchmark_query_embedding_short\" -> \"test_benchmark_query_embedding_short\"\n    \"\"\"\n    import re\n\n    match = re.match(r\"([^\\[]+)\", test_name)\n    return match.group(1) if match else test_name\n\n\ndef generate_percentile_report(json_file_path, max_n=None):\n    \"\"\"Generate p50/p95/p99 report from benchmark JSON.\n\n    Args:\n        json_file_path: Path to pytest-benchmark JSON output\n        max_n: Optional maximum N value to include (filters out tests with N > max_n)\n    \"\"\"\n    with open(json_file_path) as f:\n        data = json.load(f)\n\n    benchmarks = {}\n\n    for benchmark in data.get(\"benchmarks\", []):\n        test_name = benchmark.get(\"name\", \"\")\n        benchmark_id = extract_benchmark_id_from_test_name(test_name)\n        n = extract_n_from_test_name(test_name)\n\n        # Skip if N is None (couldn't extract) or exceeds max_n filter\n        if n is None:\n            continue\n        if max_n is not None and n > max_n:\n            continue\n\n        db_type = extract_db_type_from_test_name(test_name)\n        content_size = extract_content_size_from_test_name(test_name)\n        extra_info = benchmark.get(\"extra_info\", {}) or {}\n\n        stats = benchmark.get(\"stats\", {})\n        times = stats.get(\"data\", [])\n\n        if not times:\n            continue\n\n        peak_rss_bytes = extra_info.get(\"peak_rss_bytes\")\n\n        p50 = calculate_percentile(times, 50)\n        p95 = calculate_percentile(times, 95)\n        p99 = calculate_percentile(times, 99)\n\n        # Create composite key. We include benchmark_id so different benchmark types\n        # don't overwrite each other (e.g. end-to-end vs db fetch for same N/db/size).\n        key = (benchmark_id, n, db_type, content_size)\n\n        benchmarks[key] = {\n            \"benchmark_id\": benchmark_id,\n            \"n\": n,\n            \"db_type\": db_type,\n            \"content_size\": content_size,\n            \"p50\": p50,\n            \"p95\": p95,\n            \"p99\": p99,\n            \"mean\": stats.get(\"mean\", 0),\n            \"min\": stats.get(\"min\", 0),\n            \"max\": stats.get(\"max\", 0),\n            \"peak_rss_bytes\": peak_rss_bytes,\n        }\n\n    return benchmarks\n\n\ndef generate_report(benchmarks, output_format=\"table\"):\n    \"\"\"Generate percentile report in specified format as string.\"\"\"\n    lines = []\n\n    if output_format == \"table\":\n        lines.append(\"\\n\" + \"=\" * 100)\n        lines.append(\n            \"PERCENTILE REPORT (p50/p95/p99) per N, Database Type, and Content Size\"\n        )\n        lines.append(\"=\" * 100)\n        lines.append(\n            f\"{'Benchmark':<34} {'N':<8} {'DB':<12} {'Size':<8} {'p50 (ms)':<12} {'p95 (ms)':<12} \"\n            f\"{'p99 (ms)':<12} {'Mean (ms)':<12} {'Peak RSS (MB)':<14}\"\n        )\n        lines.append(\"-\" * 100)\n\n        for key in sorted(benchmarks.keys()):\n            stats = benchmarks[key]\n            peak_rss_mb = (\n                (stats[\"peak_rss_bytes\"] / (1024 * 1024))\n                if stats.get(\"peak_rss_bytes\") is not None\n                else None\n            )\n            lines.append(\n                f\"{stats['benchmark_id']:<34} \"\n                f\"{stats['n']:<8} \"\n                f\"{stats['db_type']:<12} \"\n                f\"{stats['content_size']:<8} \"\n                f\"{stats['p50'] * 1000:<12.4f} \"\n                f\"{stats['p95'] * 1000:<12.4f} \"\n                f\"{stats['p99'] * 1000:<12.4f} \"\n                f\"{stats['mean'] * 1000:<12.4f} \"\n                f\"{(f'{peak_rss_mb:.1f}' if peak_rss_mb is not None else ''):<14}\"\n            )\n        lines.append(\"=\" * 100)\n        return \"\\n\".join(lines)\n\n    if output_format == \"csv\":\n        lines.append(\n            \"benchmark_id,N,db_type,content_size,p50_ms,p95_ms,p99_ms,mean_ms,min_ms,max_ms,peak_rss_mb\"\n        )\n        for key in sorted(benchmarks.keys()):\n            stats = benchmarks[key]\n            peak_rss_mb = (\n                (stats[\"peak_rss_bytes\"] / (1024 * 1024))\n                if stats.get(\"peak_rss_bytes\") is not None\n                else None\n            )\n            lines.append(\n                f\"{stats['benchmark_id']},\"\n                f\"{stats['n']},\"\n                f\"{stats['db_type']},\"\n                f\"{stats['content_size']},\"\n                f\"{stats['p50'] * 1000:.4f},\"\n                f\"{stats['p95'] * 1000:.4f},\"\n                f\"{stats['p99'] * 1000:.4f},\"\n                f\"{stats['mean'] * 1000:.4f},\"\n                f\"{stats['min'] * 1000:.4f},\"\n                f\"{stats['max'] * 1000:.4f},\"\n                f\"{(f'{peak_rss_mb:.4f}' if peak_rss_mb is not None else '')}\"\n            )\n        return \"\\n\".join(lines)\n\n    if output_format == \"json\":\n        output = {}\n        for key in sorted(benchmarks.keys()):\n            stats = benchmarks[key]\n            output_key = f\"{stats['benchmark_id']}_{stats['n']}_{stats['db_type']}_{stats['content_size']}\"\n            peak_rss_mb = (\n                (stats[\"peak_rss_bytes\"] / (1024 * 1024))\n                if stats.get(\"peak_rss_bytes\") is not None\n                else None\n            )\n            output[output_key] = {\n                \"benchmark_id\": stats[\"benchmark_id\"],\n                \"n\": stats[\"n\"],\n                \"db_type\": stats[\"db_type\"],\n                \"content_size\": stats[\"content_size\"],\n                \"p50_ms\": stats[\"p50\"] * 1000,\n                \"p95_ms\": stats[\"p95\"] * 1000,\n                \"p99_ms\": stats[\"p99\"] * 1000,\n                \"mean_ms\": stats[\"mean\"] * 1000,\n                \"min_ms\": stats[\"min\"] * 1000,\n                \"max_ms\": stats[\"max\"] * 1000,\n                \"peak_rss_mb\": peak_rss_mb,\n            }\n        return json.dumps(output, indent=2)\n\n    return \"\"\n\n\ndef main():\n    if len(sys.argv) < 2:\n        print(\n            \"Usage: python generate_percentile_report.py <benchmark.json> [format] [output_file] [max_n]\"\n        )\n        print(\"  format: table (default), csv, or json\")\n        print(\"  output_file: optional file path to write report (default: stdout)\")\n        print(\n            \"  max_n: optional maximum N value to include (filters out tests with N > max_n)\"\n        )\n        sys.exit(1)\n\n    json_file = Path(sys.argv[1])\n    if not json_file.exists():\n        print(f\"Error: File not found: {json_file}\")\n        sys.exit(1)\n\n    output_format = sys.argv[2] if len(sys.argv) > 2 else \"table\"\n\n    if output_format not in [\"table\", \"csv\", \"json\"]:\n        print(f\"Error: Invalid format '{output_format}'. Use: table, csv, or json\")\n        sys.exit(1)\n\n    output_file = sys.argv[3] if len(sys.argv) > 3 else None\n    max_n = int(sys.argv[4]) if len(sys.argv) > 4 else None\n\n    benchmarks = generate_percentile_report(json_file, max_n=max_n)\n\n    if not benchmarks:\n        print(\"No benchmark data found.\")\n        sys.exit(1)\n\n    report = generate_report(benchmarks, output_format)\n\n    if output_file:\n        Path(output_file).write_text(report, encoding=\"utf-8\")\n        print(f\"Report written to: {output_file}\")\n    else:\n        print(report)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "benchmarks/perf/memory_utils.py",
    "content": "import threading\nimport time\nfrom collections.abc import Callable\nfrom typing import TypeVar\n\nT = TypeVar(\"T\")\n\n\ndef measure_peak_rss_bytes(\n    fn: Callable[[], T], *, sample_interval_seconds: float = 0.005\n) -> tuple[T, int | None]:\n    \"\"\"\n    Measure approximate peak RSS (process resident set size) while running fn.\n\n    Returns (result, peak_rss_bytes). If psutil isn't available, peak is None.\n    \"\"\"\n    try:\n        import psutil\n    except Exception:\n        return fn(), None\n\n    proc = psutil.Process()\n    peak = {\"rss\": proc.memory_info().rss}\n    stop = threading.Event()\n\n    def _sampler() -> None:\n        while not stop.is_set():\n            try:\n                rss = proc.memory_info().rss\n                if rss > peak[\"rss\"]:\n                    peak[\"rss\"] = rss\n            except Exception:\n                pass\n            time.sleep(sample_interval_seconds)\n\n    t = threading.Thread(target=_sampler, daemon=True)\n    t.start()\n    try:\n        result = fn()\n    finally:\n        stop.set()\n        t.join(timeout=1.0)\n\n    return result, int(peak[\"rss\"])\n"
  },
  {
    "path": "benchmarks/perf/run_benchmarks_ec2.sh",
    "content": "#!/bin/bash\n# Shared benchmark execution functions for AWS EC2 environment\n\nset -e\n\n# Get script location to handle relative paths correctly\nBENCHMARK_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$BENCHMARK_DIR/../..\" && pwd)\"\n\n# Default settings\nDB_TYPE=${DB_TYPE:-\"postgres\"}\nTEST_TYPE=${TEST_TYPE:-\"all\"} # options: all, end_to_end, db_retrieval, semantic_search, embedding\nOUTPUT_DIR=${OUTPUT_DIR:-\"$REPO_ROOT/results\"}\n\nmkdir -p \"$OUTPUT_DIR\"\n\nrun_benchmarks() {\n    local db=$1\n    local type=$2\n    local timestamp=$(date +%Y%m%d_%H%M%S)\n    local output_json=\"$OUTPUT_DIR/results_${db}_${type}_${timestamp}.json\"\n    local output_csv=\"$OUTPUT_DIR/report_${db}_${type}_${timestamp}.csv\"\n\n    echo \"====================================================\"\n    echo \"Running benchmarks for: DB=$db, Type=$type\"\n    echo \"Output JSON: $output_json\"\n    echo \"Output CSV:  $output_csv\"\n    echo \"====================================================\"\n\n    # Determine pytest filter (-k) based on test type\n    local filter=\"\"\n    case $type in\n        \"end_to_end\") filter=\"TestEndToEndRecallBenchmarks\" ;;\n        \"db_retrieval\") filter=\"DatabaseEmbeddingRetrievalBenchmarks or DatabaseFactContentRetrievalBenchmarks\" ;;\n        \"semantic_search\") filter=\"TestSemanticSearchBenchmarks\" ;;\n        \"embedding\") filter=\"TestQueryEmbeddingBenchmarks\" ;;\n        \"all\") filter=\"\" ;;\n        *) echo \"Unknown test type: $type\"; exit 1 ;;\n    esac\n\n    # Add database filter\n    if [[ -n \"$filter\" ]]; then\n        filter=\"($filter) and $db\"\n    else\n        filter=\"$db\"\n    fi\n\n    # Run benchmarks from repo root\n    (\n        cd \"$REPO_ROOT\"\n        uv run pytest -m benchmark \\\n            --benchmark-only \\\n            benchmarks/perf/test_recall_benchmarks.py \\\n            -k \"$filter\" \\\n            -v \\\n            --benchmark-json=\"$output_json\"\n\n        # Automatically convert to CSV\n        if [[ -f \"$output_json\" ]]; then\n            echo \"Converting results to CSV...\"\n            uv run python benchmarks/perf/generate_percentile_report.py \\\n                \"$output_json\" \\\n                csv \\\n                \"$output_csv\"\n            echo \"CSV Report generated: $output_csv\"\n        else\n            echo \"Warning: JSON results file not found, skipping CSV generation.\"\n        fi\n    )\n}\n\n# If script is executed directly (not sourced), run based on env vars\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n    # Print usage info if help requested\n    if [[ \"$1\" == \"--help\" || \"$1\" == \"-h\" ]]; then\n        echo \"Usage: DB_TYPE=[postgres|mysql] TEST_TYPE=[all|end_to_end|db_retrieval|semantic_search|embedding] $0\"\n        exit 0\n    fi\n\n    run_benchmarks \"$DB_TYPE\" \"$TEST_TYPE\"\n\n    echo \"====================================================\"\n    echo \"Benchmark Run Complete\"\n    echo \"====================================================\"\nfi\n"
  },
  {
    "path": "benchmarks/perf/setup_ec2_benchmarks.sh",
    "content": "#!/bin/bash\n# Setup script for running benchmarks on AWS EC2\n\nset -e\n\necho \"Setting up Memori benchmarks on EC2...\"\n\n# Install system dependencies\nsudo apt-get update\nsudo apt-get install -y \\\n    python3.12 \\\n    python3.12-venv \\\n    git \\\n    curl \\\n    build-essential \\\n    postgresql-client \\\n    default-mysql-client\n\n# Install uv\ncurl -LsSf https://astral.sh/uv/install.sh | sh\nsource $HOME/.cargo/env\n\n# Clone repository (replace with your repo URL if not already cloned)\n# git clone https://github.com/MemoriLabs/Memori.git\n# cd Memori\n\n# Sync dependencies\nuv sync --all-extras\n\n# Source the runner to get run_benchmarks function\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"$SCRIPT_DIR/run_benchmarks_ec2.sh\"\n\necho \"Setup complete!\"\necho \"\"\necho \"To run all benchmarks for Postgres:\"\necho \"  DB_TYPE=postgres TEST_TYPE=all ./benchmarks/perf/run_benchmarks_ec2.sh\"\necho \"\"\necho \"To run end-to-end benchmarks for MySQL:\"\necho \"  DB_TYPE=mysql TEST_TYPE=end_to_end ./benchmarks/perf/run_benchmarks_ec2.sh\"\necho \"\"\necho \"Results will be automatically saved to the ./results directory as CSV.\"\n"
  },
  {
    "path": "benchmarks/perf/test_cloud_recall_benchmarks.py",
    "content": "\"\"\"Cloud (Memori API) performance / latency benchmarks.\n\nThese benchmarks exercise the cloud recall path (network + API latency) rather than\nlocal DB-backed recall.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom uuid import uuid4\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.llm._base import BaseInvoke\nfrom memori.llm._constants import OPENAI_LLM_PROVIDER\nfrom memori.memory._manager import Manager\nfrom memori.memory.recall import Recall\n\n\ndef _make_cloud_cfg(*, entity_id: str, process_id: str) -> Config:\n    cfg = Config()\n    cfg.cloud = True\n    cfg.entity_id = entity_id\n    cfg.process_id = process_id\n    if cfg.session_id is None:\n        cfg.session_id = uuid4()\n    # Pick a provider so injection rules are deterministic.\n    cfg.llm.provider = OPENAI_LLM_PROVIDER\n    cfg.framework.provider = \"bench\"\n    return cfg\n\n\ndef _seed_cloud_messages(\n    cfg: Config, *, n_messages: int, entity_id: str, process_id: str\n) -> None:\n    cfg.cloud = True\n    cfg.entity_id = entity_id\n    cfg.process_id = process_id\n    if cfg.session_id is None:\n        cfg.session_id = uuid4()\n\n    messages: list[dict[str, object]] = []\n    for i in range(n_messages):\n        messages.append({\"role\": \"user\", \"type\": None, \"text\": f\"I like item_{i}.\"})\n        messages.append({\"role\": \"assistant\", \"type\": None, \"text\": \"Ok.\"})\n\n    Manager(cfg).execute(\n        {\n            \"attribution\": {\n                \"entity\": {\"id\": str(entity_id)},\n                \"process\": {\"id\": str(process_id)},\n            },\n            \"messages\": messages,\n            \"session\": {\"id\": str(cfg.session_id)},\n        }\n    )\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_messages\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_recall_latency(benchmark, n_messages: int) -> None:\n    \"\"\"Benchmark end-to-end cloud recall latency.\n\n    Includes: HTTP request + cloud service time. Excludes: local DB retrieval.\n    \"\"\"\n    os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n    api_key = os.environ.get(\"MEMORI_API_KEY\")\n    if not api_key:\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud recall.\")\n\n    cfg = Config()\n\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_messages,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    recall = Recall(cfg)\n\n    def _call():\n        return recall.search_facts(query=\"What do I like?\", limit=5)\n\n    result = benchmark(_call)\n    assert isinstance(result, list)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_pre_llm_overhead(benchmark, n_history_pairs: int) -> None:\n    \"\"\"Benchmark Memori overhead up to the LLM call (cloud mode).\n\n    This measures the \"added latency\" before calling an LLM:\n    - (optional) fetch cloud conversation history\n    - cloud recall request (server-side embeddings + retrieval)\n    - prompt/message injection logic\n\n    It intentionally does NOT call an LLM provider.\n    \"\"\"\n    if not os.environ.get(\"MEMORI_API_KEY\"):\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud pre-LLM overhead.\")\n\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    # Seed a stable session for cloud history fetch.\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_history_pairs,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    query = \"What do I like?\"\n\n    def _prepare():\n        invoke = BaseInvoke(cfg, lambda **_kwargs: None)\n        kwargs = {\"messages\": [{\"role\": \"user\", \"content\": query}]}\n        kwargs = invoke.inject_conversation_messages(kwargs)\n        kwargs = invoke.inject_recalled_facts(kwargs)\n        return kwargs\n\n    result = benchmark(_prepare)\n    assert isinstance(result, dict)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_network_history_get(benchmark, n_history_pairs: int) -> None:\n    \"\"\"cloud conversation history fetched via recall (POST), no injection.\"\"\"\n    if not os.environ.get(\"MEMORI_API_KEY\"):\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud history GET.\")\n\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_history_pairs,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    def _call():\n        recall = Recall(cfg)\n        data = recall._cloud_recall(query=\"History fetch benchmark\")\n        _facts, messages = recall._parse_cloud_recall_response(data)\n        return messages\n\n    result = benchmark(_call)\n    assert isinstance(result, list)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_network_recall_post(benchmark, n_history_pairs: int) -> None:\n    \"\"\"ONLY cloud recall call (POST /recall), no injection.\n\n    We seed N history pairs first so cloud has a consistent amount of data.\n    \"\"\"\n    if not os.environ.get(\"MEMORI_API_KEY\"):\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud recall POST.\")\n\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_cloud_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_history_pairs,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    recall = Recall(cfg)\n\n    def _call():\n        return recall.search_facts(query=\"What do I like?\", limit=5)\n\n    result = benchmark(_call)\n    assert isinstance(result, list)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_network_only_history_plus_recall(\n    benchmark, n_history_pairs: int\n) -> None:\n    \"\"\"ONLY the cloud recall call (history + facts), no injection.\"\"\"\n    if not os.environ.get(\"MEMORI_API_KEY\"):\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud network-only overhead.\")\n\n    entity_id = os.environ.get(\"BENCHMARK_cloud_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_history_pairs,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    recall = Recall(cfg)\n\n    def _call():\n        data = recall._cloud_recall(query=\"What do I like?\")\n        facts, _messages = recall._parse_cloud_recall_response(data)\n        return facts\n\n    result = benchmark(_call)\n    assert isinstance(result, list)\n\n\ndef _make_invoke_with_stubbed_history(\n    cfg: Config, *, n_history_pairs: int\n) -> BaseInvoke:\n    history: list[dict[str, str]] = []\n    for i in range(n_history_pairs):\n        history.append({\"role\": \"user\", \"content\": f\"I like item_{i}.\"})\n        history.append({\"role\": \"assistant\", \"content\": \"Ok.\"})\n    invoke = BaseInvoke(cfg, lambda **_kwargs: None)\n    invoke._cloud_conversation_messages = history\n    return invoke\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_injection_only_history(\n    benchmark, n_history_pairs: int\n) -> None:\n    \"\"\"ONLY Python-side history injection (no network).\"\"\"\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    query = \"What do I like?\"\n    invoke = _make_invoke_with_stubbed_history(cfg, n_history_pairs=n_history_pairs)\n\n    def _call():\n        kwargs = {\"messages\": [{\"role\": \"user\", \"content\": query}]}\n        return invoke.inject_conversation_messages(kwargs)\n\n    result = benchmark(_call)\n    assert isinstance(result, dict)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_recalled_facts\", [5, 20], ids=[\"n5\", \"n20\"])\ndef test_benchmark_cloud_injection_only_recalled_facts(\n    benchmark, n_recalled_facts: int, mocker\n) -> None:\n    \"\"\"ONLY Python-side recalled-facts injection (no network).\"\"\"\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    fake_facts: list[dict[str, object]] = [\n        {\"content\": f\"User likes item_{i}\", \"rank_score\": 1.0, \"date_created\": None}\n        for i in range(n_recalled_facts)\n    ]\n    mocker.patch(\"memori.memory.recall.Recall.search_facts\", return_value=fake_facts)\n\n    invoke = BaseInvoke(cfg, lambda **_kwargs: None)\n    query = \"What do I like?\"\n\n    def _call():\n        kwargs = {\"messages\": [{\"role\": \"user\", \"content\": query}]}\n        return invoke.inject_recalled_facts(kwargs)\n\n    result = benchmark(_call)\n    assert isinstance(result, dict)\n"
  },
  {
    "path": "benchmarks/perf/test_hosted_recall_benchmarks.py",
    "content": "\"\"\"Cloud (Memori API) performance / latency benchmarks.\n\nThese benchmarks exercise the cloud recall path (network + API latency) rather than\nlocal DB-backed recall.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom uuid import uuid4\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.llm._base import BaseInvoke\nfrom memori.llm._constants import OPENAI_LLM_PROVIDER\nfrom memori.memory._manager import Manager\nfrom memori.memory.recall import Recall\n\n\ndef _make_cloud_cfg(*, entity_id: str, process_id: str) -> Config:\n    cfg = Config()\n    cfg.cloud = True\n    cfg.entity_id = entity_id\n    cfg.process_id = process_id\n    if cfg.session_id is None:\n        cfg.session_id = uuid4()\n    # Pick a provider so injection rules are deterministic.\n    cfg.llm.provider = OPENAI_LLM_PROVIDER\n    cfg.framework.provider = \"bench\"\n    return cfg\n\n\ndef _seed_cloud_messages(\n    cfg: Config, *, n_messages: int, entity_id: str, process_id: str\n) -> None:\n    cfg.cloud = True\n    cfg.entity_id = entity_id\n    cfg.process_id = process_id\n    if cfg.session_id is None:\n        cfg.session_id = uuid4()\n\n    messages: list[dict[str, object]] = []\n    for i in range(n_messages):\n        messages.append({\"role\": \"user\", \"type\": None, \"text\": f\"I like item_{i}.\"})\n        messages.append({\"role\": \"assistant\", \"type\": None, \"text\": \"Ok.\"})\n\n    Manager(cfg).execute(\n        {\n            \"attribution\": {\n                \"entity\": {\"id\": str(entity_id)},\n                \"process\": {\"id\": str(process_id)},\n            },\n            \"messages\": messages,\n            \"session\": {\"id\": str(cfg.session_id)},\n        }\n    )\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_messages\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_recall_latency(benchmark, n_messages: int) -> None:\n    \"\"\"Benchmark end-to-end cloud recall latency.\n\n    Includes: HTTP request + cloud service time. Excludes: local DB retrieval.\n    \"\"\"\n    os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n    api_key = os.environ.get(\"MEMORI_API_KEY\")\n    if not api_key:\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud recall.\")\n\n    cfg = Config()\n\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_messages,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    recall = Recall(cfg)\n\n    def _call():\n        return recall.search_facts(query=\"What do I like?\", limit=5)\n\n    result = benchmark(_call)\n    assert isinstance(result, list)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_pre_llm_overhead(benchmark, n_history_pairs: int) -> None:\n    \"\"\"Benchmark Memori overhead up to the LLM call (cloud mode).\n\n    This measures the \"added latency\" before calling an LLM:\n    - (optional) fetch cloud conversation history\n    - cloud recall request (server-side embeddings + retrieval)\n    - prompt/message injection logic\n\n    It intentionally does NOT call an LLM provider.\n    \"\"\"\n    if not os.environ.get(\"MEMORI_API_KEY\"):\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud pre-LLM overhead.\")\n\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    # Seed a stable session for cloud history fetch.\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_history_pairs,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    query = \"What do I like?\"\n\n    def _prepare():\n        invoke = BaseInvoke(cfg, lambda **_kwargs: None)\n        kwargs = {\"messages\": [{\"role\": \"user\", \"content\": query}]}\n        kwargs = invoke.inject_conversation_messages(kwargs)\n        kwargs = invoke.inject_recalled_facts(kwargs)\n        return kwargs\n\n    result = benchmark(_prepare)\n    assert isinstance(result, dict)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_network_history_get(benchmark, n_history_pairs: int) -> None:\n    \"\"\"cloud conversation history fetched via recall (POST), no injection.\"\"\"\n    if not os.environ.get(\"MEMORI_API_KEY\"):\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud history GET.\")\n\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_history_pairs,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    def _call():\n        recall = Recall(cfg)\n        data = recall._cloud_recall(query=\"History fetch benchmark\")\n        _facts, messages = recall._parse_cloud_recall_response(data)\n        return messages\n\n    result = benchmark(_call)\n    assert isinstance(result, list)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_network_recall_post(benchmark, n_history_pairs: int) -> None:\n    \"\"\"ONLY cloud recall call (POST /recall), no injection.\n\n    We seed N history pairs first so cloud has a consistent amount of data.\n    \"\"\"\n    if not os.environ.get(\"MEMORI_API_KEY\"):\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud recall POST.\")\n\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_cloud_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_history_pairs,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    recall = Recall(cfg)\n\n    def _call():\n        return recall.search_facts(query=\"What do I like?\", limit=5)\n\n    result = benchmark(_call)\n    assert isinstance(result, list)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_network_only_history_plus_recall(\n    benchmark, n_history_pairs: int\n) -> None:\n    \"\"\"ONLY the cloud recall call (history + facts), no injection.\"\"\"\n    if not os.environ.get(\"MEMORI_API_KEY\"):\n        pytest.skip(\"Set MEMORI_API_KEY to benchmark cloud network-only overhead.\")\n\n    entity_id = os.environ.get(\"BENCHMARK_cloud_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    _seed_cloud_messages(\n        cfg,\n        n_messages=n_history_pairs,\n        entity_id=entity_id,\n        process_id=process_id,\n    )\n\n    recall = Recall(cfg)\n\n    def _call():\n        data = recall._cloud_recall(query=\"What do I like?\")\n        facts, _messages = recall._parse_cloud_recall_response(data)\n        return facts\n\n    result = benchmark(_call)\n    assert isinstance(result, list)\n\n\ndef _make_invoke_with_stubbed_history(\n    cfg: Config, *, n_history_pairs: int\n) -> BaseInvoke:\n    history: list[dict[str, str]] = []\n    for i in range(n_history_pairs):\n        history.append({\"role\": \"user\", \"content\": f\"I like item_{i}.\"})\n        history.append({\"role\": \"assistant\", \"content\": \"Ok.\"})\n    invoke = BaseInvoke(cfg, lambda **_kwargs: None)\n    invoke._cloud_conversation_messages = history\n    return invoke\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_history_pairs\", [20, 100], ids=[\"n20\", \"n100\"])\ndef test_benchmark_cloud_injection_only_history(\n    benchmark, n_history_pairs: int\n) -> None:\n    \"\"\"ONLY Python-side history injection (no network).\"\"\"\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    query = \"What do I like?\"\n    invoke = _make_invoke_with_stubbed_history(cfg, n_history_pairs=n_history_pairs)\n\n    def _call():\n        kwargs = {\"messages\": [{\"role\": \"user\", \"content\": query}]}\n        return invoke.inject_conversation_messages(kwargs)\n\n    result = benchmark(_call)\n    assert isinstance(result, dict)\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"n_recalled_facts\", [5, 20], ids=[\"n5\", \"n20\"])\ndef test_benchmark_cloud_injection_only_recalled_facts(\n    benchmark, n_recalled_facts: int, mocker\n) -> None:\n    \"\"\"ONLY Python-side recalled-facts injection (no network).\"\"\"\n    entity_id = os.environ.get(\"BENCHMARK_CLOUD_ENTITY_ID\", \"bench-cloud-entity\")\n    process_id = os.environ.get(\"BENCHMARK_CLOUD_PROCESS_ID\", \"bench-cloud-process\")\n    cfg = _make_cloud_cfg(entity_id=entity_id, process_id=process_id)\n\n    fake_facts: list[dict[str, object]] = [\n        {\"content\": f\"User likes item_{i}\", \"rank_score\": 1.0, \"date_created\": None}\n        for i in range(n_recalled_facts)\n    ]\n    mocker.patch(\"memori.memory.recall.Recall.search_facts\", return_value=fake_facts)\n\n    invoke = BaseInvoke(cfg, lambda **_kwargs: None)\n    query = \"What do I like?\"\n\n    def _call():\n        kwargs = {\"messages\": [{\"role\": \"user\", \"content\": query}]}\n        return invoke.inject_recalled_facts(kwargs)\n\n    result = benchmark(_call)\n    assert isinstance(result, dict)\n"
  },
  {
    "path": "benchmarks/perf/test_recall_benchmarks.py",
    "content": "\"\"\"Performance benchmarks for Memori recall functionality.\"\"\"\n\nimport datetime\nimport os\nfrom time import perf_counter\nfrom typing import cast\n\nimport pytest\n\nfrom benchmarks.perf._results import append_csv_row, results_dir\nfrom benchmarks.perf.memory_utils import measure_peak_rss_bytes\nfrom memori._config import Config\nfrom memori.embeddings import embed_texts\nfrom memori.memory.recall import Recall\nfrom memori.search import find_similar_embeddings\nfrom memori.search._lexical import lexical_scores_for_ids  # noqa: PLC2701\nfrom memori.search._types import FactId\n\n\ndef _default_benchmark_csv_path() -> str:\n    return str(results_dir() / \"recall_benchmarks.csv\")\n\n\ndef _write_benchmark_row(*, benchmark, row: dict[str, object]) -> None:\n    csv_path = (\n        os.environ.get(\"BENCHMARK_RESULTS_CSV_PATH\") or _default_benchmark_csv_path()\n    )\n    stats = getattr(benchmark, \"stats\", None)\n    row_out: dict[str, object] = dict(row)\n    row_out[\"timestamp_utc\"] = datetime.datetime.now(datetime.timezone.utc).isoformat()\n\n    for key in (\n        \"mean\",\n        \"stddev\",\n        \"median\",\n        \"min\",\n        \"max\",\n        \"rounds\",\n        \"iterations\",\n        \"ops\",\n    ):\n        value = getattr(stats, key, None) if stats is not None else None\n        if value is not None:\n            row_out[key] = value\n\n    header = [\n        \"timestamp_utc\",\n        \"test\",\n        \"db\",\n        \"fact_count\",\n        \"query_size\",\n        \"retrieval_limit\",\n        \"one_shot_seconds\",\n        \"peak_rss_bytes\",\n        \"mean\",\n        \"stddev\",\n        \"median\",\n        \"min\",\n        \"max\",\n        \"rounds\",\n        \"iterations\",\n        \"ops\",\n    ]\n    append_csv_row(csv_path, header=header, row=row_out)\n\n\n@pytest.mark.benchmark\nclass TestQueryEmbeddingBenchmarks:\n    \"\"\"Benchmarks for query embedding generation.\"\"\"\n\n    def test_benchmark_query_embedding_short(self, benchmark, sample_queries):\n        \"\"\"Benchmark embedding generation for short queries.\"\"\"\n        query = sample_queries[\"short\"][0]\n        cfg = Config()\n\n        def _embed():\n            return embed_texts(\n                query,\n                model=cfg.embeddings.model,\n            )\n\n        start = perf_counter()\n        result = benchmark(_embed)\n        one_shot_seconds = perf_counter() - start\n        assert len(result) > 0\n        assert len(result[0]) > 0\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"query_embedding_short\",\n                \"db\": \"\",\n                \"fact_count\": \"\",\n                \"query_size\": \"short\",\n                \"retrieval_limit\": \"\",\n                \"one_shot_seconds\": one_shot_seconds,\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n\n    def test_benchmark_query_embedding_medium(self, benchmark, sample_queries):\n        \"\"\"Benchmark embedding generation for medium-length queries.\"\"\"\n        query = sample_queries[\"medium\"][0]\n        cfg = Config()\n\n        def _embed():\n            return embed_texts(\n                query,\n                model=cfg.embeddings.model,\n            )\n\n        start = perf_counter()\n        result = benchmark(_embed)\n        one_shot_seconds = perf_counter() - start\n        assert len(result) > 0\n        assert len(result[0]) > 0\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"query_embedding_medium\",\n                \"db\": \"\",\n                \"fact_count\": \"\",\n                \"query_size\": \"medium\",\n                \"retrieval_limit\": \"\",\n                \"one_shot_seconds\": one_shot_seconds,\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n\n    def test_benchmark_query_embedding_long(self, benchmark, sample_queries):\n        \"\"\"Benchmark embedding generation for long queries.\"\"\"\n        query = sample_queries[\"long\"][0]\n        cfg = Config()\n\n        def _embed():\n            return embed_texts(\n                query,\n                model=cfg.embeddings.model,\n            )\n\n        start = perf_counter()\n        result = benchmark(_embed)\n        one_shot_seconds = perf_counter() - start\n        assert len(result) > 0\n        assert len(result[0]) > 0\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"query_embedding_long\",\n                \"db\": \"\",\n                \"fact_count\": \"\",\n                \"query_size\": \"long\",\n                \"retrieval_limit\": \"\",\n                \"one_shot_seconds\": one_shot_seconds,\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n\n    def test_benchmark_query_embedding_batch(self, benchmark, sample_queries):\n        \"\"\"Benchmark embedding generation for multiple queries at once.\"\"\"\n        queries = sample_queries[\"short\"][:5]\n        cfg = Config()\n\n        def _embed():\n            return embed_texts(\n                queries,\n                model=cfg.embeddings.model,\n            )\n\n        start = perf_counter()\n        result = benchmark(_embed)\n        one_shot_seconds = perf_counter() - start\n        assert len(result) == len(queries)\n        assert all(len(emb) > 0 for emb in result)\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"query_embedding_batch\",\n                \"db\": \"\",\n                \"fact_count\": \"\",\n                \"query_size\": \"batch\",\n                \"retrieval_limit\": \"\",\n                \"one_shot_seconds\": one_shot_seconds,\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n\n\n@pytest.mark.benchmark\nclass TestDatabaseEmbeddingRetrievalBenchmarks:\n    \"\"\"Benchmarks for database embedding retrieval.\"\"\"\n\n    def test_benchmark_db_embedding_retrieval(\n        self, benchmark, memori_instance, entity_with_n_facts\n    ):\n        \"\"\"Benchmark retrieving embeddings from database for different fact counts.\"\"\"\n        entity_db_id = entity_with_n_facts[\"entity_db_id\"]\n        fact_count = entity_with_n_facts[\"fact_count\"]\n        entity_fact_driver = memori_instance.config.storage.driver.entity_fact\n\n        def _retrieve():\n            return entity_fact_driver.get_embeddings(entity_db_id, limit=fact_count)\n\n        _, peak_rss = measure_peak_rss_bytes(_retrieve)\n        if peak_rss is not None:\n            benchmark.extra_info[\"peak_rss_bytes\"] = peak_rss\n\n        result = benchmark(_retrieve)\n        assert len(result) == fact_count\n        assert all(\"id\" in row and \"content_embedding\" in row for row in result)\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"db_embedding_retrieval\",\n                \"db\": entity_with_n_facts[\"db_type\"],\n                \"fact_count\": fact_count,\n                \"query_size\": \"\",\n                \"retrieval_limit\": \"\",\n                \"one_shot_seconds\": \"\",\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n\n\n@pytest.mark.benchmark\nclass TestDatabaseFactContentRetrievalBenchmarks:\n    \"\"\"Benchmarks for fetching fact content by ids (final recall DB step).\n\n    This benchmarks the final step after semantic search has already identified\n    the top-k most similar embeddings. We only retrieve content for those top results\n    (typically 5-10 facts), not all facts in the database.\n    \"\"\"\n\n    @pytest.mark.parametrize(\"retrieval_limit\", [5, 10], ids=[\"limit5\", \"limit10\"])\n    def test_benchmark_db_fact_content_retrieval(\n        self, benchmark, memori_instance, entity_with_n_facts, retrieval_limit\n    ):\n        \"\"\"Benchmark retrieving content for top-k facts after semantic search.\n\n        Args:\n            retrieval_limit: Number of fact IDs to retrieve content for (after semantic\n                search has already filtered to top results). This should be small (5-10).\n        \"\"\"\n        entity_db_id = entity_with_n_facts[\"entity_db_id\"]\n        entity_fact_driver = memori_instance.config.storage.driver.entity_fact\n\n        # Simulate semantic search returning top-k IDs (outside benchmark timing)\n        # In real flow: get_embeddings(embeddings_limit=1000) -> FAISS search -> top-k IDs\n        seed_rows = entity_fact_driver.get_embeddings(\n            entity_db_id, limit=retrieval_limit\n        )\n        fact_ids = [row[\"id\"] for row in seed_rows]\n\n        def _retrieve():\n            return entity_fact_driver.get_facts_by_ids(fact_ids)\n\n        _, peak_rss = measure_peak_rss_bytes(_retrieve)\n        if peak_rss is not None:\n            benchmark.extra_info[\"peak_rss_bytes\"] = peak_rss\n\n        result = benchmark(_retrieve)\n        assert len(result) == len(fact_ids)\n        assert all(\"id\" in row and \"content\" in row for row in result)\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"db_fact_content_retrieval\",\n                \"db\": entity_with_n_facts[\"db_type\"],\n                \"fact_count\": entity_with_n_facts[\"fact_count\"],\n                \"query_size\": \"\",\n                \"retrieval_limit\": retrieval_limit,\n                \"one_shot_seconds\": \"\",\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n\n\n@pytest.mark.benchmark\nclass TestSemanticSearchBenchmarks:\n    \"\"\"Benchmarks for semantic search (FAISS similarity search).\"\"\"\n\n    def test_benchmark_semantic_search(\n        self, benchmark, memori_instance, entity_with_n_facts, sample_queries\n    ):\n        \"\"\"Benchmark FAISS similarity search for different embedding counts.\"\"\"\n        entity_db_id = entity_with_n_facts[\"entity_db_id\"]\n        fact_count = entity_with_n_facts[\"fact_count\"]\n        entity_fact_driver = memori_instance.config.storage.driver.entity_fact\n\n        db_results = entity_fact_driver.get_embeddings(entity_db_id, limit=fact_count)\n        embeddings = [(row[\"id\"], row[\"content_embedding\"]) for row in db_results]\n\n        query = sample_queries[\"short\"][0]\n        query_embedding = embed_texts(\n            query,\n            model=memori_instance.config.embeddings.model,\n        )[0]\n\n        def _search():\n            return find_similar_embeddings(embeddings, query_embedding, limit=5)\n\n        _, peak_rss = measure_peak_rss_bytes(_search)\n        if peak_rss is not None:\n            benchmark.extra_info[\"peak_rss_bytes\"] = peak_rss\n\n        result = benchmark(_search)\n        assert len(result) > 0\n        assert all(isinstance(item, tuple) and len(item) == 2 for item in result)\n        assert all(\n            isinstance(item[0], int) and isinstance(item[1], float) for item in result\n        )\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"semantic_search_faiss\",\n                \"db\": entity_with_n_facts[\"db_type\"],\n                \"fact_count\": fact_count,\n                \"query_size\": \"short\",\n                \"retrieval_limit\": \"\",\n                \"one_shot_seconds\": \"\",\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n\n\n@pytest.mark.benchmark\nclass TestLexicalBenchmarks:\n    \"\"\"Benchmarks for BM25 scoring.\"\"\"\n\n    @pytest.mark.parametrize(\"fact_count\", [200, 1000], ids=[\"facts200\", \"facts1000\"])\n    def test_benchmark_bm25_scoring(self, benchmark, fact_count):\n        ids = list(range(1, fact_count + 1))\n        content_map = {i: f\"fact {i} user likes blue pizza coffee {i % 7}\" for i in ids}\n        query_text = \"blue pizza\"\n\n        def _score():\n            return lexical_scores_for_ids(\n                query_text=query_text,\n                ids=cast(list[FactId], ids),\n                content_map=cast(dict[FactId, str], content_map),\n            )\n\n        _, peak_rss = measure_peak_rss_bytes(_score)\n        if peak_rss is not None:\n            benchmark.extra_info[\"peak_rss_bytes\"] = peak_rss\n\n        result = benchmark(_score)\n        assert isinstance(result, dict)\n        assert len(result) == fact_count\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"bm25_scoring\",\n                \"db\": \"\",\n                \"fact_count\": fact_count,\n                \"query_size\": \"short\",\n                \"retrieval_limit\": \"\",\n                \"one_shot_seconds\": \"\",\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n\n\n@pytest.mark.benchmark\nclass TestEndToEndRecallBenchmarks:\n    \"\"\"Benchmarks for end-to-end recall (embed query + DB + FAISS + content fetch).\"\"\"\n\n    @pytest.mark.parametrize(\n        \"query_size\",\n        [\"short\", \"medium\", \"long\"],\n        ids=[\"short_query\", \"medium_query\", \"long_query\"],\n    )\n    def test_benchmark_end_to_end_recall(\n        self,\n        benchmark,\n        memori_instance,\n        entity_with_n_facts,\n        sample_queries,\n        query_size,\n    ):\n        entity_db_id = entity_with_n_facts[\"entity_db_id\"]\n        query = sample_queries[query_size][0]\n\n        recall = Recall(memori_instance.config)\n\n        def _recall():\n            return recall.search_facts(query=query, limit=5, entity_id=entity_db_id)\n\n        _, peak_rss = measure_peak_rss_bytes(_recall)\n        if peak_rss is not None:\n            benchmark.extra_info[\"peak_rss_bytes\"] = peak_rss\n\n        start = perf_counter()\n        result = benchmark(_recall)\n        one_shot_seconds = perf_counter() - start\n        assert isinstance(result, list)\n        assert len(result) <= 5\n        _write_benchmark_row(\n            benchmark=benchmark,\n            row={\n                \"test\": \"end_to_end_recall\",\n                \"db\": entity_with_n_facts[\"db_type\"],\n                \"fact_count\": entity_with_n_facts[\"fact_count\"],\n                \"query_size\": query_size,\n                \"retrieval_limit\": \"\",\n                \"one_shot_seconds\": one_shot_seconds,\n                \"peak_rss_bytes\": benchmark.extra_info.get(\"peak_rss_bytes\", \"\"),\n            },\n        )\n"
  },
  {
    "path": "benchmarks/scripts/fetch_locomo.py",
    "content": "from __future__ import annotations\n\nimport argparse\nimport hashlib\nimport shutil\nimport sys\nfrom pathlib import Path\n\n\ndef main(argv: list[str] | None = None) -> int:\n    parser = argparse.ArgumentParser(\n        description=\"Fetch/prepare LoCoMo dataset locally (does not commit data to git).\"\n    )\n    parser.add_argument(\n        \"--source\",\n        default=\"\",\n        help=\"Path to an existing LoCoMo JSON file to copy into the cache.\",\n    )\n    parser.add_argument(\n        \"--dest\",\n        default=\"\",\n        help=\"Destination file path (defaults to ~/.cache/memori/benchmarks/locomo/locomo.json).\",\n    )\n    parser.add_argument(\n        \"--print-dest\",\n        action=\"store_true\",\n        help=\"Print resolved destination path and exit.\",\n    )\n    args = parser.parse_args(argv)\n\n    dest = _default_dest() if not args.dest else Path(args.dest).expanduser()\n    dest = dest.resolve()\n\n    if args.print_dest:\n        print(str(dest))\n        return 0\n\n    if not args.source:\n        print(\n            \"No --source provided.\\n\\n\"\n            \"Download LoCoMo from Snap Research and pass the JSON path here.\\n\"\n            \"Upstream repo: https://github.com/snap-research/locomo\\n\",\n            file=sys.stderr,\n        )\n        return 2\n\n    src = Path(args.source).expanduser().resolve()\n    if not src.exists():\n        print(f\"Source file not found: {src}\", file=sys.stderr)\n        return 2\n\n    dest.parent.mkdir(parents=True, exist_ok=True)\n    shutil.copyfile(src, dest)\n\n    digest = _sha256(dest)\n    print(f\"Wrote: {dest}\")\n    print(f\"sha256: {digest}\")\n    return 0\n\n\ndef _default_dest() -> Path:\n    return Path(\"~/.cache/memori/benchmarks/locomo/locomo.json\").expanduser()\n\n\ndef _sha256(path: Path) -> str:\n    h = hashlib.sha256()\n    with path.open(\"rb\") as f:\n        for chunk in iter(lambda: f.read(1024 * 1024), b\"\"):\n            h.update(chunk)\n    return h.hexdigest()\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n"
  },
  {
    "path": "conftest.py",
    "content": "import pytest\n\nfrom memori._config import Config\nfrom memori.storage import Manager as StorageManager\n\n\n@pytest.fixture\ndef mock_mysql_session(mocker):\n    session = mocker.MagicMock()\n    session.get_bind.return_value.dialect.name = \"mysql\"\n    type(session).__module__ = \"sqlalchemy.orm.session\"\n\n    mock_result = mocker.MagicMock()\n    mock_result.mappings.return_value.fetchone.return_value = {\"1\": 1}\n    mock_result.mappings.return_value.fetchall.return_value = []\n    session.connection.return_value.exec_driver_sql.return_value = mock_result\n\n    return session\n\n\n@pytest.fixture\ndef mock_postgres_session(mocker):\n    session = mocker.MagicMock()\n    session.get_bind.return_value.dialect.name = \"postgresql\"\n    type(session).__module__ = \"sqlalchemy.orm.session\"\n\n    mock_result = mocker.MagicMock()\n    mock_result.mappings.return_value.fetchone.return_value = {\"one\": 1}\n    mock_result.mappings.return_value.fetchall.return_value = []\n    session.connection.return_value.exec_driver_sql.return_value = mock_result\n\n    return session\n\n\n@pytest.fixture\ndef session(mock_mysql_session):\n    return mock_mysql_session\n\n\n@pytest.fixture\ndef postgres_session(mock_postgres_session):\n    return mock_postgres_session\n\n\n@pytest.fixture\ndef config(mocker, session):\n    config = Config()\n    config.storage = StorageManager(config)\n    config.storage.adapter = mocker.MagicMock()\n    config.storage.driver = mocker.MagicMock()\n    return config\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  dev:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    volumes:\n      - .:/app\n      - /app/.venv\n      - /app/__pycache__\n      - ./google-credentials.json:/app/google-credentials.json:ro\n    working_dir: /app\n    stdin_open: true\n    tty: true\n    env_file:\n      - .env\n    environment:\n      - PYTHONUNBUFFERED=1\n      - DATABASE_URL=postgresql://memori:memori@postgres:5432/memori_test\n      - MONGODB_URL=mongodb://memori:memori@mongodb:27017/memori_test?authSource=admin\n      - MYSQL_DATABASE_URL=mysql+pymysql://memori:memori@mysql:3306/memori_test\n      - OCEANBASE_DATABASE_URL=mysql+oceanbase://root:@oceanbase:2881/memori_test?charset=utf8mb4\n      - ORACLE_DATABASE_URL=oracle+oracledb://system:memori@oracle:1521/?service_name=FREEPDB1\n      - GOOGLE_APPLICATION_CREDENTIALS=/app/google-credentials.json\n    command: /bin/bash\n    depends_on:\n      postgres:\n        condition: service_healthy\n      mongodb:\n        condition: service_healthy\n      mysql:\n        condition: service_healthy\n      oceanbase:\n        condition: service_started\n      oracle:\n        condition: service_healthy\n    networks:\n      - memori-network\n\n  postgres:\n    image: postgres:16\n    environment:\n      POSTGRES_USER: memori\n      POSTGRES_PASSWORD: memori\n      POSTGRES_DB: memori_test\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U memori\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n    networks:\n      - memori-network\n\n  mongodb:\n    image: mongo:7.0\n    environment:\n      MONGO_INITDB_ROOT_USERNAME: memori\n      MONGO_INITDB_ROOT_PASSWORD: memori\n      MONGO_INITDB_DATABASE: memori_test\n    ports:\n      - \"27017:27017\"\n    volumes:\n      - mongodb_data:/data/db\n    healthcheck:\n      test: [\"CMD\", \"mongosh\", \"--eval\", \"db.adminCommand('ping')\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n    networks:\n      - memori-network\n\n  mongo-express:\n    image: mongo-express:1.0.0\n    restart: unless-stopped\n    ports:\n      - \"8081:8081\"\n    environment:\n      ME_CONFIG_MONGODB_ADMINUSERNAME: memori\n      ME_CONFIG_MONGODB_ADMINPASSWORD: memori\n      ME_CONFIG_MONGODB_URL: mongodb://memori:memori@mongodb:27017/\n      ME_CONFIG_BASICAUTH_USERNAME: admin\n      ME_CONFIG_BASICAUTH_PASSWORD: pass\n    depends_on:\n      - mongodb\n    networks:\n      - memori-network\n\n  mysql:\n    image: mysql:8\n    environment:\n      MYSQL_ROOT_PASSWORD: memori\n      MYSQL_DATABASE: memori_test\n      MYSQL_USER: memori\n      MYSQL_PASSWORD: memori\n    ports:\n      - \"3307:3306\"\n    volumes:\n      - mysql_data:/var/lib/mysql\n    healthcheck:\n      test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"localhost\", \"-u\", \"memori\", \"-pmemori\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n    networks:\n      - memori-network\n\n  oceanbase:\n    image: quay.io/oceanbase/seekdb:latest\n    ports:\n      - \"2882:2881\"\n    networks:\n      - memori-network\n\n  oracle:\n    image: container-registry.oracle.com/database/free:latest-lite\n    environment:\n      ORACLE_PWD: memori\n      ORACLE_CHARACTERSET: AL32UTF8\n    ports:\n      - \"1521:1521\"\n    volumes:\n      - oracle_data:/opt/oracle/oradata\n    shm_size: 2gb\n    healthcheck:\n      test: [\"CMD-SHELL\", \"echo 'SELECT 1 FROM DUAL;' | sqlplus -s system/memori@FREEPDB1 || exit 1\"]\n      interval: 15s\n      timeout: 10s\n      retries: 20\n      start_period: 90s\n    networks:\n      - memori-network\n\nvolumes:\n  postgres_data:\n  mongodb_data:\n  mysql_data:\n  oracle_data:\n\nnetworks:\n  memori-network:\n    driver: bridge\n"
  },
  {
    "path": "docs/memori-byodb/concepts/advanced-augmentation.mdx",
    "content": "---\ntitle: Advanced Augmentation\ndescription: How Memori's Advanced Augmentation engine extracts structured facts, preferences, and knowledge from your AI conversations — all stored in your own database.\n---\n\n# Advanced Augmentation\n\nAdvanced Augmentation is the AI engine inside Memori that turns raw conversations into structured, searchable memories. It runs asynchronously in the background to minimize impact on your response path. All extracted data is stored directly in your database.\n\n## What It Does\n\nWhen your application has a conversation through a Memori-wrapped LLM client, the augmentation engine:\n\n1. Reads the full conversation (user messages and AI responses)\n2. Identifies facts, preferences, skills, attributes, and events\n3. Extracts semantic triples (subject-predicate-object relationships)\n4. Generates vector embeddings for semantic search\n5. Stores everything in your database\n\nNo extra code required — just initialize Memori with your database connection and set attribution.\n\n## How It Works\n\nThe augmentation flow is fully asynchronous and designed to avoid blocking your main request path.\n\n1. Your app makes an LLM call through the wrapped client\n2. Memori returns the response immediately\n3. In the background, the conversation is queued for processing\n4. The augmentation engine extracts structured memories\n5. Memories are stored in your database for future recall\n\nIn short-lived scripts, call `mem.augmentation.wait()` to ensure processing completes before exit.\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\nmem.config.storage.build()\n\n# This returns immediately — no augmentation delay\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"I love hiking in the mountains.\"}\n    ]\n)\nprint(response.choices[0].message.content)\n\n# Only needed in short-lived scripts\nmem.augmentation.wait()\n```\n\n## Extraction Types\n\n| Type                   | What it captures                                        | Scope                                |\n| ---------------------- | ------------------------------------------------------- | ------------------------------------ |\n| **Facts**              | Objective information with vector embeddings            | Per entity — shared across processes |\n| **Preferences**        | User choices, opinions, and tastes                      | Per entity                           |\n| **Skills & Knowledge** | Abilities and expertise levels                          | Per entity                           |\n| **Attributes**         | Process-level information about what your agent handles | Per process                          |\n\n**Database tables:**\n\n| Table                         | Purpose                                      |\n| ----------------------------- | -------------------------------------------- |\n| `memori_conversation`         | Stores conversations                         |\n| `memori_conversation_message` | Individual messages within conversations     |\n| `memori_session`              | Groups related LLM interactions              |\n| `memori_entity`               | Entities (users, organizations, etc.)        |\n| `memori_process`              | Processes (agents, bots, workflows)          |\n| `memori_entity_fact`          | Extracted facts with vector embeddings       |\n| `memori_process_attribute`    | Process-level attributes and characteristics |\n\n## Semantic Triples\n\nAdvanced Augmentation uses named-entity recognition to extract semantic triples (subject, predicate, object). These form the building blocks of the [Knowledge Graph](/docs/memori-byodb/concepts/knowledge-graph).\n\n**Example** — from _\"My favorite database is PostgreSQL and I use it with FastAPI\"_:\n\n| Subject | Predicate         | Object               |\n| ------- | ----------------- | -------------------- |\n| user    | favorite_database | PostgreSQL           |\n| user    | uses              | FastAPI              |\n| user    | uses_with         | PostgreSQL + FastAPI |\n\nMemori automatically deduplicates triples — if the same fact is mentioned multiple times, it increments the mention count and updates the timestamp.\n\n**Database tables:** `memori_subject`, `memori_predicate`, `memori_object`, `memori_knowledge_graph`\n\n## Embeddings\n\nVector embeddings are created using the **all-mpnet-base-v2** sentence transformer with **768 dimensions**. These embeddings power the semantic search used for context recall via cosine similarity.\n\n## Context Recall\n\nWhen a query is sent to an LLM through a wrapped client, Memori automatically:\n\n1. Intercepts the outbound LLM call\n2. Uses semantic search to find entity facts matching the query\n3. Passes vector embeddings to FAISS for similarity ranking\n4. Injects the top-N most relevant facts into the system prompt\n5. Forwards the enriched request to the LLM provider\n\n## Schema ERD\n\n![Memori Schema ERD](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/erd.png)\n\n## When to Use It\n\n- Chatbots or AI assistants with returning users\n- Use cases that need to remember user preferences across sessions\n- You want personalized AI interactions\n- With multi-step workflows or agentic systems\n- For building relationships between entities\n\n<Admonition type=\"important\" title=\"Attribution is Required\">\n  For Memori to provide all Advanced Augmentation capabilities, attribution must\n  be set before making LLM calls. Without attribution, Memori cannot create or\n  recall memories.\n</Admonition>\n"
  },
  {
    "path": "docs/memori-byodb/concepts/architecture.mdx",
    "content": "---\ntitle: Architecture\ndescription: Understand how Memori's open-source architecture works — from your app to your own database, with local storage, augmentation, and recall.\n---\n\n# Architecture\n\nMemori is a modular memory layer for AI applications. You connect your LLM client, set attribution, point Memori at your database, and it handles everything else — storage, augmentation, knowledge graph construction, and recall. All data stays on your infrastructure.\n\n## System Overview\n\n![\"Memori BYODB System Overview\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-byodb-architecture-detail.webp)\n\n## Core Components\n\n**Memori Core** — The central coordinator between your application and your database. Manages attribution, coordinates storage and augmentation, provides LLM wrappers, and exposes the Recall API.\n\n**LLM Provider Wrappers** — Wraps your existing LLM client transparently. Intercepts calls, captures messages and responses, persists conversation data to your database. Supports sync, async, and streaming.\n\n**Attribution System** — Tags every memory with who created it and in what context. Tracks three dimensions: entity (the user), process (the agent), and session (the conversation thread).\n\n**Storage System** — Stores all data in your database with no external dependencies. Supports SQLAlchemy `sessionmaker`, DB-API 2.0 connections, Django ORM, and MongoDB. Works with SQLite, PostgreSQL, MySQL, MariaDB, Oracle, CockroachDB, and OceanBase, including providers like Neon, Supabase, and AWS RDS/Aurora.\n\n**Advanced Augmentation** — Turns raw conversations into structured memories. Extracts facts, preferences, and skills, generates vector embeddings locally, and builds a knowledge graph. Runs asynchronously with zero latency impact.\n\n## Configuration\n\nSetting up Memori requires a database connection and attribution:\n\n```python\nimport sqlite3\nfrom memori import Memori\nfrom openai import OpenAI\n\ndef get_connection():\n    return sqlite3.connect(\"memori.db\")\n\nclient = OpenAI()\nmem = Memori(conn=get_connection).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\nmem.config.storage.build()\n```\n\n## Data Flow\n\n1. **Conversation Capture** — Every LLM call through the wrapped client is captured and stored in your database. Your app gets the response immediately.\n\n2. **Attribution Tracking** — Attribution links every conversation to a specific entity and process so memories are properly scoped and indexed.\n\n3. **Augmentation** — After a conversation completes, Memori processes it asynchronously — extracts facts, generates embeddings locally, and builds knowledge graph triples.\n\n4. **Recall** — On the next LLM call, Memori surfaces the right memories at the right time: semantic search, intelligent ranking and decay, and seamless injection of the most relevant context into the system prompt so your AI stays contextually aware.\n"
  },
  {
    "path": "docs/memori-byodb/concepts/async-patterns.mdx",
    "content": "---\ntitle: Async Patterns\ndescription: Best practices for using Memori with async/await — AsyncOpenAI, AsyncAnthropic, FastAPI, and thread safety.\n---\n\n# Async Patterns\n\nMemori works with Python's async/await out of the box. Use `AsyncOpenAI` or `AsyncAnthropic` instead of their sync counterparts — everything else stays the same.\n\n## When to Use Async\n\n| Scenario                 | Async? | Why                         |\n| ------------------------ | ------ | --------------------------- |\n| Web servers (FastAPI)    | Yes    | Concurrent request handling |\n| Chatbots with many users | Yes    | Non-blocking I/O            |\n| CLI scripts              | No     | Sync is simpler             |\n| Jupyter notebooks        | No     | Event loop already running  |\n\n## Basic Async Setup\n\n```python\nimport os\nimport asyncio\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import AsyncOpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nasync def main():\n    client = AsyncOpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n    mem = Memori(conn=SessionLocal).llm.register(client)\n    mem.attribution(entity_id=\"user_123\", process_id=\"async_agent\")\n    mem.config.storage.build()\n\n    response = await client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"I prefer async Python.\"}]\n    )\n    print(response.choices[0].message.content)\n    mem.augmentation.wait()\n\nasyncio.run(main())\n```\n\nWorks identically with `AsyncAnthropic` — just swap the client.\n\n## FastAPI Example\n\n```python\nimport os\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import AsyncOpenAI\n\napp = FastAPI()\nengine = create_engine(\"sqlite:///memori.db\", connect_args={\"check_same_thread\": False})\nSessionLocal = sessionmaker(bind=engine)\nMemori(conn=SessionLocal).config.storage.build()\n\nclass ChatRequest(BaseModel):\n    message: str\n\n@app.post(\"/chat/{user_id}\")\nasync def chat(user_id: str, req: ChatRequest):\n    client = AsyncOpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n    mem = Memori(conn=SessionLocal).llm.register(client)\n    mem.attribution(entity_id=user_id, process_id=\"fastapi_async\")\n\n    response = await client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": req.message}]\n    )\n    return {\"response\": response.choices[0].message.content}\n```\n\n## Thread Safety\n\n| Pattern                         | Safe? | Why                       |\n| ------------------------------- | ----- | ------------------------- |\n| `conn=SessionLocal` (factory)   | Yes   | New session per operation |\n| `conn=lambda: existing_session` | No    | Shares one session        |\n\nFor production async apps, use PostgreSQL with larger pools:\n\n```python\nengine = create_engine(\n    \"postgresql+psycopg://user:pass@host/db\",\n    pool_pre_ping=True,\n    pool_size=20,\n    max_overflow=40,\n    pool_recycle=300\n)\n```\n"
  },
  {
    "path": "docs/memori-byodb/concepts/cli-quickstart.mdx",
    "content": "---\ntitle: CLI Quickstart\ndescription: Get started with the Memori command line interface for setup, diagnostics, and management.\n---\n\n# CLI Quickstart\n\nThe Memori CLI lets you manage your environment, check quotas, and perform setup tasks from the terminal. It is included with the `memori` package.\n\n## View Available Commands\n\n```bash\npython -m memori\n```\n\n## Essential Setup\n\nPre-download the embedding model that Memori uses for semantic search. Without this, the model downloads automatically on first use (slower).\n\n```bash\npython -m memori setup\n```\n\n## Command Reference\n\n| Command                      | Parameters | Description                                    |\n| ---------------------------- | ---------- | ---------------------------------------------- |\n| `setup`                      | None       | Prepare environment (download embedding model) |\n| `quota`                      | None       | Display available augmentation quota           |\n| `sign-up`                    | `<email>`  | Sign up for an API key                         |\n| `cockroachdb cluster start`  | None       | Start a new CockroachDB cluster                |\n| `cockroachdb cluster claim`  | None       | Get cluster claim URL                          |\n| `cockroachdb cluster delete` | None       | Delete cluster (destructive)                   |\n\n<CodeGroup title=\"CLI Commands\">\n\n```bash {{ title: 'Setup' }}\n# Cache the embedding model for faster runtime\npython -m memori setup\n```\n\n```bash {{ title: 'Check Quota' }}\n# View augmentation usage and limits\npython -m memori quota\n```\n\n```bash {{ title: 'Sign Up' }}\n# Get an API key for higher augmentation limits\npython -m memori sign-up your-email@example.com\n```\n\n```bash {{ title: 'CockroachDB' }}\n# Start a new serverless database cluster\npython -m memori cockroachdb cluster start\n\n# Get the cluster claim URL\npython -m memori cockroachdb cluster claim\n\n# Delete the cluster (destructive!)\npython -m memori cockroachdb cluster delete\n```\n\n</CodeGroup>\n\n<Admonition type=\"warning\" title=\"Destructive Commands\">\n  The `cockroachdb cluster delete` command permanently removes your cluster\n  including all data. This cannot be undone.\n</Admonition>\n"
  },
  {
    "path": "docs/memori-byodb/concepts/how-memory-works.mdx",
    "content": "---\ntitle: How Memori Works\ndescription: Understand the core concepts behind Memori — entities, processes, sessions, memory types, attribution, and how recall brings it all together.\n---\n\n# How Memori Works\n\nMemori gives your AI application long-term memory. Instead of forgetting everything after each conversation, your AI can remember facts, preferences, and context across sessions and across different applications — all stored in your own database.\n\n## Attribution\n\nEvery memory in Memori is tagged with three pieces of information: **who**, **what**, and **when**.\n\n- **Entity (`entity_id`)** — The person, place, or thing generating memories. Typically a user ID (e.g., `\"user_alice\"`, `\"company_acme\"`).\n- **Process (`process_id`)** — The agent, program, or workflow creating memories (e.g., `\"support_bot\"`, `\"code_review_agent\"`).\n- **Session (`session_id`)** — Groups related LLM interactions into a conversation thread. Auto-generated as a UUID by default.\n\nThe combination of `entity_id` + `process_id` + `session_id` creates a unique memory scope — different users have isolated memories, the same user can have different context in different applications, and each conversation is tracked separately.\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\n\n# Set attribution before any LLM calls\nmem.attribution(\n    entity_id=\"user_alice\",\n    process_id=\"support_bot\"\n)\n# session_id is auto-generated\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"I prefer dark mode.\"}\n    ]\n)\n```\n\n## Memory Types\n\nWhen you have a conversation through a Memori-wrapped LLM client, Advanced Augmentation extracts structured memories in the background:\n\n| Type            | What it captures           | Example                                         |\n| --------------- | -------------------------- | ----------------------------------------------- |\n| **Facts**       | Objective information      | \"User uses PostgreSQL for production databases\" |\n| **Preferences** | Choices, opinions, tastes  | \"Prefers concise answers\"                       |\n| **Skills**      | Abilities and expertise    | \"Experienced with React (5 years)\"              |\n| **Rules**       | Constraints and principles | \"Follows test-driven development\"               |\n| **Events**      | Milestones and occurrences | \"Product launched recently\"                     |\n\n## How Recall Works\n\nRecall brings stored memories back into your AI conversations. There are two modes.\n\n### Automatic Recall (Default)\n\nOn every LLM call, Memori automatically:\n\n1. Intercepts the outbound request\n2. Uses semantic search to find relevant facts for the current entity\n3. Injects the most relevant memories into the system prompt\n4. Forwards the enriched request to the LLM\n\nNo extra code required — it happens transparently.\n\n### Manual Recall\n\nUse `mem.recall()` when you want to retrieve memories explicitly — for custom prompts, displaying memories in a UI, or debugging.\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.attribution(entity_id=\"user_alice\", process_id=\"support_bot\")\n\nfacts = mem.recall(\"coding preferences\", limit=5)\n\nfor fact in facts:\n    print(f\"Fact: {fact.content}\")\n    print(f\"Score: {fact.similarity:.4f}\")\n```\n\nEach returned fact includes `id`, `content`, `similarity` (0–1 relevance score), `rank_score`, and `date_created`.\n\n### Recall Configuration\n\nMemori uses the **all-mpnet-base-v2** sentence transformer with cosine similarity for semantic search. You can tune recall behavior with these configuration options:\n\n| Option                                  | Default | Description                                        |\n| --------------------------------------- | ------- | -------------------------------------------------- |\n| `mem.config.recall_relevance_threshold` | `0.1`   | Minimum similarity score for a fact to be included |\n| `mem.config.recall_embeddings_limit`    | `1000`  | Maximum number of embeddings to compare against    |\n\n```python\n# Example: tune recall for broader or narrower results\nmem.config.recall_relevance_threshold = 0.05  # Lower = more results\nmem.config.recall_embeddings_limit = 500      # Reduce for lower memory usage\n```\n\n## Memory Lifecycle\n\n![\"Memori Lifecycle\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-lifecycle.webp)\n\n1. **Conversation** — Your user talks to your AI through the wrapped LLM client\n2. **Capture** — Memori intercepts and stores the raw conversation in your database\n3. **Augmentation** — Advanced Augmentation processes the conversation asynchronously\n4. **Extraction** — Facts, preferences, skills, rules, and events are identified\n5. **Storage** — Extracted memories are stored in your database with vector embeddings\n6. **Recall** — On the next LLM call, relevant memories are retrieved and injected into context\n"
  },
  {
    "path": "docs/memori-byodb/concepts/knowledge-graph.mdx",
    "content": "---\ntitle: Knowledge Graph\ndescription: How Memori automatically builds a knowledge graph from your AI conversations using semantic triples, stored in your own database where you can query it directly.\n---\n\n# Knowledge Graph\n\nMemori automatically builds a knowledge graph from your AI conversations. Every time Advanced Augmentation processes a conversation, it extracts structured relationships — semantic triples — and connects them into a graph. Because you own the database, you can query the knowledge graph directly using SQL.\n\n## How It Works\n\n1. **Conversation captured** — Your user talks to your AI through the Memori-wrapped LLM client\n2. **Augmentation processes** — Memori analyzes the conversation in the background\n3. **NER extraction** — Named-entity recognition identifies key entities and relationships\n4. **Triple creation** — Relationships are expressed as subject-predicate-object triples\n5. **Graph storage** — Triples are stored and deduplicated in your database\n6. **Recall ready** — The graph is available for semantic search on the next LLM call\n\n## Semantic Triples\n\nEvery fact in the knowledge graph is a semantic triple — a three-part statement: **[Subject]** **[Predicate]** **[Object]**.\n\n- \"Alice\" \"prefers\" \"dark mode\"\n- \"PostgreSQL\" \"is\" \"a relational database\"\n- \"The project\" \"uses\" \"FastAPI\"\n\n### Example Extraction\n\nFrom _\"My favorite database is PostgreSQL and I use it with FastAPI for our REST APIs. I've been using Python for about 8 years\"_:\n\n| Subject | Predicate         | Object               |\n| ------- | ----------------- | -------------------- |\n| user    | favorite_database | PostgreSQL           |\n| user    | uses              | FastAPI              |\n| user    | uses_for          | REST APIs            |\n| user    | uses_with         | PostgreSQL + FastAPI |\n| user    | experience_years  | Python (8 years)     |\n\nMemori automatically deduplicates triples — frequently mentioned facts get a higher mention count and updated timestamp.\n\n## Database Tables\n\n| Table                    | Purpose                                              |\n| ------------------------ | ---------------------------------------------------- |\n| `memori_subject`         | Stores unique subjects                               |\n| `memori_predicate`       | Stores unique predicates                             |\n| `memori_object`          | Stores unique objects                                |\n| `memori_knowledge_graph` | Links subjects, predicates, and objects into triples |\n| `memori_entity_fact`     | Stores facts with vector embeddings for recall       |\n\n## Querying\n\n### Via Recall API\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.attribution(entity_id=\"user_alice\", process_id=\"my_agent\")\n\nfacts = mem.recall(\"database preferences\", limit=5)\n\nfor fact in facts:\n    print(f\"Fact: {fact.content}\")\n    print(f\"Similarity: {fact.similarity:.4f}\")\n```\n\n### Via Direct SQL\n\nSince the knowledge graph lives in your database, you can query it directly for debugging, dashboards, or exploration.\n\n<CodeGroup title=\"Direct Database Queries\">\n\n```sql {{ title: 'View All Triples' }}\nSELECT\n    s.name AS subject,\n    p.name AS predicate,\n    o.name AS object\nFROM memori_knowledge_graph kg\nJOIN memori_subject s ON kg.subject_id = s.id\nJOIN memori_predicate p ON kg.predicate_id = p.id\nJOIN memori_object o ON kg.object_id = o.id;\n```\n\n```sql {{ title: 'Triples for an Entity' }}\nSELECT\n    s.name AS subject,\n    p.name AS predicate,\n    o.name AS object,\n    kg.mention_count,\n    kg.last_mentioned_at\nFROM memori_knowledge_graph kg\nJOIN memori_subject s ON kg.subject_id = s.id\nJOIN memori_predicate p ON kg.predicate_id = p.id\nJOIN memori_object o ON kg.object_id = o.id\nWHERE kg.entity_id = 'user_alice'\nORDER BY kg.mention_count DESC;\n```\n\n```sql {{ title: 'View Facts' }}\nSELECT content, created_at\nFROM memori_entity_fact\nWHERE entity_id = 'user_alice'\nORDER BY created_at DESC;\n```\n\n</CodeGroup>\n\n## Scope\n\n| Aspect         | Scope                                                           |\n| -------------- | --------------------------------------------------------------- |\n| **Triples**    | Per entity — shared across all processes                        |\n| **Visibility** | All processes for an entity can see and use the graph           |\n| **Growth**     | Conversations from any process contribute to the entity's graph |\n\nIf Alice tells your support bot about PostgreSQL, your code assistant also knows she uses PostgreSQL.\n"
  },
  {
    "path": "docs/memori-byodb/concepts/multi-user-support.mdx",
    "content": "---\ntitle: Multi-User Support\ndescription: How Memori isolates memories across users, applications, and sessions so each user gets a personalized experience — all in your own database.\n---\n\n# Multi-User Support\n\nMemori provides built-in multi-user and multi-process isolation through its attribution system. Each combination of entity, process, and session creates an isolated memory space — User A never sees User B's memories, and your support bot has different context than your sales bot.\n\n## Isolation Model\n\n![\"Memori - Multi User Support\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-multi-user-support.webp)\n\n## What's Shared vs Isolated\n\n| Data                | Scope                                    |\n| ------------------- | ---------------------------------------- |\n| **Facts**           | Per entity — shared across all processes |\n| **Preferences**     | Per entity                               |\n| **Skills**          | Per entity                               |\n| **Attributes**      | Per process                              |\n| **Conversations**   | Per entity + process + session           |\n| **Sessions**        | Per entity + process                     |\n| **Knowledge Graph** | Per entity                               |\n\n## Examples\n\n<CodeGroup title=\"Multi-User Patterns\">\n\n```python {{ title: 'Per-User Isolation' }}\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\n\n# User A's conversations\nmem.attribution(entity_id=\"user_alice\", process_id=\"support_bot\")\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"I prefer dark mode\"}]\n)\n\n# User B's conversations — completely isolated\nmem.attribution(entity_id=\"user_bob\", process_id=\"support_bot\")\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"What are my preferences?\"}]\n)\n# Bob will NOT see Alice's preferences\n```\n\n```python {{ title: 'Multi-Process' }}\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\n\n# Same user, different processes\nmem.attribution(entity_id=\"user_alice\", process_id=\"support_bot\")\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"I use PostgreSQL for my databases\"}\n    ]\n)\n\n# Switch to a different process for the same user\nmem.attribution(entity_id=\"user_alice\", process_id=\"sales_bot\")\n# The sales bot can recall Alice's facts (like \"uses PostgreSQL\")\n# because facts are shared across processes for the same entity.\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"What databases do I use?\"}\n    ]\n)\n```\n\n```python {{ title: 'Session Management' }}\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_alice\", process_id=\"support_bot\")\n\n# Get the current session ID\ncurrent_session = mem.config.session_id\n\n# Start a new conversation group\nmem.new_session()\n\n# Or restore a previous session\nmem.set_session(current_session)\n```\n\n</CodeGroup>\n\n## Common Patterns\n\n### Web Application\n\nSet the entity ID from the authenticated user's session. Works with Flask, FastAPI, Django, or any framework.\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\ndef handle_chat(user_id: str, message: str):\n    client = OpenAI()\n    mem = Memori(conn=SessionLocal).llm.register(client)\n    mem.attribution(entity_id=user_id, process_id=\"web_assistant\")\n\n    response = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": message}]\n    )\n    return response.choices[0].message.content\n```\n\n### Multi-Agent System\n\nGive each agent a unique process ID. Facts are shared across agents for the same user, but each maintains its own conversation history.\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\ndef create_agent(user_id: str, agent_name: str):\n    client = OpenAI()\n    mem = Memori(conn=SessionLocal).llm.register(client)\n    mem.attribution(entity_id=user_id, process_id=agent_name)\n    return client\n\n# Three agents, one user, shared facts\nsupport = create_agent(\"user_alice\", \"support_agent\")\nsales = create_agent(\"user_alice\", \"sales_agent\")\nonboard = create_agent(\"user_alice\", \"onboarding_agent\")\n```\n"
  },
  {
    "path": "docs/memori-byodb/contribute/development-setup.mdx",
    "content": "---\ntitle: Development Setup\ndescription: Set up your local development environment for contributing to the Memori open-source project.\n---\n\n# Development Setup\n\nThis guide walks you through setting up a local development environment for working on the Memori codebase. Follow these steps to clone the repository, install dependencies, and run the test suite.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Prerequisites\n\nBefore you begin, make sure you have the following installed on your system:\n\n- **Python 3.10 or higher** — Memori requires Python 3.10+. Check your version with `python --version`.\n- **git** — For cloning the repository and managing branches.\n- **pip** — Python's package manager (usually included with Python).\n\n## Clone the Repository\n\nStart by forking the repository on GitHub, then clone your fork locally:\n\n```bash\ngit clone https://github.com/YOUR_USERNAME/Memori.git\ncd Memori\n```\n\nIf you plan to submit a pull request, make sure to keep your fork synced with the upstream repository:\n\n```bash\ngit remote add upstream https://github.com/MemoriLabs/Memori.git\ngit fetch upstream\n```\n\n## Install Dependencies\n\nIt is recommended to use a virtual environment to avoid conflicts with other Python projects:\n\n<CodeGroup title=\"Virtual Environment Setup\">\n\n```bash {{ title: 'venv (Built-in)' }}\npython -m venv .venv\nsource .venv/bin/activate    # macOS/Linux\n# .venv\\Scripts\\activate     # Windows\n```\n\n```bash {{ title: 'conda' }}\nconda create -n memori python=3.12\nconda activate memori\n```\n\n</CodeGroup>\n\nOnce your virtual environment is active, install Memori with development dependencies:\n\n```bash\npip install -e \".[dev]\"\n```\n\nThis installs Memori in editable mode (`-e`) so that changes you make to the source code are immediately reflected without reinstalling. The `[dev]` extra includes testing and linting tools.\n\nIf the project uses a `requirements-dev.txt` file instead, install with:\n\n```bash\npip install -e .\npip install -r requirements-dev.txt\n```\n\n## Running Tests\n\nMemori uses pytest for its test suite. Run all tests from the project root:\n\n```bash\npytest\n```\n\nTo run tests with verbose output:\n\n```bash\npytest -v\n```\n\nTo run a specific test file or test function:\n\n```bash\n# Run all tests in a specific file\npytest tests/test_openai.py\n\n# Run a specific test function\npytest tests/test_openai.py::test_sync_completion\n```\n\nTo run tests with coverage reporting:\n\n```bash\npytest --cov=memori --cov-report=term-missing\n```\n\n## Local Development Workflow\n\nHere is a typical workflow for making changes to Memori:\n\n1. **Create a branch** for your feature or fix:\n\n   ```bash\n   git checkout -b feat/my-new-feature\n   ```\n\n2. **Make your changes** — Edit the source code in the `memori/` directory.\n\n3. **Write tests** — Add or update tests in the `tests/` directory to cover your changes.\n\n4. **Run the tests** to make sure nothing is broken:\n\n   ```bash\n   pytest\n   ```\n\n5. **Check code style** — The project uses Ruff for linting and formatting:\n\n   ```bash\n   # Lint\n   ruff check .\n\n   # Format\n   ruff format --check .\n   ```\n\n6. **Commit your changes** with a descriptive message:\n\n   ```bash\n   git add .\n   git commit -m \"feat: add support for Redis storage backend\"\n   ```\n\n7. **Push to your fork** and open a pull request:\n   ```bash\n   git push origin feat/my-new-feature\n   ```\n\n## Project Structure\n\nHere is a high-level overview of the Memori repository structure:\n\n| Directory   | Purpose                         |\n| ----------- | ------------------------------- |\n| `memori/`   | Core library source code        |\n| `tests/`    | Test suite (pytest)             |\n| `docs/`     | Documentation source files      |\n| `examples/` | Example scripts and usage demos |\n\nThe core integration logic for each LLM provider typically lives in separate modules within the `memori/` directory. When adding support for a new provider, look at existing integrations (such as OpenAI or Anthropic) as templates.\n"
  },
  {
    "path": "docs/memori-byodb/contribute/overview.mdx",
    "content": "---\ntitle: Contributing to Memori\ndescription: How to contribute to the Memori open-source project — code, documentation, issues, and discussions.\n---\n\n# Contributing to Memori\n\nThank you for your interest in contributing to Memori. Whether you are fixing a bug, adding a feature, improving documentation, or just asking a question, every contribution helps make Memori better for everyone.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\nThe Memori source code lives on GitHub at [github.com/MemoriLabs/Memori](https://github.com/MemoriLabs/Memori).\n\n## Ways to Contribute\n\nThere are many ways to contribute to Memori, and not all of them involve writing code:\n\n### Report Bugs\n\nFound a bug? Open an issue on [GitHub](https://github.com/MemoriLabs/Memori/issues) with:\n\n- A clear title describing the problem\n- Steps to reproduce the issue\n- What you expected to happen versus what actually happened\n- Your Python version, OS, and Memori version (`pip show memori`)\n\n### Suggest Features\n\nHave an idea for a new feature or improvement? Open a [GitHub Discussion](https://github.com/MemoriLabs/Memori/discussions) or an issue tagged with `enhancement`. Describe the use case and why it would be valuable.\n\n### Improve Documentation\n\nDocumentation improvements are always welcome. This includes:\n\n- Fixing typos or unclear explanations\n- Adding new examples or use cases\n- Improving the guides for specific LLM providers or databases\n- Translating documentation\n\n### Contribute Code\n\nReady to write some code? Here is the process:\n\n1. **Fork the repository** — Create your own fork of [MemoriLabs/Memori](https://github.com/MemoriLabs/Memori) on GitHub\n2. **Create a branch** — Branch off `main` with a descriptive name (for example, `fix/streaming-bug` or `feat/redis-support`)\n3. **Make your changes** — Write your code, add tests, and update documentation if needed\n4. **Run the tests** — Make sure all existing tests pass and your new tests cover the changes\n5. **Submit a pull request** — Open a PR back to the `main` branch with a clear description of what you changed and why\n\nSee the [Development Setup](/docs/memori-byodb/contribute/development-setup) guide for instructions on setting up your local environment.\n\n## Pull Request Guidelines\n\nWhen submitting a pull request:\n\n- **Keep PRs focused** — One feature or fix per PR. This makes review faster and easier.\n- **Write descriptive commit messages** — Explain what changed and why, not just what files were modified.\n- **Include tests** — New features should include tests. Bug fixes should include a test that reproduces the bug.\n- **Update documentation** — If your change affects the public API or user-facing behavior, update the relevant docs.\n- **Follow existing patterns** — Look at how existing code is structured and follow the same conventions.\n\n## Code of Conduct\n\nWe are committed to providing a welcoming and inclusive experience for everyone. All contributors are expected to be respectful and constructive in their interactions. Harassment, discrimination, and toxic behavior are not tolerated.\n\nPlease review the project's Code of Conduct on the [GitHub repository](https://github.com/MemoriLabs/Memori) before contributing.\n\n## Community\n\nJoin the Memori community:\n\n- **GitHub Issues** — For bug reports and feature requests: [github.com/MemoriLabs/Memori/issues](https://github.com/MemoriLabs/Memori/issues)\n- **GitHub Discussions** — For questions, ideas, and general conversation: [github.com/MemoriLabs/Memori/discussions](https://github.com/MemoriLabs/Memori/discussions)\n"
  },
  {
    "path": "docs/memori-byodb/dashboard/api-keys.mdx",
    "content": "---\ntitle: API Keys\ndescription: Create and manage Memori API keys from the dashboard.\n---\n\n# API Keys\n\nUse the **API keys** page in [app.memorilabs.ai](https://app.memorilabs.ai)\nto create and manage keys for your organization.\n\n## Overview\n\nThe table includes:\n\n- **Name**\n- **Key** (masked)\n- **Created at**\n- **Last used**\n\nIf no keys are connected yet, the table shows **No connected APIs.**\n\n## Create a New API Key\n\n1. Open **API keys** in the dashboard sidebar.\n2. Click **Create API key**.\n3. In **New API key**, enter a value in **Name**.\n4. Click **Create key**.\n\n![API keys page with empty table state and New API key modal](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/new-api-key-modal.webp)\n\n## Copy the Key (Shown Once)\n\nAfter creating a key, the **Your API key** modal appears with the full key\nvalue and a **Copy** button.\n\n<Admonition type=\"warning\" title=\"Shown Once\">\n  The full key value is shown only once. Copy it and store it securely before\n  clicking **Continue**.\n</Admonition>\n\n1. Click **Copy**.\n2. Click **Continue** to return to the API keys table.\n\n![API keys page with table row and Your API key modal for one-time copy](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/your-api-key-modal.webp)\n\n## Security Best Practices\n\n<Admonition type=\"danger\" title=\"Never Commit API Keys\">\n  Never commit API keys to version control. Treat API keys like passwords.\n</Admonition>\n\n- Store keys in environment variables or a secure secrets manager\n- Rotate keys immediately if you suspect exposure\n- Use separate keys for development, staging, and production\n"
  },
  {
    "path": "docs/memori-byodb/databases/cockroachdb.mdx",
    "content": "---\ntitle: CockroachDB\ndescription: Set up Memori with CockroachDB — distributed SQL database with PostgreSQL compatibility, automatic scaling, and strong consistency.\n---\n\n# CockroachDB\n\nCockroachDB is a distributed SQL database that uses the PostgreSQL wire protocol. It works with Memori through the same `psycopg` driver as PostgreSQL — no special adapter needed.\n\n## Install\n\n```bash\npip install memori psycopg2-binary\n```\n\n## Quick Start\n\n<CodeGroup title=\"CockroachDB Connection\">\n\n```python {{ title: 'Basic' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"cockroachdb+psycopg2://user:password@localhost:26257/memori_db\",\n    pool_pre_ping=True\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'CockroachDB Cloud' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"cockroachdb+psycopg2://user:password@free-tier.gcp-us-central1.cockroachlabs.cloud:26257/memori_db\"\n    \"?sslmode=verify-full\",\n    pool_pre_ping=True,\n    pool_size=10,\n    max_overflow=20\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n</CodeGroup>\n\n## Connection Strings\n\n| Environment           | Connection String                                                                                  |\n| --------------------- | -------------------------------------------------------------------------------------------------- |\n| **Local**             | `cockroachdb+psycopg2://root@localhost:26257/memori_db`                                            |\n| **With Auth**         | `cockroachdb+psycopg2://user:pass@host:26257/memori_db`                                            |\n| **CockroachDB Cloud** | `cockroachdb+psycopg2://user:pass@cluster.cockroachlabs.cloud:26257/memori_db?sslmode=verify-full` |\n| **PostgreSQL scheme** | `postgresql+psycopg2://user:pass@host:26257/memori_db`                                             |\n\n## Complete Example\n\n```python\nimport os\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\n    os.getenv(\"COCKROACHDB_URL\"),\n    pool_pre_ping=True,\n    pool_size=10,\n    max_overflow=20,\n    pool_recycle=300\n)\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\nmem.config.storage.build()\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"I manage a distributed systems team.\"}]\n)\nprint(response.choices[0].message.content)\n\nmem.augmentation.wait()\nfacts = mem.recall(\"job role\")\nprint(facts)\n```\n"
  },
  {
    "path": "docs/memori-byodb/databases/mongodb.mdx",
    "content": "---\ntitle: MongoDB\ndescription: Set up Memori with MongoDB — document-oriented AI memory using PyMongo.\n---\n\n# MongoDB\n\nMongoDB stores data as flexible JSON-like documents. Memori integrates through PyMongo, giving you a NoSQL option for AI agent memory.\n\n## Install\n\n```bash\npip install memori pymongo\n```\n\n## Quick Start\n\n```python\nfrom memori import Memori\nfrom pymongo import MongoClient\n\nclient = MongoClient(\"mongodb://localhost:27017\")\n\ndef get_db():\n    return client[\"memori_db\"]\n\nmem = Memori(conn=get_db)\nmem.config.storage.build()\n```\n\n## Connection Strings\n\n| Environment     | Connection String                                    |\n| --------------- | ---------------------------------------------------- |\n| **Local**       | `mongodb://localhost:27017`                          |\n| **With Auth**   | `mongodb://user:password@localhost:27017`            |\n| **Atlas (SRV)** | `mongodb+srv://user:password@cluster.mongodb.net`    |\n| **Replica Set** | `mongodb://host1:27017,host2:27017/?replicaSet=myRS` |\n\n## Complete Example\n\n```python\nimport os\nfrom pymongo import MongoClient\nfrom memori import Memori\nfrom openai import OpenAI\n\nmongo_client = MongoClient(\"mongodb://localhost:27017\")\n\ndef get_db():\n    return mongo_client[\"memori_db\"]\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori(conn=get_db).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\nmem.config.storage.build()\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"I love hiking in the Rockies.\"}]\n)\nprint(response.choices[0].message.content)\n\nmem.augmentation.wait()\nfacts = mem.recall(\"hobbies and outdoor activities\")\nprint(facts)\n```\n\n## MongoDB Atlas\n\n```python\nfrom pymongo import MongoClient\n\nmongo_client = MongoClient(\n    \"mongodb+srv://user:password@cluster.mongodb.net\"\n    \"/?retryWrites=true&w=majority\"\n)\n\ndef get_db():\n    return mongo_client[\"memori_db\"]\n```\n\n## Connection Pooling\n\nPyMongo manages its own connection pool:\n\n```python\nmongo_client = MongoClient(\n    \"mongodb://localhost:27017\",\n    maxPoolSize=50,\n    minPoolSize=5,\n    maxIdleTimeMS=30000\n)\n```\n"
  },
  {
    "path": "docs/memori-byodb/databases/mysql.mdx",
    "content": "---\ntitle: MySQL\ndescription: Set up Memori with MySQL — use your existing MySQL infrastructure for AI agent memory.\n---\n\n# MySQL\n\nIf your infrastructure already runs MySQL, you can use it directly with Memori without setting up a separate database.\n\n## Install\n\n<CodeGroup title=\"Install MySQL Driver\">\n\n```bash {{ title: 'PyMySQL (Pure Python)' }}\npip install memori pymysql\n```\n\n```bash {{ title: 'mysqlclient (C Extension)' }}\npip install memori mysqlclient\n```\n\n</CodeGroup>\n\n## Quick Start\n\n<CodeGroup title=\"MySQL Connection\">\n\n```python {{ title: 'PyMySQL' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"mysql+pymysql://user:password@localhost:3306/memori_db\",\n    pool_pre_ping=True\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'mysqlclient' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"mysql+mysqldb://user:password@localhost:3306/memori_db\",\n    pool_pre_ping=True\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n</CodeGroup>\n\n## Connection Strings\n\n| Driver           | Connection String                                                     |\n| ---------------- | --------------------------------------------------------------------- |\n| **PyMySQL**      | `mysql+pymysql://user:pass@host:3306/database`                        |\n| **mysqlclient**  | `mysql+mysqldb://user:pass@host:3306/database`                        |\n| **With charset** | `mysql+pymysql://user:pass@host:3306/database?charset=utf8mb4`        |\n| **With SSL**     | `mysql+pymysql://user:pass@host:3306/database?ssl_ca=/path/to/ca.pem` |\n\n## Complete Example\n\n```python\nimport os\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\n    \"mysql+pymysql://user:password@localhost:3306/memori_db\"\n    \"?charset=utf8mb4\",\n    pool_pre_ping=True,\n    pool_size=5,\n    max_overflow=10,\n    pool_recycle=1800\n)\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\nmem.config.storage.build()\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"I work at Acme Corp as a designer.\"}]\n)\nprint(response.choices[0].message.content)\n\nmem.augmentation.wait()\nfacts = mem.recall(\"workplace\")\nprint(facts)\n```\n"
  },
  {
    "path": "docs/memori-byodb/databases/oracle.mdx",
    "content": "---\ntitle: Oracle\ndescription: Set up Memori with Oracle Database — enterprise-grade AI memory for Oracle infrastructure.\n---\n\n# Oracle\n\nOracle Database integrates with Memori through SQLAlchemy, so you can add AI agent memory to existing Oracle infrastructure.\n\n## Install\n\n<CodeGroup title=\"Install Oracle Driver\">\n\n```bash {{ title: 'oracledb (Recommended)' }}\npip install memori oracledb\n```\n\n```bash {{ title: 'cx_Oracle (Legacy)' }}\npip install memori cx_Oracle\n```\n\n</CodeGroup>\n\n## Quick Start\n\n<CodeGroup title=\"Oracle Connection\">\n\n```python {{ title: 'oracledb (Modern)' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"oracle+oracledb://user:password@hostname:1521/service_name\"\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'cx_Oracle (Legacy)' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"oracle+cx_oracle://user:password@hostname:1521/service_name\"\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n</CodeGroup>\n\n## Connection Strings\n\n| Format           | Connection String                                                       |\n| ---------------- | ----------------------------------------------------------------------- |\n| **oracledb**     | `oracle+oracledb://user:pass@host:1521/service_name`                    |\n| **cx_Oracle**    | `oracle+cx_oracle://user:pass@host:1521/service_name`                   |\n| **TNS Name**     | `oracle+oracledb://user:pass@tns_alias`                                 |\n| **Oracle Cloud** | `oracle+oracledb://user:pass@host:1522/service?ssl_server_dn_match=yes` |\n\n## Complete Example\n\n```python\nimport os\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\n    \"oracle+oracledb://memori_user:password@oracle-host:1521/ORCL\",\n    pool_pre_ping=True,\n    pool_size=5,\n    max_overflow=10,\n    pool_recycle=300\n)\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\nmem.config.storage.build()\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"Our quarterly review is on March 15.\"}]\n)\nprint(response.choices[0].message.content)\n\nmem.augmentation.wait()\nfacts = mem.recall(\"quarterly review date\")\nprint(facts)\n```\n\n## Oracle Cloud\n\nFor Oracle Autonomous Database, use the wallet-based connection:\n\n```python\nimport oracledb\n\noracledb.init_oracle_client(config_dir=\"/path/to/wallet\")\n\nengine = create_engine(\n    \"oracle+oracledb://ADMIN:password@db_alias\"\n    \"?config_dir=/path/to/wallet\"\n    \"&wallet_location=/path/to/wallet\"\n    \"&wallet_password=wallet_password\"\n)\n```\n"
  },
  {
    "path": "docs/memori-byodb/databases/overview.mdx",
    "content": "---\ntitle: Databases Overview\ndescription: Memori supports SQLite, PostgreSQL, MySQL, MariaDB, Oracle, MongoDB, CockroachDB, and OceanBase, plus providers like Neon, Supabase, and AWS RDS/Aurora.\n---\n\n# Databases Overview\n\nMemori is database-agnostic. With the BYODB (Bring Your Own Database) approach, your data stays on your infrastructure — you choose the database, you own the data.\n\n<Note>\n  Want a zero-setup option? Try Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Supported Databases\n\n| Database                                                    | Best For                           | Driver                     | Connection String                                   |\n| ----------------------------------------------------------- | ---------------------------------- | -------------------------- | --------------------------------------------------- |\n| **[SQLite](/docs/memori-byodb/databases/sqlite)**           | Development, prototyping           | Built-in (`sqlite3`)       | `sqlite:///memori.db`                               |\n| **[PostgreSQL](/docs/memori-byodb/databases/postgres)**     | Production, high concurrency       | `psycopg2-binary`          | `postgresql://user:pass@host/db`                    |\n| **[MySQL](/docs/memori-byodb/databases/mysql)**             | Existing MySQL infrastructure      | `pymysql` or `mysqlclient` | `mysql+pymysql://user:pass@host/db`                 |\n| **MariaDB**                                                 | MySQL-compatible alternative       | `pymysql` or `mysqlclient` | `mysql+pymysql://user:pass@host/db`                 |\n| **[Oracle](/docs/memori-byodb/databases/oracle)**           | Enterprise environments            | `oracledb` or `cx_Oracle`  | `oracle+oracledb://user:pass@host/service`          |\n| **[MongoDB](/docs/memori-byodb/databases/mongodb)**         | Document-oriented, flexible schema | `pymongo`                  | `mongodb://host:27017`                              |\n| **[CockroachDB](/docs/memori-byodb/databases/cockroachdb)** | Distributed SQL, global scale      | `psycopg2-binary`          | `cockroachdb+psycopg2://user:pass@host/db`          |\n| **OceanBase**                                               | Distributed HTAP workloads         | `pyobvector`               | `mysql+pyobvector://user:pass@host:2881/db`         |\n| **Neon**                                                    | Serverless PostgreSQL              | `psycopg2-binary`          | `postgresql://user:pass@ep-xxx.neon.tech/db`        |\n| **Supabase**                                                | Hosted PostgreSQL with extras      | `psycopg2-binary`          | `postgresql://user:pass@db.xxx.supabase.co:5432/db` |\n\nMariaDB uses the same drivers and connection strings as MySQL. Neon and Supabase are PostgreSQL-compatible — see the [PostgreSQL](/docs/memori-byodb/databases/postgres) page for full setup details.\n\n## Managed Providers\n\nMemori also works with hosted services that expose standard PostgreSQL/MySQL endpoints:\n\n| Provider       | Engine Compatibility | Typical Host Pattern  |\n| -------------- | -------------------- | --------------------- |\n| **Neon**       | PostgreSQL           | `*.neon.tech`         |\n| **Supabase**   | PostgreSQL           | `*.supabase.co`       |\n| **AWS RDS**    | PostgreSQL/MySQL     | `*.rds.amazonaws.com` |\n| **AWS Aurora** | PostgreSQL/MySQL     | `*.rds.amazonaws.com` |\n\n## Quick Start Code\n\n<CodeGroup title=\"Database Setup\">\n\n```python {{ title: 'SQLite (DB API 2.0)' }}\nimport sqlite3\nfrom memori import Memori\n\ndef get_connection():\n    return sqlite3.connect(\"memori.db\")\n\nmem = Memori(conn=get_connection)\nmem.config.storage.build()\n```\n\n```python {{ title: 'PostgreSQL' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"postgresql+psycopg://user:password@localhost:5432/memori_db\",\n    pool_pre_ping=True\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'MySQL' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"mysql+pymysql://user:password@localhost:3306/memori_db\",\n    pool_pre_ping=True\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'Oracle' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"oracle+oracledb://user:password@localhost:1521/service_name\"\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'MongoDB' }}\nfrom memori import Memori\nfrom pymongo import MongoClient\n\nclient = MongoClient(\"mongodb://localhost:27017\")\n\ndef get_db():\n    return client[\"memori_db\"]\n\nmem = Memori(conn=get_db)\nmem.config.storage.build()\n```\n\n```python {{ title: 'CockroachDB' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"cockroachdb+psycopg2://user:password@localhost:26257/memori_db\",\n    pool_pre_ping=True\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'OceanBase' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"mysql+pyobvector://user:password@localhost:2881/memori_db\"\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n</CodeGroup>\n\n## Connection Methods\n\nMemori supports four connection methods depending on your stack:\n\n| Method         | Description                               | Use Case                                  |\n| -------------- | ----------------------------------------- | ----------------------------------------- |\n| **SQLAlchemy** | Industry-standard ORM with `sessionmaker` | Production applications, connection pools |\n| **DB API 2.0** | Direct Python database drivers (PEP 249)  | Lightweight, minimal dependencies         |\n| **Django ORM** | Native Django ORM integration             | Django applications                       |\n| **MongoDB**    | Function returning a database object      | Document databases via `pymongo`          |\n\nMemori accepts a `conn` parameter — a callable that returns a new connection each time it is called.\n\n- **SQLAlchemy databases** (SQLite, PostgreSQL, MySQL, MariaDB, Oracle, CockroachDB, OceanBase, plus providers like Neon/Supabase/RDS/Aurora): pass a `sessionmaker`\n- **DB API 2.0**: pass a function that returns a PEP 249 connection object\n- **Django ORM**: use Django's native ORM integration\n- **MongoDB**: pass a function returning a database instance\n"
  },
  {
    "path": "docs/memori-byodb/databases/postgres.mdx",
    "content": "---\ntitle: PostgreSQL\ndescription: Set up Memori with PostgreSQL — recommended for production with connection pooling and high concurrency.\n---\n\n# PostgreSQL\n\nPostgreSQL is the recommended database for production Memori deployments. Full concurrent write support, connection pooling, and cloud-ready.\n\n## Install\n\n```bash\npip install memori psycopg2-binary\n```\n\n## Quick Start\n\n<CodeGroup title=\"PostgreSQL Connection\">\n\n```python {{ title: 'Basic' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"postgresql+psycopg://user:password@localhost:5432/memori_db\",\n    pool_pre_ping=True\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'With Connection Pool' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"postgresql+psycopg://user:password@localhost:5432/memori_db\",\n    pool_pre_ping=True,\n    pool_size=10,\n    max_overflow=20,\n    pool_recycle=300\n)\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n</CodeGroup>\n\n## Complete Example\n\n```python\nimport os\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\n    os.getenv(\"DATABASE_URL\"),\n    pool_pre_ping=True,\n    pool_size=10,\n    max_overflow=20,\n    pool_recycle=300\n)\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\nmem.config.storage.build()\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"I'm a senior engineer at Google.\"}]\n)\nprint(response.choices[0].message.content)\n\nmem.augmentation.wait()\nfacts = mem.recall(\"job title and company\")\nprint(facts)\n```\n\n## Cloud Providers\n\n| Provider             | Connection Format                                            |\n| -------------------- | ------------------------------------------------------------ |\n| **Neon**             | `postgresql+psycopg://...@*.neon.tech/...`                   |\n| **Supabase**         | `postgresql+psycopg://...@*.supabase.co/...`                 |\n| **AWS RDS**          | `postgresql+psycopg://...@*.rds.amazonaws.com/...`           |\n| **AWS Aurora**       | `postgresql+psycopg://...@*.rds.amazonaws.com/...`           |\n| **Google Cloud SQL** | `postgresql+psycopg://...@*.cloudsql/...`                    |\n| **Azure Database**   | `postgresql+psycopg://...@*.postgres.database.azure.com/...` |\n\n## SSL Connections\n\nFor cloud-hosted PostgreSQL, use SSL:\n\n```python\nengine = create_engine(\n    \"postgresql+psycopg://user:password@host:5432/memori_db\"\n    \"?sslmode=require\",\n    pool_pre_ping=True\n)\n```\n\n| Mode          | Description                               |\n| ------------- | ----------------------------------------- |\n| `require`     | SSL required, no certificate verification |\n| `verify-ca`   | SSL + verify server certificate           |\n| `verify-full` | SSL + verify certificate + hostname       |\n"
  },
  {
    "path": "docs/memori-byodb/databases/sqlite.mdx",
    "content": "---\ntitle: SQLite\ndescription: Set up Memori with SQLite — zero install, file-based storage perfect for development and prototyping.\n---\n\n# SQLite\n\nSQLite requires no server setup and stores data in a local file. It is built into Python, so it works out of the box.\n\n## Install\n\nNo extra packages needed — SQLite is built into Python.\n\n```bash\npip install memori openai\n```\n\n## Quick Start\n\n<CodeGroup title=\"SQLite Connection\">\n\n```python {{ title: 'SQLAlchemy (Recommended)' }}\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\nmem.config.storage.build()\n```\n\n```python {{ title: 'DB API 2.0' }}\nimport sqlite3\nfrom memori import Memori\n\ndef get_connection():\n    return sqlite3.connect(\"memori.db\")\n\nmem = Memori(conn=get_connection)\nmem.config.storage.build()\n```\n\n</CodeGroup>\n\n## Connection Strings\n\n| Path Type     | Connection String                     | Description                  |\n| ------------- | ------------------------------------- | ---------------------------- |\n| **Relative**  | `sqlite:///memori.db`                 | File in current directory    |\n| **Absolute**  | `sqlite:////home/user/data/memori.db` | Absolute path (four slashes) |\n| **In-Memory** | `sqlite:///:memory:`                  | Temporary, lost on exit      |\n\n## Complete Example\n\n```python\nimport os\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\nmem.config.storage.build()\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"My favorite language is Python.\"}]\n)\nprint(response.choices[0].message.content)\n\nmem.augmentation.wait()\nfacts = mem.recall(\"favorite programming language\")\nprint(facts)\n```\n\n## Multi-Threading\n\nFor web servers or multi-threaded apps, disable the same-thread check:\n\n```python\nengine = create_engine(\n    \"sqlite:///memori.db\",\n    connect_args={\"check_same_thread\": False}\n)\n```\n"
  },
  {
    "path": "docs/memori-byodb/getting-started/installation.mdx",
    "content": "---\ntitle: Installation\ndescription: Install Memori and set up your database for the Memori BYODB.\n---\n\n# Installation\n\nGet Memori installed and connected to your own database in a few steps.\n\n<Note>\n  Memori BYODB supports the Python SDK only. For TypeScript, use\n  [Memori Cloud](/docs/memori-cloud).\n</Note>\n\n## Install Memori\n\n<CodeGroup title=\"Install Memori\">\n\n```bash {{ title: 'pip' }}\npip install memori\n```\n\n```bash {{ title: 'poetry' }}\npoetry add memori\n```\n\n```bash {{ title: 'uv' }}\nuv add memori\n```\n\n</CodeGroup>\n\n## Install Your Database Driver\n\nMemori supports SQLite, PostgreSQL, MySQL, MariaDB, Oracle, MongoDB, CockroachDB, and OceanBase. Managed services like Neon, Supabase, and AWS RDS/Aurora work through their compatible PostgreSQL/MySQL engines. Install the driver for your preferred database:\n\n<CodeGroup title=\"Database Drivers\">\n\n```bash {{ title: 'SQLite (built-in)' }}\n# No extra install needed!\n# SQLite support is included with Python.\n```\n\n```bash {{ title: 'PostgreSQL' }}\npip install psycopg2-binary\n# Or for async: pip install asyncpg\n```\n\n```bash {{ title: 'MySQL' }}\npip install pymysql\n# Or: pip install mysqlclient\n```\n\n```bash {{ title: 'MariaDB' }}\npip install pymysql\n# Or: pip install mysqlclient\n```\n\n```bash {{ title: 'Oracle' }}\npip install oracledb\n```\n\n```bash {{ title: 'MongoDB' }}\npip install pymongo\n```\n\n```bash {{ title: 'CockroachDB' }}\npip install psycopg2-binary\n```\n\n```bash {{ title: 'OceanBase' }}\npip install pyobvector\n```\n\n</CodeGroup>\n\nNeon, Supabase, and AWS RDS/Aurora use standard PostgreSQL drivers (`psycopg2-binary` or `psycopg`).\n\n## Connection Patterns\n\n| Pattern    | What to pass to `conn`                          | Works With                                                            |\n| ---------- | ----------------------------------------------- | --------------------------------------------------------------------- |\n| SQLAlchemy | `sessionmaker`                                  | SQLite, PostgreSQL, MySQL, MariaDB, Oracle, CockroachDB, OceanBase    |\n| DB API 2.0 | Function that returns a PEP 249 connection      | SQLite and SQL drivers (`sqlite3`, `psycopg2`, `pymysql`, `oracledb`) |\n| Django ORM | Django connection callable                      | Django applications                                                   |\n| MongoDB    | Function that returns a MongoDB database object | MongoDB via `pymongo`                                                 |\n\n<CodeGroup title=\"Database Setup\">\n\n```python {{ title: 'SQLite (DB API 2.0)' }}\nimport sqlite3\n\ndef get_connection():\n    return sqlite3.connect(\"memori.db\")\n\nfrom memori import Memori\nmem = Memori(conn=get_connection)\n```\n\n```python {{ title: 'PostgreSQL (SQLAlchemy)' }}\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"postgresql+psycopg2://user:password@localhost:5432/mydb\"\n)\nSessionLocal = sessionmaker(bind=engine)\n\nfrom memori import Memori\nmem = Memori(conn=SessionLocal)\n```\n\n```python {{ title: 'PostgreSQL (DB API 2.0)' }}\nimport psycopg2\nfrom memori import Memori\n\ndef get_connection():\n    return psycopg2.connect(\n        dbname=\"mydb\",\n        user=\"user\",\n        password=\"password\",\n        host=\"localhost\",\n        port=5432,\n    )\n\nmem = Memori(conn=get_connection)\n```\n\n```python {{ title: 'MySQL / MariaDB (SQLAlchemy)' }}\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\n    \"mysql+pymysql://user:password@localhost:3306/mydb\"\n)\nSessionLocal = sessionmaker(bind=engine)\n\nfrom memori import Memori\nmem = Memori(conn=SessionLocal)\n```\n\n```python {{ title: 'MongoDB' }}\nfrom pymongo import MongoClient\nfrom memori import Memori\n\nclient = MongoClient(\"mongodb://localhost:27017\")\n\ndef get_db():\n    return client[\"memori_db\"]\n\nmem = Memori(conn=get_db)\n```\n\n</CodeGroup>\n\n## Create the Schema\n\nAfter setting up your connection, run `build()` once to create the Memori tables in your database. This only needs to be done the first time, or when you upgrade Memori.\n\n```python\nimport sqlite3\nfrom memori import Memori\n\ndef get_connection():\n    return sqlite3.connect(\"memori.db\")\n\nmem = Memori(conn=get_connection)\nmem.config.storage.build()  # Creates all required tables\n```\n\n## Install Your LLM Provider\n\nInstall the SDK for your preferred LLM provider:\n\n<CodeGroup title=\"LLM Provider SDKs\">\n\n```bash {{ title: 'OpenAI' }}\npip install openai\n```\n\n```bash {{ title: 'Anthropic' }}\npip install anthropic\n```\n\n```bash {{ title: 'Google Gemini' }}\npip install google-genai\n```\n\n```bash {{ title: 'LangChain' }}\npip install langchain-openai\n```\n\n</CodeGroup>\n\n## Set Up Your LLM Provider Key\n\nYou will need an API key for your LLM provider:\n\n```bash\n# OpenAI\nexport OPENAI_API_KEY=\"your-openai-key\"\n\n# Anthropic\nexport ANTHROPIC_API_KEY=\"your-anthropic-key\"\n\n# Google Gemini\nexport GOOGLE_API_KEY=\"your-google-key\"\n```\n\n## Set Up Your Memori API Key (Optional)\n\nA Memori API key unlocks higher augmentation quotas (5,000/month vs 100 without a key). You can sign up directly from the CLI:\n\n```bash\npython -m memori sign-up your-email@example.com\n```\n\nThen set the key as an environment variable:\n\n```bash\nexport MEMORI_API_KEY=\"your-api-key-here\"\n```\n\nOr add it to a `.env` file in your project root:\n\n```\nMEMORI_API_KEY=your-api-key-here\n```\n\nCheck your current quota anytime:\n\n```bash\npython -m memori quota\n```\n\n## Pre-download the Embedding Model\n\nMemori uses a local embedding model for semantic search. On first run, it downloads the model automatically, which can take a moment. To pre-download it:\n\n```bash\npython -m memori setup\n```\n\n## Verify Installation\n\nRun `pip show memori` in your terminal to confirm the package is installed.\n"
  },
  {
    "path": "docs/memori-byodb/getting-started/python-quickstart.mdx",
    "content": "---\ntitle: Python SDK Quickstart\ndescription: Get started with Memori BYODB in under 3 minutes using SQLite and OpenAI.\n---\n\n# Python SDK Quickstart\n\nGet started with Memori in under 3 minutes. Since Memori BYODB is open source, you bring your own database — and for this quick start, we will use SQLite so there is nothing extra to install.\n\n<Note>\n  Want a zero-setup option? Try Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\nIn this example, we will use Memori with OpenAI and SQLite. Check out the [LLM providers](/docs/memori-byodb/llm/overview) and [database](/docs/memori-byodb/databases/overview) guides for other integrations.\n\n## Prerequisites\n\n- Python 3.10 or higher\n- An OpenAI API key\n\n## Step 1: Install Libraries\n\nInstall Memori and the OpenAI SDK:\n\n<CodeGroup title=\"Install Memori\">\n\n```bash {{ title: 'pip' }}\npip install memori openai\n```\n\n```bash {{ title: 'poetry' }}\npoetry add memori openai\n```\n\n```bash {{ title: 'uv' }}\nuv add memori openai\n```\n\n</CodeGroup>\n\n## Step 2: Set Environment Variables\n\nSet your OpenAI API key as an environment variable:\n\n```bash\nexport OPENAI_API_KEY=\"your-openai-api-key\"\n```\n\n## Step 3: Run Your First Memori Application\n\nCreate a new Python file `quickstart.py` and add the following code:\n\n### Setup & Configuration\n\nImport libraries, set up a SQLite database with Python's built-in `sqlite3`, and initialize Memori with your OpenAI client.\n\n- `conn` accepts a connection factory (SQLAlchemy, DB-API 2.0, Django ORM, or MongoDB callable)\n- `llm.register()` wraps your LLM client for automatic memory capture\n- `attribution()` links memories to a specific user and process\n- `build()` creates the Memori schema tables in your database\n\n```python\nimport os\nimport sqlite3\nfrom memori import Memori\nfrom openai import OpenAI\n\ndef get_sqlite_connection():\n    return sqlite3.connect(\"memori.db\")\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori(conn=get_sqlite_connection).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"test-ai-agent\")\nmem.config.storage.build()\n```\n\nIf your app already uses SQLAlchemy, you can pass a `sessionmaker` instead:\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nmem = Memori(conn=SessionLocal)\n```\n\n### First Conversation\n\nTell the LLM a fact about yourself. Memori automatically captures the\nconversation and processes it through Advanced Augmentation.\n\nSince augmentation runs asynchronously, call `augmentation.wait()` in\nshort-lived scripts to ensure memories are fully processed before continuing.\n\n```python\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"My favorite color is blue.\"}\n    ]\n)\nprint(response.choices[0].message.content + \"\\n\")\n\n# Wait for background augmentation to finish\nmem.augmentation.wait()\n```\n\n### Memory Recall\n\nCreate a completely new client and Memori instance — no prior context\ncarried over. When you ask the LLM what it remembers, Memori\nautomatically injects the relevant facts via semantic search.\n\nThe second response should correctly recall your favorite color, proving\nmemory persistence works across sessions.\n\n```python\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n\nmem = Memori(conn=get_sqlite_connection).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"test-ai-agent\")\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"What's my favorite color?\"}\n    ]\n)\nprint(response.choices[0].message.content + \"\\n\")\n```\n\n## Step 4: Run the Application\n\nExecute your Python file:\n\n```bash\npython quickstart.py\n```\n\nYou should see the AI respond to both questions, with the second response correctly recalling that your favorite color is blue!\n\n## Step 5: Inspect Your Memories\n\nSince you own the database, you can inspect what Memori stored directly:\n\n<CodeGroup title=\"Inspect Memories\">\n\n```bash {{ title: 'Messages' }}\nsqlite3 memori.db \"SELECT * FROM memori_conversation_message;\"\n```\n\n```bash {{ title: 'Facts' }}\nsqlite3 memori.db \"SELECT * FROM memori_entity_fact;\"\n```\n\n```bash {{ title: 'Knowledge Graph' }}\nsqlite3 memori.db \"SELECT * FROM memori_knowledge_graph;\"\n```\n\n```bash {{ title: 'Sessions' }}\nsqlite3 memori.db \"SELECT * FROM memori_session;\"\n```\n\n</CodeGroup>\n"
  },
  {
    "path": "docs/memori-byodb/getting-started/use-cases.mdx",
    "content": "---\ntitle: Use Cases\ndescription: Common use cases and applications for Memori open source.\n---\n\n# Use Cases\n\nMemori is designed for any application where AI agents need to remember context across conversations. Here are the most common use cases — all running with your own database.\n\n<Note>\n  Want a zero-setup option? Try Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Customer Support Chatbots\n\nBuild support bots that remember customer history, preferences, and previous issues. No more \"Can you repeat your account number?\" — Memori recalls everything automatically.\n\n**Benefits:**\n\n- Remember customer preferences and history\n- Recall previous support tickets and resolutions\n- Personalize responses based on past interactions\n- Track issues across multiple sessions\n- All data stays in your database for compliance\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\n\n# Each customer gets their own memory space\nmem.attribution(\n    entity_id=\"customer_456\",\n    process_id=\"support_bot\"\n)\n\n# Memori automatically recalls relevant context\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\n        \"role\": \"user\",\n        \"content\": \"I'm having that issue again\"\n    }]\n)\n# Memori injects: \"Customer previously reported\n# login timeout issues on 2024-01-15\"\n```\n\n## Personalized AI Assistants\n\nCreate AI assistants that learn and adapt to each user over time. Memori builds a profile of preferences, skills, and context that makes every interaction more relevant.\n\n**Benefits:**\n\n- Learn coding preferences and tech stack\n- Remember project context across sessions\n- Adapt communication style to user preferences\n- Build long-term user profiles automatically\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom anthropic import Anthropic\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = Anthropic()\nmem = Memori(conn=SessionLocal).llm.register(client)\n\nmem.attribution(\n    entity_id=\"developer_789\",\n    process_id=\"code_assistant\"\n)\n\n# Over time, Memori learns:\n# - \"Uses Python 3.12 with FastAPI\"\n# - \"Prefers type hints and dataclasses\"\n# - \"Works on e-commerce platform\"\nresponse = client.messages.create(\n    model=\"claude-sonnet-4-5-20250929\",\n    max_tokens=1024,\n    messages=[{\n        \"role\": \"user\",\n        \"content\": \"How should I structure this endpoint?\"\n    }]\n)\n```\n\n## Multi-Agent Workflows\n\nCoordinate multiple AI agents that share context through Memori. Each agent contributes to a shared memory space while maintaining its own process identity.\n\n**Benefits:**\n\n- Share context between specialized agents\n- Track which agent contributed what information\n- Maintain conversation continuity across handoffs\n- Build collective knowledge graphs\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\nfrom openai import OpenAI\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\n\n# Research agent gathers information\nmem.attribution(\n    entity_id=\"project_alpha\",\n    process_id=\"research_agent\"\n)\nclient.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\n        \"role\": \"user\",\n        \"content\": \"Research competitor pricing\"\n    }]\n)\n\n# Analysis agent recalls research findings\nmem.attribution(\n    entity_id=\"project_alpha\",\n    process_id=\"analysis_agent\"\n)\n# Memori shares context across agents\n# for the same entity\n```\n"
  },
  {
    "path": "docs/memori-byodb/index.mdx",
    "content": "---\ntitle: Introduction\ndescription: Memori is an open-source, structured memory layer for AI agents — own your data, choose your database, keep full control.\n---\n\n# What is Memori?\n\n__Memori__ is a memory layer for LLM applications, agents, and copilots. It continuously captures interactions, extracts structured knowledge, and intelligently ranks, decays, and retrieves the relevant memories. So your AI remembers the right things at the right time across every session.\n\n<Note>\n  Memori BYODB supports the Python SDK only. For TypeScript, use\n  [Memori Cloud](/docs/memori-cloud).\n</Note>\nMemori uses [Advanced\nAugmentation](/docs/memori-byodb/concepts/advanced-augmentation) to turn raw\nconversations into structured, searchable memories. It runs asynchronously in\nthe background to minimize impact on your response path\n\n## Why Memori BYODB?\n\n### Database Freedom\n\nUse SQLite, PostgreSQL, MySQL, MariaDB, Oracle, MongoDB, CockroachDB, or\nOceanBase. Managed providers like Neon, Supabase, and AWS RDS/Aurora are also\nsupported through their compatible engines.\n\n### Full Data Ownership\n\nYour data stays in your database, on your infrastructure. Full compliance and\nregulatory control with no third-party storage.\n\n### LLM Provider Support\n\nOpenAI, Anthropic, Gemini, and Grok (xAI) via direct SDK wrappers. Bedrock is\nsupported via LangChain `ChatBedrock`. OpenAI-compatible providers (Nebius,\nDeepseek, NVIDIA NIM, Azure OpenAI, and more) work through OpenAI's `base_url`\nparameter. Supports sync, async, streamed, and unstreamed modes, plus LangChain\n, Agno, and Pydantic AI.\n\n### Intelligent Recall\n\nIntelligent Recall surfaces the right memories at the right time. Memories are\nranked by relevance and importance, with intelligent decay so older or less\nrelevant facts recede — so your AI stays contextually aware without clutter.\nRecall any memory later with semantic search; use manual recall for custom\nprompts, UIs, or debugging. See [How Memori\nWorks](/docs/memori-byodb/concepts/how-memory-works#how-recall-works) for\nautomatic vs manual recall and tuning.\n\n## Quick Example\n\nGet started with a database connection and your favorite LLM:\n\n- **One-line setup** — Connect your DB and LLM; memory capture, augmentation, and recall work without extra config.\n- **Semantic recall** — Queries like “what does this user prefer?” pull the right memories automatically.\n- **Dashboard** — Use [app.memorilabs.ai](https://app.memorilabs.ai) for API keys, usage, and (with Memori Cloud) the Graph Explorer and Playground.\n- **Your data, your rules** — Store everything in your DB; compliance, backups, and custom analytics stay under your control.\n- **Roadmap** — Knowledge-graph APIs, configurable decay, and richer dashboarding are on the way.\n\n```python\nimport os\nimport sqlite3\nfrom memori import Memori\nfrom openai import OpenAI\n\n# Requires OPENAI_API_KEY in your environment\ndef get_sqlite_connection():\n    return sqlite3.connect(\"memori.db\")\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori(conn=get_sqlite_connection).llm.register(client)\n\n# Track conversations by user and process\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"support_agent\")\n\n# All conversations automatically persisted and recalled\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"My favorite color is blue.\"}]\n)\n```\n\n## OpenAI-Compatible Providers\n\nMemori supports any model that uses OpenAI's client interface via the `base_url` parameter — including Nebius, Deepseek, NVIDIA NIM, and more.\n\n```python\nimport os\nimport sqlite3\nfrom memori import Memori\nfrom openai import OpenAI\n\ndef get_sqlite_connection():\n    return sqlite3.connect(\"memori.db\")\n\nclient = OpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\n\nmem = Memori(conn=get_sqlite_connection).llm.register(client)\n```\n\n## Core Concepts\n\n| Concept          | Description                                           | Example                                  |\n| ---------------- | ----------------------------------------------------- | ---------------------------------------- |\n| **Entity**       | Person, place, or thing (like a user)                 | `entity_id=\"user_123\"`                   |\n| **Process**      | Your agent, LLM interaction, or program               | `process_id=\"support_agent\"`             |\n| **Session**      | Groups LLM interactions together                      | Auto-generated UUID, manually manageable |\n| **Augmentation** | Background AI enhancement of memories                 | Auto-runs after wrapped LLM calls        |\n| **Recall**       | Retrieve relevant memories from previous interactions | Auto-injects recalled memories           |\n\n## Architecture Overview\n\nThe diagram has three lanes: your app, the Memori SDK, and your own database. Your app calls the LLM normally, Memori intercepts the call, and the synchronous response path continues with zero added latency.\n\nSynchronous capture: conversation messages are stored in `memori_conversation_message` while your normal LLM flow continues.\n\nRecall injection: relevant memories are pulled from `memori_entity_fact` and injected into later prompts.\n\nAsync augmentation: background processing extracts facts, preferences, rules, events, and relationships from conversations.\n\nOwn-your-data storage: structured memory records are written to your database, including `memori_entity_fact`, `memori_process_attribute`, and `memori_knowledge_graph`.\n\n![\"Memori BYODB architecture\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-byodb-architecture.webp)\n"
  },
  {
    "path": "docs/memori-byodb/llm/agno.mdx",
    "content": "---\ntitle: Agno\ndescription: Using Memori with Agno agents and Memori BYODB.\n---\n\n# Agno\n\nMemori integrates with Agno at the model layer. Register your Agno model with `llm.register(...)` and Memori captures `run()`, `arun()`, and streamed responses automatically.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"Agno Integration\">\n\n```python {{ title: 'Sync' }}\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori(conn=SessionLocal).llm.register(openai_chat=model)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"agno_agent\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nresponse = agent.run(\"Hello!\", session_id=\"support-session\")\nprint(response.content)\n```\n\n```python {{ title: 'Async' }}\nimport asyncio\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori(conn=SessionLocal).llm.register(openai_chat=model)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"agno_agent\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nasync def main():\n    response = await agent.arun(\"Hello!\", session_id=\"support-session\")\n    print(response.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori(conn=SessionLocal).llm.register(openai_chat=model)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"agno_agent\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nstream = agent.run(\"Hello!\", session_id=\"support-session\", stream=True)\nfor chunk in stream:\n    if hasattr(chunk, \"content\") and chunk.content:\n        print(chunk.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Different Providers\n\nAgno supports multiple model families. Use the matching registration keyword in Memori.\n\n| Package                 | Model Class  | Registration Keyword |\n| ----------------------- | ------------ | -------------------- |\n| `agno.models.openai`    | `OpenAIChat` | `openai_chat=model`  |\n| `agno.models.anthropic` | `Claude`     | `claude=model`       |\n| `agno.models.google`    | `Gemini`     | `gemini=model`       |\n| `agno.models.xai`       | `xAI`        | `xai=model`          |\n\n<CodeGroup title=\"Agno Providers\">\n\n```python {{ title: 'OpenAI' }}\nfrom agno.models.openai import OpenAIChat\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\nmem = Memori(conn=SessionLocal).llm.register(openai_chat=model)\n```\n\n```python {{ title: 'Anthropic' }}\nfrom agno.models.anthropic import Claude\n\nmodel = Claude(id=\"claude-sonnet-4-20250514\")\nmem = Memori(conn=SessionLocal).llm.register(claude=model)\n```\n\n```python {{ title: 'Google Gemini' }}\nfrom agno.models.google import Gemini\n\nmodel = Gemini(id=\"gemini-2.0-flash-exp\")\nmem = Memori(conn=SessionLocal).llm.register(gemini=model)\n```\n\n```python {{ title: 'xAI' }}\nfrom agno.models.xai import xAI\n\nmodel = xAI(id=\"grok-3\")\nmem = Memori(conn=SessionLocal).llm.register(xai=model)\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                   |\n| ------------ | ------------------------ |\n| **Sync**     | `agent.run()`            |\n| **Async**    | `await agent.arun()`     |\n| **Streamed** | `agent.run(stream=True)` |\n"
  },
  {
    "path": "docs/memori-byodb/llm/anthropic.mdx",
    "content": "---\ntitle: Anthropic\ndescription: Using Memori with Anthropic Claude models and Memori BYODB.\n---\n\n# Anthropic\n\nMemori supports all Anthropic Claude models. Register your client once and every conversation is automatically captured and stored. Note: `max_tokens` is required for all Anthropic API calls.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"Anthropic Integration\">\n\n```python {{ title: 'Sync' }}\nfrom anthropic import Anthropic\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = Anthropic()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"claude_assistant\")\n\nresponse = client.messages.create(\n    model=\"claude-3-5-sonnet-20241022\",\n    max_tokens=1024,\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.content[0].text)\n```\n\n```python {{ title: 'Async' }}\nimport asyncio\nfrom anthropic import AsyncAnthropic\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = AsyncAnthropic()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"claude_assistant\")\n\nasync def main():\n    response = await client.messages.create(\n        model=\"claude-3-5-sonnet-20241022\",\n        max_tokens=1024,\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.content[0].text)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nfrom anthropic import Anthropic\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = Anthropic()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"claude_assistant\")\n\nwith client.messages.stream(\n    model=\"claude-3-5-sonnet-20241022\",\n    max_tokens=1024,\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n) as stream:\n    for text in stream.text_stream:\n        print(text, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                           |\n| ------------ | -------------------------------- |\n| **Sync**     | `client.messages.create()`       |\n| **Async**    | `await client.messages.create()` |\n| **Streamed** | `client.messages.stream()`       |\n\n## System Prompts\n\nAnthropic supports a top-level `system` parameter separate from the `messages` array. Memori captures both.\n\n```python\nresponse = client.messages.create(\n    model=\"claude-3-5-sonnet-20241022\",\n    max_tokens=1024,\n    system=\"You are a helpful coding assistant.\",\n    messages=[{\"role\": \"user\", \"content\": \"Explain Python decorators.\"}]\n)\nprint(response.content[0].text)\n```\n"
  },
  {
    "path": "docs/memori-byodb/llm/aws-bedrock.mdx",
    "content": "---\ntitle: AWS Bedrock\ndescription: Using Memori with AWS Bedrock models via the LangChain ChatBedrock adapter and Memori BYODB.\n---\n\n# AWS Bedrock\n\nMemori supports AWS Bedrock through `langchain-aws`. Register using the `chatbedrock` keyword to access Claude, Llama, Mistral, and other Bedrock models.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"Bedrock Integration\">\n\n```python {{ title: 'Sync' }}\nfrom langchain_aws import ChatBedrock\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = ChatBedrock(\n    model_id=\"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n    region_name=\"us-east-1\"\n)\n\nmem = Memori(conn=SessionLocal).llm.register(chatbedrock=client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"bedrock_agent\")\n\nresponse = client.invoke(\"Hello!\")\nprint(response.content)\n```\n\n```python {{ title: 'Async' }}\nimport asyncio\nfrom langchain_aws import ChatBedrock\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = ChatBedrock(\n    model_id=\"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n    region_name=\"us-east-1\"\n)\n\nmem = Memori(conn=SessionLocal).llm.register(chatbedrock=client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"bedrock_agent\")\n\nasync def main():\n    response = await client.ainvoke(\"Hello!\")\n    print(response.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nfrom langchain_aws import ChatBedrock\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = ChatBedrock(\n    model_id=\"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n    region_name=\"us-east-1\"\n)\n\nmem = Memori(conn=SessionLocal).llm.register(chatbedrock=client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"bedrock_agent\")\n\nfor chunk in client.stream(\"Hello!\"):\n    print(chunk.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                   |\n| ------------ | ------------------------ |\n| **Sync**     | `client.invoke()`        |\n| **Async**    | `await client.ainvoke()` |\n| **Streamed** | `client.stream()`        |\n\n## Available Models\n\n| Model                 | Model ID                                    |\n| --------------------- | ------------------------------------------- |\n| **Claude 3.5 Sonnet** | `anthropic.claude-3-5-sonnet-20241022-v2:0` |\n| **Claude 3 Haiku**    | `anthropic.claude-3-haiku-20240307-v1:0`    |\n| **Llama 3.1 70B**     | `meta.llama3-1-70b-instruct-v1:0`           |\n| **Mistral Large**     | `mistral.mistral-large-2407-v1:0`           |\n"
  },
  {
    "path": "docs/memori-byodb/llm/deepseek.mdx",
    "content": "---\ntitle: DeepSeek\ndescription: Using Memori with DeepSeek models via the OpenAI-compatible API and Memori BYODB.\n---\n\n# DeepSeek\n\nDeepSeek provides an OpenAI-compatible API. Use the `openai` Python package with `base_url=\"https://api.deepseek.com\"` — no special adapter needed.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"DeepSeek Integration\">\n\n```python {{ title: 'Sync' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(\n    base_url=\"https://api.deepseek.com\",\n    api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"deepseek_assistant\")\n\nresponse = client.chat.completions.create(\n    model=\"deepseek-chat\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n```python {{ title: 'Async' }}\nimport os, asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = AsyncOpenAI(\n    base_url=\"https://api.deepseek.com\",\n    api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"deepseek_assistant\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"deepseek-chat\",\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(\n    base_url=\"https://api.deepseek.com\",\n    api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"deepseek_assistant\")\n\nstream = client.chat.completions.create(\n    model=\"deepseek-chat\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n    stream=True\n)\nfor chunk in stream:\n    if chunk.choices[0].delta.content:\n        print(chunk.choices[0].delta.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                                   |\n| ------------ | ---------------------------------------- |\n| **Sync**     | `client.chat.completions.create()`       |\n| **Async**    | `await client.chat.completions.create()` |\n| **Streamed** | `stream=True` parameter                  |\n"
  },
  {
    "path": "docs/memori-byodb/llm/gemini.mdx",
    "content": "---\ntitle: Google Gemini\ndescription: Using Memori with Google Gemini models and Memori BYODB.\n---\n\n# Google Gemini\n\nMemori integrates with Google Gemini via the `google-genai` SDK. Register the `GenerativeModel` instance and all `generate_content()` calls are automatically captured.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"Gemini Integration\">\n\n```python {{ title: 'Sync' }}\nimport os\nfrom memori import Memori\nimport google.generativeai as genai\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\ngenai.configure(api_key=os.getenv(\"GOOGLE_API_KEY\"))\nclient = genai.GenerativeModel(\"gemini-2.0-flash-exp\")\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"gemini_assistant\")\n\nresponse = client.generate_content(\"Hello!\")\nprint(response.text)\n```\n\n```python {{ title: 'Async' }}\nimport os, asyncio\nfrom memori import Memori\nimport google.generativeai as genai\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\ngenai.configure(api_key=os.getenv(\"GOOGLE_API_KEY\"))\nclient = genai.GenerativeModel(\"gemini-2.0-flash-exp\")\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"gemini_assistant\")\n\nasync def main():\n    response = await client.generate_content_async(\"Hello!\")\n    print(response.text)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nimport os\nfrom memori import Memori\nimport google.generativeai as genai\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\ngenai.configure(api_key=os.getenv(\"GOOGLE_API_KEY\"))\nclient = genai.GenerativeModel(\"gemini-2.0-flash-exp\")\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"gemini_assistant\")\n\nresponse = client.generate_content(\"Hello!\", stream=True)\nfor chunk in response:\n    print(chunk.text, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                                  |\n| ------------ | --------------------------------------- |\n| **Sync**     | `client.generate_content()`             |\n| **Async**    | `await client.generate_content_async()` |\n| **Streamed** | `stream=True` parameter                 |\n\n## Multi-Turn Conversations\n\nUse `start_chat()` for multi-turn interactions. Memori tracks the full conversation automatically.\n\n```python\nchat = client.start_chat()\nresponse = chat.send_message(\"My name is Alice.\")\nprint(response.text)\n\nresponse = chat.send_message(\"What's my name?\")\nprint(response.text)\n```\n"
  },
  {
    "path": "docs/memori-byodb/llm/langchain.mdx",
    "content": "---\ntitle: LangChain\ndescription: Using Memori with LangChain chat models and Memori BYODB.\n---\n\n# LangChain\n\nMemori supports any LangChain chat model. Each class has its own registration keyword: `chatopenai` for ChatOpenAI/ChatAnthropic, `chatbedrock` for ChatBedrock, `chatgooglegenai` for ChatGoogleGenerativeAI.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"LangChain Integration\">\n\n```python {{ title: 'Sync' }}\nfrom langchain_openai import ChatOpenAI\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = ChatOpenAI(model=\"gpt-4o-mini\")\nmem = Memori(conn=SessionLocal).llm.register(chatopenai=client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"langchain_agent\")\n\nresponse = client.invoke(\"Hello!\")\nprint(response.content)\n```\n\n```python {{ title: 'Async' }}\nimport asyncio\nfrom langchain_openai import ChatOpenAI\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = ChatOpenAI(model=\"gpt-4o-mini\")\nmem = Memori(conn=SessionLocal).llm.register(chatopenai=client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"langchain_agent\")\n\nasync def main():\n    response = await client.ainvoke(\"Hello!\")\n    print(response.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nfrom langchain_openai import ChatOpenAI\nfrom memori import Memori\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = ChatOpenAI(model=\"gpt-4o-mini\")\nmem = Memori(conn=SessionLocal).llm.register(chatopenai=client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"langchain_agent\")\n\nfor chunk in client.stream(\"Hello!\"):\n    print(chunk.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Different Providers\n\n| Package                  | Chat Model               | Registration Keyword     |\n| ------------------------ | ------------------------ | ------------------------ |\n| `langchain-openai`       | `ChatOpenAI`             | `chatopenai=client`      |\n| `langchain-anthropic`    | `ChatAnthropic`          | `chatopenai=client`      |\n| `langchain-google-genai` | `ChatGoogleGenerativeAI` | `chatgooglegenai=client` |\n| `langchain-aws`          | `ChatBedrock`            | `chatbedrock=client`     |\n\n<CodeGroup title=\"LangChain Providers\">\n\n```python {{ title: 'Anthropic' }}\nfrom langchain_anthropic import ChatAnthropic\n\nclient = ChatAnthropic(model=\"claude-3-5-sonnet-20241022\")\nmem = Memori(conn=SessionLocal).llm.register(chatopenai=client)\n```\n\n```python {{ title: 'Google Gemini' }}\nfrom langchain_google_genai import ChatGoogleGenerativeAI\n\nclient = ChatGoogleGenerativeAI(model=\"gemini-2.0-flash-exp\")\nmem = Memori(conn=SessionLocal).llm.register(chatgooglegenai=client)\n```\n\n```python {{ title: 'AWS Bedrock' }}\nfrom langchain_aws import ChatBedrock\n\nclient = ChatBedrock(model_id=\"anthropic.claude-3-5-sonnet-20241022-v2:0\", region_name=\"us-east-1\")\nmem = Memori(conn=SessionLocal).llm.register(chatbedrock=client)\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                   |\n| ------------ | ------------------------ |\n| **Sync**     | `client.invoke()`        |\n| **Async**    | `await client.ainvoke()` |\n| **Streamed** | `client.stream()`        |\n"
  },
  {
    "path": "docs/memori-byodb/llm/nebius.mdx",
    "content": "---\ntitle: Nebius AI Studio\ndescription: Using Memori with Nebius AI Studio models via the OpenAI-compatible API and Memori BYODB.\n---\n\n# Nebius AI Studio\n\nNebius AI Studio provides an OpenAI-compatible API. Use the `openai` Python package with `base_url=\"https://api.studio.nebius.com/v1/\"` — no special adapter needed.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"Nebius AI Studio Integration\">\n\n```python {{ title: 'Sync' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"nebius_assistant\")\n\nresponse = client.chat.completions.create(\n    model=\"meta-llama/Llama-3.3-70B-Instruct\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n```python {{ title: 'Async' }}\nimport os, asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = AsyncOpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"nebius_assistant\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"meta-llama/Llama-3.3-70B-Instruct\",\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"nebius_assistant\")\n\nstream = client.chat.completions.create(\n    model=\"meta-llama/Llama-3.3-70B-Instruct\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n    stream=True\n)\nfor chunk in stream:\n    if chunk.choices[0].delta.content:\n        print(chunk.choices[0].delta.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                                   |\n| ------------ | ---------------------------------------- |\n| **Sync**     | `client.chat.completions.create()`       |\n| **Async**    | `await client.chat.completions.create()` |\n| **Streamed** | `stream=True` parameter                  |\n"
  },
  {
    "path": "docs/memori-byodb/llm/openai.mdx",
    "content": "---\ntitle: OpenAI\ndescription: Using Memori with OpenAI models including GPT-4o, GPT-4.1, and the Responses API with Memori BYODB.\n---\n\n# OpenAI\n\nMemori supports all OpenAI models and API styles. Register your client once and every call is automatically captured and stored.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"OpenAI Integration\">\n\n```python {{ title: 'Sync' }}\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n```python {{ title: 'Async' }}\nimport asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = AsyncOpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nstream = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n    stream=True\n)\nfor chunk in stream:\n    if chunk.choices[0].delta.content:\n        print(chunk.choices[0].delta.content, end=\"\")\n```\n\n```python {{ title: 'Responses API' }}\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nresponse = client.responses.create(\n    model=\"gpt-4o-mini\",\n    input=\"Hello!\",\n    instructions=\"You are a helpful assistant.\"\n)\nprint(response.output_text)\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode              | Method                                   |\n| ----------------- | ---------------------------------------- |\n| **Sync**          | `client.chat.completions.create()`       |\n| **Async**         | `await client.chat.completions.create()` |\n| **Streamed**      | `stream=True` parameter                  |\n| **Responses API** | `client.responses.create()`              |\n\n## Multi-Turn Conversations\n\nMemori captures each call and links them within the same session. Pass your full conversation history as usual.\n\n```python\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nmessages = [{\"role\": \"user\", \"content\": \"My name is Alice.\"}]\nresponse = client.chat.completions.create(model=\"gpt-4o-mini\", messages=messages)\nmessages.append({\"role\": \"assistant\", \"content\": response.choices[0].message.content})\n\nmessages.append({\"role\": \"user\", \"content\": \"What's my name?\"})\nresponse = client.chat.completions.create(model=\"gpt-4o-mini\", messages=messages)\nprint(response.choices[0].message.content)\n```\n"
  },
  {
    "path": "docs/memori-byodb/llm/overview.mdx",
    "content": "---\ntitle: LLM Providers Overview\ndescription: Memori is LLM-agnostic. Register any supported client with a local database connection and Memori handles memory capture, augmentation, and recall automatically.\n---\n\n# LLM Providers Overview\n\nMemori works with all major LLM providers and frameworks. Register any supported client and Memori handles memory capture, augmentation, and recall automatically.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Supported Providers\n\n| Provider                                              | Integration        | Install                               |\n| ----------------------------------------------------- | ------------------ | ------------------------------------- |\n| **[OpenAI](/docs/memori-byodb/llm/openai)**           | Direct SDK wrapper | `pip install memori openai`           |\n| **[Anthropic](/docs/memori-byodb/llm/anthropic)**     | Direct SDK wrapper | `pip install memori anthropic`        |\n| **[Google Gemini](/docs/memori-byodb/llm/gemini)**    | Direct SDK wrapper | `pip install memori google-genai`     |\n| **[xAI Grok](/docs/memori-byodb/llm/xai-grok)**       | OpenAI-compatible  | `pip install memori openai`           |\n| **[AWS Bedrock](/docs/memori-byodb/llm/aws-bedrock)** | LangChain adapter  | `pip install memori langchain-aws`    |\n| **[LangChain](/docs/memori-byodb/llm/langchain)**     | Framework support  | `pip install memori langchain-openai` |\n| **[Agno](/docs/memori-byodb/llm/agno)**               | Framework support  | `pip install memori agno`             |\n| **[Pydantic AI](/docs/memori-byodb/llm/pydantic-ai)** | Framework support  | `pip install memori pydantic-ai`      |\n| **[Nebius AI Studio](/docs/memori-byodb/llm/nebius)** | OpenAI-compatible  | `pip install memori openai`           |\n| **[DeepSeek](/docs/memori-byodb/llm/deepseek)**       | OpenAI-compatible  | `pip install memori openai`           |\n\nAll providers support sync, async, streamed, and unstreamed modes.\n\nAny provider with an OpenAI-compatible API works by setting a custom `base_url` — including Nebius AI Studio, DeepSeek, NVIDIA NIM, Azure OpenAI, and more. See [OpenAI-Compatible Providers](#openai-compatible-providers) below.\n\n## Pydantic AI\n\nRegister the `Agent` instance directly — Memori wraps `run_sync` and `run` automatically.\n\n```python\nfrom memori import Memori\nfrom pydantic_ai import Agent\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nagent = Agent(\"openai:gpt-4o-mini\")\nmem = Memori(conn=SessionLocal).llm.register(agent)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"pydantic_agent\")\n\nresult = agent.run_sync(\"Hello!\")\nprint(result.output)\n```\n\n## OpenAI-Compatible Providers\n\nAny provider with an OpenAI-compatible API works by setting a custom `base_url`. Examples: xAI, Nebius AI Studio, DeepSeek, Azure OpenAI.\n\n```python\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nresponse = client.chat.completions.create(\n    model=\"meta-llama/Llama-3.3-70B-Instruct\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n## OpenAI Responses API\n\n```python\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nresponse = client.responses.create(\n    model=\"gpt-4o-mini\",\n    input=\"Hello!\",\n    instructions=\"You are a helpful assistant.\"\n)\nprint(response.output_text)\n```\n"
  },
  {
    "path": "docs/memori-byodb/llm/pydantic-ai.mdx",
    "content": "---\ntitle: Pydantic AI\ndescription: Using Memori with Pydantic AI agents and Memori BYODB.\n---\n\n# Pydantic AI\n\nMemori integrates at the agent level — register the `Agent` instance and Memori wraps `run_sync` and `run` automatically. Works with any Pydantic AI-supported model.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n```python\nfrom memori import Memori\nfrom pydantic_ai import Agent\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nagent = Agent(\"openai:gpt-4o-mini\")\n\nmem = Memori(conn=SessionLocal).llm.register(agent)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"pydantic_agent\")\n\nresult = agent.run_sync(\"Hello!\")\nprint(result.output)\n```\n\n## Async Usage\n\n```python\nimport asyncio\nfrom memori import Memori\nfrom pydantic_ai import Agent\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nagent = Agent(\"openai:gpt-4o-mini\")\n\nmem = Memori(conn=SessionLocal).llm.register(agent)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"pydantic_agent\")\n\nasync def main():\n    result = await agent.run(\"Hello!\")\n    print(result.output)\n\nasyncio.run(main())\n```\n\n## Different Providers\n\nPydantic AI supports multiple providers via model strings. Registration is identical regardless of provider.\n\n<CodeGroup title=\"Pydantic AI Providers\">\n\n```python {{ title: 'OpenAI' }}\nagent = Agent(\"openai:gpt-4o-mini\")\nmem = Memori(conn=SessionLocal).llm.register(agent)\n```\n\n```python {{ title: 'Anthropic' }}\nagent = Agent(\"anthropic:claude-3-5-sonnet-20241022\")\nmem = Memori(conn=SessionLocal).llm.register(agent)\n```\n\n```python {{ title: 'Google Gemini' }}\nagent = Agent(\"google-gla:gemini-2.0-flash-exp\")\nmem = Memori(conn=SessionLocal).llm.register(agent)\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                      |\n| ------------ | --------------------------- |\n| **Sync**     | `agent.run_sync()`          |\n| **Async**    | `await agent.run()`         |\n| **Streamed** | Via agent streaming support |\n"
  },
  {
    "path": "docs/memori-byodb/llm/xai-grok.mdx",
    "content": "---\ntitle: xAI Grok\ndescription: Using Memori with xAI Grok models via the OpenAI-compatible API and Memori BYODB.\n---\n\n# xAI Grok\n\nxAI provides an OpenAI-compatible API. Use the `openai` Python package with `base_url=\"https://api.x.ai/v1\"` — no special adapter needed.\n\n<Note>\n  Want a zero-setup option? The Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"xAI Grok Integration\">\n\n```python {{ title: 'Sync' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(\n    base_url=\"https://api.x.ai/v1\",\n    api_key=os.getenv(\"XAI_API_KEY\")\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"grok_assistant\")\n\nresponse = client.chat.completions.create(\n    model=\"grok-2-latest\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n```python {{ title: 'Async' }}\nimport os, asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = AsyncOpenAI(\n    base_url=\"https://api.x.ai/v1\",\n    api_key=os.getenv(\"XAI_API_KEY\")\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"grok_assistant\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"grok-2-latest\",\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\n\nclient = OpenAI(\n    base_url=\"https://api.x.ai/v1\",\n    api_key=os.getenv(\"XAI_API_KEY\")\n)\n\nmem = Memori(conn=SessionLocal).llm.register(client)\nmem.config.storage.build()\nmem.attribution(entity_id=\"user_123\", process_id=\"grok_assistant\")\n\nstream = client.chat.completions.create(\n    model=\"grok-2-latest\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n    stream=True\n)\nfor chunk in stream:\n    if chunk.choices[0].delta.content:\n        print(chunk.choices[0].delta.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                                   |\n| ------------ | ---------------------------------------- |\n| **Sync**     | `client.chat.completions.create()`       |\n| **Async**    | `await client.chat.completions.create()` |\n| **Streamed** | `stream=True` parameter                  |\n"
  },
  {
    "path": "docs/memori-byodb/support/faq.mdx",
    "content": "---\ntitle: FAQ\ndescription: Frequently asked questions about Memori open source.\n---\n\n# FAQ\n\n<Note>\n  Want a zero-setup option? Try Memori Cloud at\n  [app.memorilabs.ai](https://app.memorilabs.ai).\n</Note>\n\n### What is Memori?\n\nMemori is a memory layer for LLM applications, agents, and copilots. It continuously captures interactions, extracts structured knowledge, and intelligently ranks, decays, and retrieves the relevant memories. So your AI remembers the right things at the right time across every session.\n\n### Memori Cloud vs Memori BYODB?\n\n**Memori Cloud** — managed storage, dashboard UI, just add an API key. **Memori BYODB** — bring your own database, full control. Both use the same Python SDK.\n\n### Which databases are supported?\n\nSQLite, PostgreSQL, MySQL, MariaDB, Oracle, MongoDB, CockroachDB, and OceanBase. Managed providers like Neon, Supabase, and AWS RDS/Aurora are supported through compatible PostgreSQL/MySQL engines. See the [Database guides](/docs/memori-byodb/databases/overview).\n\n### Do I need SQLAlchemy to use BYODB?\n\nNo. SQLAlchemy is one option, but not a requirement. Memori also supports direct DB-API 2.0 connection factories, Django ORM integration, and MongoDB database callables.\n\n### Which LLM providers work?\n\nOpenAI, Anthropic, Google Gemini, xAI Grok, Deepseek, Nebius AI Studio, AWS Bedrock, LangChain, Agno, and Pydantic AI. All support sync, async, and streaming. See the [LLM guides](/docs/memori-byodb/llm/overview).\n\n### Do I need a Memori API key?\n\nNo. The open-source version works without one. An API key only unlocks higher Advanced Augmentation quotas (100 free without key, 5,000/month with a free key).\n\n### Do I need to manage infrastructure?\n\nYes — you run your own database. For the simplest setup, use SQLite (single file, no server). For production, PostgreSQL is recommended.\n\n### Does it support async?\n\nYes. All providers support async mode out of the box. Just use your provider's async client.\n\n### How does augmentation work?\n\nIt runs in the background after each conversation and extracts facts, preferences, and relationships while minimizing impact on your LLM response path. See [Advanced Augmentation](/docs/memori-byodb/concepts/advanced-augmentation).\n\n### Can I use multiple LLM providers?\n\nYes. Register multiple clients on the same Memori instance. Memories are shared across providers since they're linked to entities, not providers.\n\n### Can I migrate between Memori Cloud and BYODB?\n\nYes. Both use the same SDK — just swap `conn=SessionLocal` for `api_key=\"...\"`. Existing local memories don't auto-transfer though.\n\n### What is `augmentation.wait()`?\n\nIt blocks until background augmentation finishes. Only needed in short-lived scripts that might exit before processing completes.\n\n### Still have questions?\n\n- [Troubleshooting](/docs/memori-byodb/support/troubleshooting) guide\n- [GitHub Issues](https://github.com/MemoriLabs/Memori/issues)\n- [Discord](https://discord.gg/abD4eGym6v)\n"
  },
  {
    "path": "docs/memori-byodb/support/troubleshooting.mdx",
    "content": "---\ntitle: Troubleshooting\ndescription: Common issues and solutions when using Memori open source.\n---\n\n# Troubleshooting\n\nQuick fixes for the most common Memori issues.\n\n## Installation\n\n**`pip install memori` fails** — Requires Python 3.10+.\nRun `python --version` to check, then `pip install --upgrade pip && pip install memori`.\n\n**Missing binary deps** — Install prerequisites first: `pip install numpy>=1.24.0 sentence-transformers>=3.0.0 memori`.\n\n## Database Connection\n\n**`No connection factory provided`** — You must pass `conn` when initializing:\n\n```python\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom memori import Memori\n\nengine = create_engine(\"sqlite:///memori.db\")\nSessionLocal = sessionmaker(bind=engine)\nmem = Memori(conn=SessionLocal)\n```\n\n**`Table does not exist`** — Run `mem.config.storage.build()` once after initialization or after upgrading.\n\n**Connection pool errors** — Enable pre-ping and recycling:\n\n```python\nengine = create_engine(\"postgresql+psycopg2://user:pass@host/db\", pool_pre_ping=True, pool_recycle=300)\n```\n\n**Connection string formats:**\n\n| Database   | Format                                         |\n| ---------- | ---------------------------------------------- |\n| SQLite     | `sqlite:///memori.db`                          |\n| PostgreSQL | `postgresql+psycopg2://user:pass@host:5432/db` |\n| MySQL      | `mysql+pymysql://user:pass@host:3306/db`       |\n\n## No Memories Being Created\n\n1. **Set attribution** before LLM calls — without it, no memories are stored:\n\n```python\nmem.attribution(entity_id=\"user_123\", process_id=\"my_app\")\n```\n\n2. **Wait for augmentation** in short-lived scripts:\n\n```python\nmem.augmentation.wait()\n```\n\n3. **Register your LLM client** — conversations aren't captured without registration:\n\n```python\nclient = OpenAI()\nmem = Memori(conn=SessionLocal).llm.register(client)\n```\n\n## Recall Returns Empty\n\n- Verify `entity_id` matches what was used when memories were created\n- Wait for augmentation: `mem.augmentation.wait()`\n- Increase limit: `mem.recall(\"query\", limit=10)`\n- Lower threshold: `mem.config.recall_relevance_threshold = 0.05`\n\n## Quota Exceeded\n\n```\nQuotaExceededError: your IP address is over quota\n```\n\nSign up for a free API key at [app.memorilabs.ai](https://app.memorilabs.ai) — gives 5,000/month (vs 100 without a key). Set it via `export MEMORI_API_KEY=\"your-key\"`.\n\n## Performance\n\n**Slow first run** — Memori downloads the embedding model on first use. Pre-download with `python -m memori setup`.\n\n**High memory usage** — Reduce embeddings limit: `mem.config.recall_embeddings_limit = 500`. Use PostgreSQL for production.\n\n**Network timeouts** — Increase timeout: `mem.config.request_secs_timeout = 10` and retries: `mem.config.request_num_backoff = 10`.\n\n## Debug Logging\n\n```python\nimport logging\nlogging.basicConfig(level=logging.DEBUG, format=\"%(asctime)s | %(name)s | %(levelname)s | %(message)s\")\n\nfrom memori import Memori\nmem = Memori(conn=SessionLocal, debug_truncate=False)\n```\n\n## Getting Help\n\n- [GitHub Issues](https://github.com/MemoriLabs/Memori/issues)\n- [Discord](https://discord.gg/abD4eGym6v)\n- [Examples](https://github.com/MemoriLabs/Memori/tree/main/examples)\n\nInclude your Python version, Memori version (`pip show memori`), database type, and full error trace.\n"
  },
  {
    "path": "docs/memori-cloud/benchmark/experiments.mdx",
    "content": "---\ntitle: LoCoMo Benchmark Experiments\ndescription: These experiments evaluate the quality and accuracy of the memory assets produced by Memori's Advanced Augmentation pipeline.\n---\n\n# LoCoMo Benchmark Experiments\n\nThe primary objective of these experiments is to evaluate the quality and accuracy of the memory assets produced by Memori's Advanced Augmentation pipeline.\n\n### Dataset: The LoCoMo Benchmark\n\nThe primary dataset utilized for benchmarking is the Long Conversation Memory (LoCoMo) dataset. LoCoMo is a rigorous framework engineered to evaluate an AI agent's ability to track, retain, and synthesize information across extensive, multi-session chat histories. Unlike standard QA datasets, LoCoMo challenges models with complex state tracking, temporal reasoning, and the retrieval of subtle user preferences buried deep within noisy, unstructured conversational logs\n\nTo ensure a fair comparison with other published results on this benchmark, we excluded the adversarial category from the evaluation.\n\n### Evaluating Memory Extraction via Advanced Augmentation\n\nTo measure the quality of Memori's Advanced Augmentation, all sections of each LoCoMo conversation were processed through the pipeline. Each session produced a set of semantic triples along with a conversation-level summary. The extracted triples were embedded using the Gemma-300 embedding model, enabling efficient semantic retrieval for the benchmark’s question-answering tasks. All generated memories were indexed and stored locally using FAISS to support fast similarity search. The ultimate accuracy of the LLM’s answers serves as a direct reflection of how well the Advanced Augmentation pipeline structured, preserved, and surfaced the relevant facts.\n\n### Answer Generation\n\nEach question in the LoCoMo benchmark was answered using GPT-4.1-mini, conditioned on the retrieved triples and their corresponding summaries (the utilized prompt is presented in the appendix). Triples were retrieved using a hybrid search approach that combines cosine similarity over embeddings with BM25 keyword matching.\n\n### Performance Metrics: LLM-as-a-Judge\n\nWe employ an LLM-as-a-Judge methodology (the utilized prompt is presented in the Appendix), using GPT-4.1-mini as the evaluator. The judge model analyzes the user query, the ground-truth answer, and the generated response to provide a nuanced assessment. \n\n### Token-Driven Cost Analysis \n\nBeyond response quality, practical deployment considerations are paramount for enterprise AI applications. We evaluate Memori against traditional architectures (e.g., standard RAG) by systematically measuring system efficiency as a function of context consumption.. \n\nThe absolute number of tokens added to the LLM prompt is the primary driver of operational costs in conversational AI. We measure the exact number of tokens extracted during retrieval and injected into the prompt context. This metric highlights a critical architectural distinction: while traditional architectures consume massive token budgets by indiscriminately injecting large, raw text chunks or full histories into the prompt, Memori retrieves highly concise, structured memory facts. By minimizing the context footprint, Memori directly curtails API expenditure and optimizes operational economics.\n"
  },
  {
    "path": "docs/memori-cloud/benchmark/overview.mdx",
    "content": "---\ntitle: LoCoMo Benchmark Overview\ndescription: Memori - A Persistent Memory Layer for Efficient, Context-Aware LLM Agents\n---\n\n# LoCoMo Benchmark Overview\n\nAs large language models (LLMs) evolve into autonomous agents, persistent memory at the API layer is essential for enabling context-aware behavior across LLMs and multi-session interactions. Existing approaches force vendor lock-in and rely on injecting large volumes of raw conversation into prompts, leading to high token costs and degraded performance.\n\nWe introduce Memori, a LLM-agnostic persistent memory layer that treats memory as a data structuring problem. Its Advanced Augmentation pipeline converts unstructured dialogue into compact semantic triples and conversation summaries, enabling precise retrieval and coherent reasoning.\n\nEvaluated on the LoCoMo benchmark, Memori achieves 81.95% accuracy, outperforming existing memory systems while using only 1,294 tokens per query (\\~5% of full context). This results in substantial cost reductions, including 67% fewer tokens than competing approaches and over 20× savings compared to full-context methods.\n\nThese results show that effective memory in LLM agents depends on structured representations instead of larger context windows, enabling scalable and cost-efficient deployment.\n\n<Admonition type=\"tip\">\n  Download the Paper &rarr; [Memori LoCoMo Benchmark PDF](https://s3.us-east-1.amazonaws.com/files.memorilabs.ai/memori-locomo-benchmark.pdf)\n</Admonition>\n\n## Introduction\n\nIn recent years, large language models (LLMs) have rapidly evolved into fully sophisticated AI agents. These foundation-model-powered systems have demonstrated strong performance across diverse domains, including research, software engineering, and scientific discovery, pushing the trajectory toward more general and adaptive intelligence. As the field matures, it is evident that modern agents extend beyond a pure LLM backbone and incorporate additional capabilities such as reasoning, planning, perception, memory, and tool use. Together, these components transform LLMs from static conditional generators into adaptive systems capable of interacting with external environments and improving over time.\n\nLarge language models (LLMs) have quickly become sophisticated AI agents. These foundation-model-powered systems perform well in research, software engineering, and scientific discovery, driving the move toward general intelligence. Modern agents now go beyond using only LLMs by adding reasoning, planning, perception, memory, and tool use. These components let LLMs act as adaptive systems that interact with their environments and improve over time.\n\nAmong these capabilities, memory stands out as a foundational pillar. Unlike reasoning or tool use, which are increasingly internalized within model parameters, memory remains largely dependent on external system design. This dependency arises because LLM parameters cannot be updated in real time during deployment. Memory mechanisms, therefore, play a key role in enabling agents to persist information across interactions, adapt to user context, and evolve based on experience.\n\nThis reliance on external memory is especially apparent from an application perspective: persistent memory is essential. Domains such as personalized assistants, recommendation systems, social simulations, and complex investigative workflows all require agents to retain and reason over historical information. Without memory, these systems behave as stateless responders, repeatedly reprocessing context and failing to build continuity over time. From a broader research perspective, agents’ ability to continually evolve through interaction is central to the pursuit of general intelligence. This capacity is fundamentally grounded in memory\n\nEnabling long-term, cross-session, cross-model memory introduces significant challenges. Naively storing and injecting past interactions into the prompt leads to rapidly growing context windows. This increases both cost and instability. As context size grows, models become more prone to overlooking critical information. They may produce inconsistent outputs and suffer from what is commonly referred to as *context rot*, in which relevant information is present but not effectively used.\n\nThese limitations highlight a key insight: memory in LLM systems is not simply a storage problem, but a structuring problem. The challenge is to transform noisy, unstructured conversational data into representations that are efficient to retrieve. These representations must also be effective for downstream reasoning.\n\nMemori implements this as a persistent memory layer that incrementally distills conversational data into structured representations. This process is handled by Advanced Augmentation, a memory creation pipeline that extracts, compresses, and organizes high-signal information from raw interactions for efficient retrieval and downstream use. Through empirical evaluation on the LoCoMo benchmark, we demonstrate that high-quality memory structuring enables strong reasoning performance while dramatically reducing the number of tokens required in the prompt, thereby improving the cost-efficiency and scalability of LLM agents.\n\n\n## System Architecture\n\nAs depicted in Figure 1, Memori operates as a decoupled memory layer positioned between the application logic and the underlying LLM. The system integrates via a lightweight **Memori SDK**, seamlessly wrapping existing LLM clients to intercept requests and manage memory natively.\n\n![\"Memori Cloud System Overview\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-cloud-architecture-detail.webp)\n\nThe core differentiators of the Memori architecture lie in how it structures unstructured data and how it intelligently retrieves that data for reasoning.\n\n### Advanced Augmentation: Structuring the Unstructured\n\nRaw conversation logs are noisy, filled with colloquialisms, pleasantries, self-corrections, and tangential discussions. When these raw, unstructured transcripts are directly chunked and embedded, as is standard in traditional RAG architectures, the resulting vector space becomes heavily cluttered. Direct retrieval from this noisy data is highly inefficient, leading to false positives, contradictory context, and massively inflated token consumption during the generation phase.\n\nTo solve this, **Advanced Augmentation** functions as an automated cognitive filter. It is a background memory creation pipeline designed to distill raw dialogue into searchable memory assets, shifting the system's memory from mere text storage to an organized knowledge base.\n\n* **Semantic Extraction & Triple Generation:** Rather than saving sentences, the pipeline deconstructs dialogue messages into atomic units of knowledge. It actively scans conversations for concrete facts, user preferences, constraints, and evolving attributes, structuring them into semantic triples (subject–predicate–object). Each triple is then linked to the exact conversation in which it was mentioned. This design delivers two key advantages. First, it produces a low-noise, high-signal index that improves vector search retrieval accuracy. Second, it functions as a compression layer.  \n* **Conversation Summarization:** While semantic triples excel at capturing granular, static facts, they inherently strip away the surrounding context. An isolated triple might state what a user prefers, but it lacks the narrative of *why* a decision was made or how a user’s goal evolved throughout a specific interaction. To bridge this gap, the pipeline simultaneously generates Conversation Summaries. These are concise, high-level overviews of specific conversational threads that capture the user’s overarching intent, the dialogue's chronological progression, and the task's implicit context. Because triples are tied to their source, each individual triple can be directly linked to the proper summary of the conversation in which it appears, allowing the system to easily retrieve the background story behind any isolated fact.\n\nAdvanced Augmentation creates an interconnected, dual-layered memory asset: Triples provide the precise, token-efficient facts needed for exact recall, while Conversation Summaries provide the cohesive narrative flow required for the LLM to understand temporal changes and execute complex reasoning. By linking atomic triples directly to the summaries of the conversations they originated from, the system ensures that granular facts are never divorced from their broader context.\n"
  },
  {
    "path": "docs/memori-cloud/benchmark/results.mdx",
    "content": "---\ntitle: LoCoMo Benchmark Results\ndescription: See how Memori’s Advanced Augmentation performed on the LoCoMo benchmark. Using our LLM-as-a-Judge framework, we evaluated four reasoning categories (Multi-Hop, Temporal, Open-Domain, and Single-Hop) and compared Memori against several memory baselines and a Full-Context ceiling.\n---\n# LoCoMo Benchmark Results\n\nThis section summarizes how Memori’s Advanced Augmentation performed on the LoCoMo benchmark. Using our LLM-as-a-Judge framework, we evaluated four reasoning categories (Multi-Hop, Temporal, Open-Domain, and Single-Hop) and compared Memori against several memory baselines and a Full-Context ceiling. We also examined the vital tradeoff between output accuracy and token cost efficiency. The results are presented in Table 1\\.\n\n| Table 1: LLM-as-a-Judge Evaluation Results on the LoCoMo Benchmark |  |  |  |  |  |\n| ----- | :---: | :---: | :---: | :---: | :---: |\n| Method | Single-hop (%) | Multi-hop (%) | Open-domain (%) | Temporal (%) | Overall (%) |\n| Memori | 87.87 | 72.70 | 63.54 | 80.37 | 81.95 |\n| Zep | 79.43 | 69.16 | 73.96 | 83.33 | 79.09 |\n| LangMem | 74.47 | 61.06 | 67.71 | 86.92 | 78.05 |\n| Mem0 | 62.41 | 57.32 | 44.79 | 66.47 | 62.47 |\n| Full-Context (Ceiling) | 88.53 | 77.70 | 71.88 | 92.70 | 87.52 |\n\n<Note>This table compares the factual accuracy and reasoning capabilities of Memori’s Advanced Augmentation assets against state-of-the-art baselines and a full-context ceiling. Memori performance values were computed using the average of three rounds.</Note>\n\nGraphical representation of Memori's average accuracy along with the standard deviation is presented in Figure 2\\.\n\n![\"Memori's average accuracy along with the standard deviation\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-locomo-benchmark.webp)\n\n## **Overall Performance**\n\nAs expected, the Full-Context setup achieved the highest score (87.52%). However, passing the entire conversation history into the prompt is fundamentally impractical in production due to prohibitive token costs, uncontrolled context expansion, and context degradation over time.\n\nAmong retrieval-based systems, Memori achieved a leading overall score of 81.95%, successfully outperforming Zep (79.09%), LangMem (78.05%), and Mem0 (62.47%). This validates the assumption that by structuring unstructured chat logs into semantic triples and summaries, Memori effectively isolates high-signal knowledge. This structured memory design significantly narrows the gap to the Full-Context ceiling while keeping context windows highly manageable and operational token costs low.\n\n## **Performance by Category**\n\nAnalyzing the results across reasoning categories highlights the specific strengths of Memori's extracted memory assets:\n\n* **Single-Hop Reasoning (87.87%):** Memori excels in direct fact retrieval, outperforming both LangMem (74.47%) and Zep (79.43%). By minimizing conversational noise and structuring data cleanly, the LLM is fed exact, undeniable facts, minimizing token consumption while maximizing direct recall.  \n* **Temporal Reasoning (80.37%):** Memori outperforms Mem0 (66.47%) but trails LangMem (86.92%) and Zep (83.33%) in temporal tracking. Isolated semantic triples capture static facts but often miss the temporal context needed to identify changes in user states or preferences across sessions. Memori’s summaries help rebuild this timeline, but the results show it needs better temporal reasoning.  \n* **Multi-Hop Reasoning (72.70%):** Memori performs strongly when asked to connect disparate pieces of information, outperforming Zep (69.16%) and trailing LangMem (61.06%) by a narrow margin. The combination of precise triples and cohesive summaries provides the necessary backdrop that helps the LLM connect isolated facts without needing the entire conversational transcript injected into the prompt.  \n* **Open-Domain Reasoning (63.54%):** This category remains challenging across all retrieval-based systems. Open-ended questions often lack clear retrieval anchors, making them difficult to match with granular triples. Furthermore, these queries require broad synthesis across massive contexts rather than simple fact extraction. While Memori lags slightly behind LangMem (67.71%) here, it is important to note that improving open-domain scores typically requires retrieving significantly larger chunks of text, which actively works against the system's core operational goal: strictly minimizing the number of tokens added to the context to control API costs.\n\n## **Token Usage and Cost Efficiency**\n\nTraditional memory architectures and standard RAG setups often rely on retrieving raw, uncompressed text chunks. This indiscriminately injects conversational noise and redundant dialogue into the prompt, consuming context limits and inflating API bills.\n\nMemori’s Advanced Augmentation pipeline completely bypasses this inefficiency by acting as an intelligent cognitive filter. Rather than retrieving raw text, it compresses chat logs into structured representations, including dense semantic triples and concise conversation-level summaries.  As a result, only high-signal, structured information is passed to the LLM, minimizing context overhead while preserving relevant context. \n\n| Table 2: Token Usage and Cost Efficiency |  |  |  |\n| ----- | :---: | :---: | :---: |\n| Method | Added Tokens to Context (mean) | Context Cost ($) | Context Footprint (%) |\n| **Memori** | **1,294** | **0.001035** | **4.97** |\n| Full-context | 26,031 | 0.020825 | 100.00 |\n| Mem0 | 1,764 | 0.001411 | 6.78 |\n| Zep | 3,911 | 0.003129 | 15.02 |\n\n<Note>  This table analyzes the operational efficiency of each method by measuring the absolute number of tokens added to the context and the resulting cost per query. Costs are computed based on current gpt-4.1-mini pricing: $0.8 per 1M tokens.</Note>\n\nMemori requires an average of only 1,294 tokens to ground each LLM response. This token footprint represents just 4.97% of the full conversational context, while achieving the 81.95% overall accuracy detailed in the previous section.\n\nWhen compared to competing memory frameworks, the operational advantages become even more pronounced:\n\n* **Compared to Zep (3,911 tokens):** Memori reduces the prompt size by roughly 67% per query, directly cutting API inference costs by the same margin, while simultaneously delivering a higher accuracy score (81.95% vs. 79.09%).  \n* **Compared to the Full-Context approach (26,031 tokens):** Passing the entire history is financially unsustainable for persistent agents, costing over 20 times more per turn than Memori. Furthermore, repeatedly injecting 26K+ tokens drastically increases the risk of \"lost in the middle\" hallucinations.\n\n## **Conclusion**\n\nFor LLM agents to scale in production, persistent memory must address two fundamental challenges: context degradation and rapidly increasing token costs. Memori approaches this not as a storage issue, but as a data structuring problem.\n\nThrough its Advanced Augmentation memory creation pipeline, Memori transforms noisy conversational logs into compact, high-signal representations, combining precise semantic triples with coherent conversation-level summaries. This dual representation enables accurate fact retrieval alongside strong temporal and contextual reasoning, without inflating the prompt with unnecessary tokens.\n\nOur evaluation on the LoCoMo benchmark demonstrates the effectiveness of this approach:\n\n* **High-Quality Reasoning:** Memori achieves state-of-the-art performance among retrieval-based systems, with particularly strong gains in temporal and single-hop reasoning \\- highlighting the impact of structured memory on reasoning fidelity.  \n* **Minimal Context Footprint:** Responses are grounded using a small fraction of the original conversation, showing that well-structured memory can replace large, unfiltered context without sacrificing accuracy.  \n* **Cost-Efficient Scaling:** By significantly reducing the number of tokens injected into the prompt, Memori directly lowers inference costs and enables sustainable deployment of long-running agents.\n\nThese results highlight a fundamental shift in memory design for LLM systems: performance is determined not by how much context is used, but by the quality of its structure. \n\nMemori eliminates the traditional tradeoff between reasoning quality and operational cost. By delivering accurate, cross-session recall with a compact context footprint, it provides a practical and scalable foundation for deploying persistent AI agents in real-world environments.\n"
  },
  {
    "path": "docs/memori-cloud/concepts/advanced-augmentation.mdx",
    "content": "---\ntitle: Advanced Augmentation\ndescription: How Memori's Advanced Augmentation engine extracts structured facts, preferences, and knowledge from your AI conversations — all managed in Memori Cloud.\n---\n\n# Advanced Augmentation\n\nAdvanced Augmentation is the AI engine inside Memori Cloud that turns raw conversations into structured, searchable memories. It runs asynchronously in the background to minimize impact on your response path.\n\n## What It Does\n\nWhen your application has a conversation through a Memori-wrapped LLM client, the augmentation engine:\n\n1. Reads the full conversation (user messages and AI responses)\n2. Identifies facts, preferences, skills, and attributes\n3. Extracts semantic triples (subject-predicate-object relationships)\n4. Generates vector embeddings for semantic search\n5. Stores everything in your managed memory space\n\nNo extra code required — just initialize Memori and set attribution.\n\n## How It Works\n\nThe augmentation flow is fully asynchronous and designed to avoid blocking your main request path.\n\n1. Your app makes an LLM call through the wrapped client\n2. Memori returns the response immediately\n3. In the background, the conversation is queued for processing\n4. The augmentation engine extracts structured memories\n5. Memories are stored in Memori Cloud for future recall\n\n<CodeGroup title=\"Advanced Augmentation\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\n# This returns immediately — no augmentation delay\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"I love hiking in the mountains.\"}\n    ]\n)\nprint(response.choices[0].message.content)\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'my_agent');\n\n// This returns immediately — no augmentation delay\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [\n    { role: 'user', content: 'I love hiking in the mountains.' },\n  ],\n});\nconsole.log(response.choices[0].message.content);\n```\n\n</CodeGroup>\n\n## Extraction Types\n\n| Type                   | What it captures                                        | Scope                                |\n| ---------------------- | ------------------------------------------------------- | ------------------------------------ |\n| **Facts**              | Objective information with vector embeddings            | Per entity — shared across processes |\n| **Preferences**        | User choices, opinions, and tastes                      | Per entity                           |\n| **Skills & Knowledge** | Abilities and expertise levels                          | Per entity                           |\n| **Attributes**         | Process-level information about what your agent handles | Per process                          |\n\n## Semantic Triples\n\nAdvanced Augmentation uses named-entity recognition to extract semantic triples (subject, predicate, object). These form the building blocks of the [Knowledge Graph](/docs/memori-cloud/concepts/knowledge-graph).\n\n**Example** — from _\"My favorite database is PostgreSQL and I use it with FastAPI\"_:\n\n| Subject | Predicate         | Object               |\n| ------- | ----------------- | -------------------- |\n| user    | favorite_database | PostgreSQL           |\n| user    | uses              | FastAPI              |\n| user    | uses_with         | PostgreSQL + FastAPI |\n\nMemori automatically deduplicates triples — if the same fact is mentioned multiple times, it increments the mention count and updates the timestamp.\n\n## Context Recall\n\nWhen a query is sent to an LLM through a wrapped client, Memori automatically:\n\n1. Intercepts the outbound LLM call\n2. Uses semantic search to find entity facts matching the query\n3. Ranks facts by vector similarity\n4. Injects the most relevant facts into the system prompt\n5. Forwards the enriched request to the LLM provider\n\n<Admonition type=\"important\" title=\"Attribution is Required\">\n  For Memori to provide all Advanced Augmentation capabilities, attribution must\n  be set before making LLM calls. Without attribution, Memori cannot create or\n  recall memories.\n</Admonition>\n"
  },
  {
    "path": "docs/memori-cloud/concepts/architecture.mdx",
    "content": "---\ntitle: Architecture\ndescription: Understand how Memori's Cloud platform is designed — from your app to Memori Cloud, with managed storage, augmentation, and recall.\n---\n\n# Architecture\n\nMemori Cloud is a managed memory platform for AI applications. Connect your LLM client, set attribution, and Memori handles the rest — storage, augmentation, knowledge graph construction, and recall.\n\n## System Overview\n\n![\"Memori Cloud System Overview\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-cloud-architecture-detail.webp)\n\n## Core Components\n\n### Your Application\n\nYour code and existing LLM client. It sends requests through the Memori SDK and receives model responses as usual.\n\n### Memori SDK\n\nThe integration layer between your app and Memori Cloud. It provides LLM wrappers, attribution, and the Recall API.\n\n### Memori Cloud\n\nThe managed backend that processes captured conversations and powers storage, augmentation, and recall services.\n\n### Managed Storage\n\nStores conversations, sessions, and facts for each attribution scope.\n\n### Advanced Augmentation\n\nProcesses raw conversation data into structured memory through fact extraction, embeddings, and knowledge graph construction.\n\n### Recall Engine\n\nSurfaces the right memories at the right time — semantic search over stored memory, intelligent ranking and decay, and seamless injection of relevant context into every LLM call so your AI stays contextually aware.\n\n## Configuration\n\nSetting up Memori requires only your API key and attribution:\n\n<CodeGroup title=\"Configuration\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom openai import OpenAI\n\n# Set MEMORI_API_KEY as an environment variable\n# export MEMORI_API_KEY=\"your-memori-api-key\"\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\n// Set MEMORI_API_KEY as an environment variable\n// export MEMORI_API_KEY=\"your-memori-api-key\"\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'my_agent');\n```\n\n</CodeGroup>\n\n## Data Flow\n\n1. **Conversation Capture** — Every LLM call through the wrapped client is captured and sent to Memori Cloud. Your app gets the response immediately.\n\n2. **Attribution Tracking** — Attribution links every conversation to a specific entity and process so memories are properly scoped and indexed.\n\n3. **Augmentation** — After a conversation completes, Memori Cloud processes it asynchronously — extracts facts, generates embeddings, and builds knowledge graph triples.\n\n4. **Recall** — On the next LLM call, Memori embeds the query, performs vector search across the entity's stored facts, and injects the most relevant memories into the context.\n"
  },
  {
    "path": "docs/memori-cloud/concepts/async-patterns.mdx",
    "content": "---\ntitle: Async Patterns\ndescription: Best practices for using Memori with async/await in Python and TypeScript.\n---\n\n# Async Patterns\n\nMemori works with async/await out of the box in both Python and TypeScript. In Python, use `AsyncOpenAI` or `AsyncAnthropic` instead of their sync counterparts — everything else stays the same. TypeScript is natively async.\n\n## When to Use Async\n\n| Scenario                 | Python | TypeScript | Why                         |\n| ------------------------ | ------ | ---------- | --------------------------- |\n| Web servers              | Yes    | Default    | Concurrent request handling |\n| Chatbots with many users | Yes    | Default    | Non-blocking I/O            |\n| CLI scripts              | No     | Default    | Sync is simpler in Python   |\n| Jupyter notebooks        | No     | —          | Event loop already running  |\n\n<Note>\n  TypeScript is natively async — all Memori SDK calls return Promises. No special async client or `asyncio` setup is needed.\n</Note>\n\n## Basic Async Setup\n\n<CodeGroup title=\"Async Setup\">\n\n```python {{ title: 'Python' }}\nimport asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\n\nclient = AsyncOpenAI()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"async_agent\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"I prefer async Python.\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'async_agent');\n\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [{ role: 'user', content: 'I prefer TypeScript.' }],\n});\nconsole.log(response.choices[0].message.content);\n```\n\n</CodeGroup>\n\n## Web Server Example\n\n<CodeGroup title=\"Web Server\">\n\n```python\nimport os\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\nfrom memori import Memori\nfrom openai import AsyncOpenAI\n\napp = FastAPI()\n\nclass ChatRequest(BaseModel):\n    message: str\n\n@app.post(\"/chat/{user_id}\")\nasync def chat(user_id: str, req: ChatRequest):\n    client = AsyncOpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n    mem = Memori().llm.register(client)\n    mem.attribution(entity_id=user_id, process_id=\"fastapi_async\")\n\n    response = await client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": req.message}]\n    )\n    return {\"response\": response.choices[0].message.content}\n```\n\n```typescript\nimport express from 'express';\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst app = express();\napp.use(express.json());\n\napp.post('/chat/:userId', async (req, res) => {\n  const client = new OpenAI();\n  const mem = new Memori().llm.register(client);\n  mem.attribution(req.params.userId, 'express_async');\n\n  const response = await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: req.body.message }],\n  });\n  res.json({ response: response.choices[0].message.content });\n});\n\napp.listen(3000);\n```\n\n</CodeGroup>\n"
  },
  {
    "path": "docs/memori-cloud/concepts/how-memory-works.mdx",
    "content": "---\ntitle: How Memori Works\ndescription: Understand the core concepts behind Memori — entities, processes, sessions, memory types, attribution, and how recall brings it all together.\n---\n\n# How Memori Works\n\nMemori gives your AI application long-term memory. Instead of forgetting everything after each conversation, your AI can remember facts, preferences, and context across sessions and across different applications.\n\n## Attribution\n\nEvery memory in Memori is tagged with three dimensions: **who** (entity), **what** (process), and **which conversation** (session).\n\n- **Entity (`entity_id`)** — The person, place, or thing generating memories. Typically a user ID (e.g., `\"user_alice\"`, `\"company_acme\"`).\n- **Process (`process_id`)** — The agent, program, or workflow creating memories (e.g., `\"support_bot\"`, `\"code_review_agent\"`).\n- **Session (`session_id`)** — Groups related LLM interactions into a conversation thread. Auto-generated as a UUID by default.\n\nThe combination of `entity_id` + `process_id` + `session_id` creates a unique memory scope — different users have isolated memories, the same user can have different context in different applications, and each conversation is tracked separately.\n\n<CodeGroup title=\"Attribution\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\n\n# Set attribution before any LLM calls\nmem.attribution(\n    entity_id=\"user_alice\",\n    process_id=\"support_bot\"\n)\n# session_id is auto-generated\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"I prefer dark mode.\"}\n    ]\n)\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\n\n// Set attribution before any LLM calls\nmem.attribution('user_alice', 'support_bot');\n// session ID is auto-generated\n\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [\n    { role: 'user', content: 'I prefer dark mode.' },\n  ],\n});\n```\n\n</CodeGroup>\n\n## Memory Types\n\nWhen you have a conversation through a Memori-wrapped LLM client, Advanced Augmentation extracts structured memories in the background:\n\n| Type                   | What it captures                          | Example                                         |\n| ---------------------- | ----------------------------------------- | ----------------------------------------------- |\n| **Facts**              | Objective information with embeddings     | \"User uses PostgreSQL for production databases\" |\n| **Preferences**        | Choices, opinions, and tastes             | \"Prefers concise answers\"                       |\n| **Skills & Knowledge** | Abilities and expertise levels            | \"Experienced with React (5 years)\"              |\n| **Attributes**         | Process-level information about the agent | \"Handles billing and subscription queries\"      |\n\n## How Recall Works\n\nRecall brings stored memories back into your AI conversations. There are two modes.\n\n### Automatic Recall (Default)\n\nOn every LLM call, Memori automatically:\n\n1. Intercepts the outbound request\n2. Uses semantic search to find relevant facts for the current entity\n3. Injects the most relevant memories into the system prompt\n4. Forwards the enriched request to the LLM\n\nNo extra code required — it happens transparently.\n\n### Manual Recall\n\nUse mem.recall() to retrieve memories explicitly — useful for building custom prompts, displaying memories in a UI, or debugging.\n\n<CodeGroup title=\"Manual Recall\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\n\nmem = Memori()\nmem.attribution(entity_id=\"user_alice\", process_id=\"support_bot\")\n\nfacts = mem.recall(\"coding preferences\", limit=5)\n\nfor fact in facts:\n    print(f\"Fact: {fact.content}\")\n    print(f\"Score: {fact.similarity:.4f}\")\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport { Memori } from '@memorilabs/memori';\n\nconst mem = new Memori();\nmem.attribution('user_alice', 'support_bot');\n\nconst facts = await mem.recall('coding preferences');\n\nfor (const fact of facts) {\n  console.log(`Fact: ${fact.content}`);\n  console.log(`Score: ${fact.score.toFixed(4)}`);\n}\n```\n\n</CodeGroup>\n\nEach returned fact includes `id`, `content`, `similarity` (0–1 relevance score), `rank_score`, and `date_created`.\n\n### Recall Configuration\n\nMemori uses semantic search (vector similarity) to find relevant facts. You can tune recall behavior with:\n\n| Option                                  | Default | Description                                        |\n| --------------------------------------- | ------- | -------------------------------------------------- |\n| `mem.config.recall_relevance_threshold` | `0.1`   | Minimum similarity score for a fact to be included |\n| `mem.config.recall_embeddings_limit`    | `1000`  | Maximum number of embeddings to compare against    |\n\n<CodeGroup title=\"Recall Configuration\">\n\n```python {{ title: 'Python' }}\n# Example: tune recall for broader or narrower results\nmem.config.recall_relevance_threshold = 0.05  # Lower = more results\nmem.config.recall_embeddings_limit = 500      # Reduce for lower memory usage\n```\n\n```typescript {{ title: 'TypeScript' }}\n// Example: tune recall for broader or narrower results\nmem.config.recallRelevanceThreshold = 0.05; // Lower = more results\n```\n\n</CodeGroup>\n\n## Memory Lifecycle\n\n![\"Memori Lifecycle\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-lifecycle.webp)\n\n1. **Conversation** — Your user talks to your AI through the wrapped LLM client\n2. **Capture** — Memori intercepts and stores the raw conversation\n3. **Augmentation** — Advanced Augmentation processes the conversation asynchronously, extracting structured memories\n4. **Extraction** — Facts, preferences, skills, and attributes are identified\n5. **Storage** — Extracted memories are stored in Memori Cloud with vector embeddings\n6. **Recall** — On the next LLM call, relevant memories are retrieved and injected into context\n"
  },
  {
    "path": "docs/memori-cloud/concepts/knowledge-graph.mdx",
    "content": "---\ntitle: Knowledge Graph\ndescription: How Memori automatically builds a knowledge graph from your AI conversations using semantic triples, and how to query it through the Recall API.\n---\n\n# Knowledge Graph\n\nMemori automatically builds a knowledge graph from your AI conversations. Each time Advanced Augmentation processes a conversation, it extracts structured relationships — semantic triples — and connects them into a graph. This powers richer recall and gives your AI deeper understanding of each user.\n\n## How It Works\n\n1. **Conversation captured** — Your user talks to your AI through the Memori-wrapped LLM client\n2. **Augmentation processes** — Memori Cloud analyzes the conversation in the background\n3. **NER extraction** — Named-entity recognition identifies key entities and relationships\n4. **Triple creation** — Relationships are expressed as subject-predicate-object triples\n5. **Graph storage** — Triples are stored and deduplicated in the knowledge graph\n6. **Recall ready** — The graph is available for semantic search on subsequent LLM calls\n\n![Memori Cloud Graph](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/entities-knowledge-graph.webp)\n\n## Semantic Triples\n\nEvery fact in the knowledge graph is a semantic triple — a three-part statement: **[Subject]** **[Predicate]** **[Object]**.\n\n- \"Alice\" \"prefers\" \"dark mode\"\n- \"PostgreSQL\" \"is\" \"a relational database\"\n- \"The project\" \"uses\" \"FastAPI\"\n\n### Example Extraction\n\nFrom _\"My favorite database is PostgreSQL and I use it with FastAPI for our REST APIs. I've been using Python for about 8 years\"_:\n\n| Subject | Predicate         | Object               |\n| ------- | ----------------- | -------------------- |\n| user    | favorite_database | PostgreSQL           |\n| user    | uses              | FastAPI              |\n| user    | uses_for          | REST APIs            |\n| user    | uses_with         | PostgreSQL + FastAPI |\n| user    | experience_years  | Python (8 years)     |\n\nOver time, as more conversations happen, the graph grows richer. Memori connects new facts to existing ones, building a comprehensive picture of each entity.\n\n## Visualizing the Graph\n\nThe Memori Playground at [app.memorilabs.ai](https://app.memorilabs.ai) includes a **Memory Graph Viewer** that shows:\n\n| Element            | What it shows                                  |\n| ------------------ | ---------------------------------------------- |\n| **Nodes**          | Subjects and objects from semantic triples     |\n| **Edges**          | Predicates (relationships) between nodes       |\n| **Mention counts** | How often a fact was discussed across sessions |\n| **Timestamps**     | When facts were first and last seen            |\n\n## Scope\n\nThe knowledge graph follows the same scoping rules as other memory types:\n\n| Aspect         | Scope                                                           |\n| -------------- | --------------------------------------------------------------- |\n| **Triples**    | Per entity — shared across all processes                        |\n| **Visibility** | All processes for an entity can see and use the graph           |\n| **Growth**     | Conversations from any process contribute to the entity's graph |\n\nIf Alice tells your support bot about PostgreSQL, your code assistant also knows she uses PostgreSQL.\n\n## Querying the Graph\n\nThe knowledge graph is automatically used during recall. When you call `mem.recall()` or make an LLM call through a wrapped client, Memori searches across both extracted facts and the knowledge graph to find the most relevant context.\n"
  },
  {
    "path": "docs/memori-cloud/concepts/multi-user-support.mdx",
    "content": "---\ntitle: Multi-User Support\ndescription: How Memori isolates memories across users, applications, and sessions so each user gets a personalized experience.\n---\n\n# Multi-User Support\n\nMemori provides built-in multi-user and multi-process isolation through its attribution system. Each combination of entity, process, and session creates an isolated memory space — user A never sees user B's memories, and your support bot has different context than your sales bot.\n\n## Isolation Model\n\n![\"Memori - Multi User Support\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-multi-user-support.webp)\n\n## What's Shared vs Isolated\n\n| Data                | Scope                                      |\n| ------------------- | ------------------------------------------ |\n| **Facts**           | Per entity — shared across all processes   |\n| **Preferences**     | Per entity                                 |\n| **Skills**          | Per entity                                 |\n| **Attributes**      | Per process                                |\n| **Conversations**   | Per entity + process + session             |\n| **Sessions**        | Per entity + process (auto-generated UUID) |\n| **Knowledge Graph** | Per entity                                 |\n\n## Examples\n\n<CodeGroup title=\"Per-User Isolation\">\n\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\n\n# User A's conversations\nmem.attribution(entity_id=\"user_alice\", process_id=\"support_bot\")\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"I prefer dark mode\"}]\n)\n\n# User B's conversations — completely isolated\nmem.attribution(entity_id=\"user_bob\", process_id=\"support_bot\")\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"What are my preferences?\"}]\n)\n# Bob will NOT see Alice's preferences\n```\n```typescript\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\n\n// User A's conversations\nmem.attribution('user_alice', 'support_bot');\nawait client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [{ role: 'user', content: 'I prefer dark mode' }],\n});\n\n// User B's conversations — completely isolated\nmem.attribution('user_bob', 'support_bot');\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [{ role: 'user', content: 'What are my preferences?' }],\n});\n// Bob will NOT see Alice's preferences\n```\n</CodeGroup>\n\n<CodeGroup title=\"Multi-Process\">\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\n\n# Same user, different processes\nmem.attribution(entity_id=\"user_alice\", process_id=\"support_bot\")\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"I use PostgreSQL for my databases\"}\n    ]\n)\n\n# Switch to a different process for the same user\nmem.attribution(entity_id=\"user_alice\", process_id=\"sales_bot\")\n# The sales bot can recall Alice's facts (like \"uses PostgreSQL\")\n# because facts are shared across processes for the same entity.\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"What databases do I use?\"}\n    ]\n)\n```\n\n```typescript\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\n\n// Same user, different processes\nmem.attribution('user_alice', 'support_bot');\nawait client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [\n    { role: 'user', content: 'I use PostgreSQL for my databases' },\n  ],\n});\n\n// Switch to a different process for the same user\nmem.attribution('user_alice', 'sales_bot');\n// The sales bot can recall Alice's facts (like \"uses PostgreSQL\")\n// because facts are shared across processes for the same entity.\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [\n    { role: 'user', content: 'What databases do I use?' },\n  ],\n});\n```\n</CodeGroup>\n\n<CodeGroup title=\"Session Management\">\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_alice\", process_id=\"support_bot\")\n\n# Get the current session ID\ncurrent_session = mem.config.session_id\n\n# Start a new conversation group\nmem.new_session()\n\n# Or restore a previous session\nmem.set_session(current_session)\n```\n\n```typescript\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_alice', 'support_bot');\n\n// Get the current session ID\nconst currentSession = mem.session.id;\n\n// Start a new conversation group\nmem.resetSession();\n\n// Or restore a previous session\nmem.setSession(currentSession);\n```\n</CodeGroup>\n\n## Common Patterns\n\n### Web Application\n\nSet the entity ID from the authenticated user. Works with Flask, FastAPI, Django, or any web framework.\n\n<CodeGroup title=\"Common Patterns\">\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\ndef handle_chat(user_id: str, message: str):\n    client = OpenAI()\n    mem = Memori().llm.register(client)\n    mem.attribution(entity_id=user_id, process_id=\"web_assistant\")\n\n    response = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": message}]\n    )\n    return response.choices[0].message.content\n```\n\n```typescript\nimport express from 'express';\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst app = express();\napp.use(express.json());\n\napp.post('/chat/:userId', async (req, res) => {\n  const client = new OpenAI();\n  const mem = new Memori().llm.register(client);\n  mem.attribution(req.params.userId, 'web_assistant');\n\n  const response = await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: req.body.message }],\n  });\n  res.json({ response: response.choices[0].message.content });\n});\n```\n</CodeGroup>\n\n### Multi-Agent System\n\nGive each agent a unique process ID. Facts are shared across agents for the same entity, but each agent maintains its own conversation history.\n\n<CodeGroup title=\"Multi-Agent System\">\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\ndef create_agent(user_id: str, agent_name: str):\n    client = OpenAI()\n    mem = Memori().llm.register(client)\n    mem.attribution(entity_id=user_id, process_id=agent_name)\n    return client\n\n# Three agents, one user, shared facts\nsupport = create_agent(\"user_alice\", \"support_agent\")\nsales = create_agent(\"user_alice\", \"sales_agent\")\nonboard = create_agent(\"user_alice\", \"onboarding_agent\")\n```\n\n```typescript\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nfunction createAgent(userId: string, agentName: string) {\n  const client = new OpenAI();\n  const mem = new Memori().llm.register(client);\n  mem.attribution(userId, agentName);\n  return client;\n}\n\n// Three agents, one user, shared facts\nconst support = createAgent('user_alice', 'support_agent');\nconst sales = createAgent('user_alice', 'sales_agent');\nconst onboard = createAgent('user_alice', 'onboarding_agent');\n```\n</CodeGroup>\n"
  },
  {
    "path": "docs/memori-cloud/dashboard/analytics.mdx",
    "content": "---\ntitle: Analytics\ndescription: Understand memory volume, retrieval performance, usage quotas, and top subjects in Memori Cloud analytics.\n---\n\n# Analytics\n\nUse **Analytics** in [app.memorilabs.ai](https://app.memorilabs.ai) to monitor memory creation, recall activity, traffic trends, and quota usage.\n\nUse the date selector (for example **Last 7 days**) to scope metrics, and check **Last refreshed** for data freshness.\n\n**What Each Data Point Means:**\n\n| Data point                        | Meaning                                                                                    |\n| --------------------------------- | ------------------------------------------------------------------------------------------ |\n| **Unique memories**               | Total number of unique memory records in the selected time range.                          |\n| **Entities**                      | Number of unique extracted entities attached to memories in the selected range.             |\n| **Recalls**                       | Number of memory retrieval events served in the selected range.                            |\n| **Cache hit rate**                | Percentage of recall requests served from cache instead of a full retrieval path.          |\n| **Total conversations**           | Number of conversation threads processed by Memori.                                        |\n| **Total sessions**                | Number of distinct sessions in the selected range.                                         |\n| **Trend chart**                   | Time-series trend for the active activity metric across the selected period.               |\n| **Monthly quota resets on ...**   | Date when monthly limits reset for your organization.                                      |\n| **Memories created** (usage bar)  | Current month created-memory usage vs monthly plan limit.                                  |\n| **Memories recalled** (usage bar) | Current month recall usage vs monthly plan limit.                                          |\n| **Top subjects**                  | Ranked subjects/entities by memory volume, with entity type tags and relative volume bars. |\n\n![Analytics page with KPI cards, activity chart, monthly usage, and top subjects](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/analytics-overview.webp)\n\n## Read the Page in Order\n\n1. Start with the top KPI cards to understand overall volume and retrieval load.\n2. Check **Activity and performance** for traffic trends and directional changes (up/down percentages).\n3. Review **Monthly usage** to confirm distance from quota limits.\n4. Use **Top subjects** to see which entities dominate memory creation.\n\n## Interpreting Directional Changes\n\n- Green upward changes indicate growth versus the comparison period.\n- Red downward changes indicate decreases versus the comparison period.\n- Combine change percentages with absolute counts to avoid overreacting to small-volume shifts.\n"
  },
  {
    "path": "docs/memori-cloud/dashboard/api-keys.mdx",
    "content": "---\ntitle: API Keys\ndescription: Create and manage Memori API keys from the dashboard.\n---\n\n# API Keys\n\nUse the **API keys** page in [app.memorilabs.ai](https://app.memorilabs.ai)\nto create and manage keys for your organization.\n\n## Overview\n\nThe table includes:\n\n- **Name**\n- **Key** (masked)\n- **Created at**\n- **Last used**\n\nIf no keys are connected yet, the table shows **No connected APIs.**\n\n## Create a New API Key\n\n1. Open **API keys** in the dashboard sidebar.\n2. Click **Create API key**.\n3. In **New API key**, enter a value in **Name**.\n4. Click **Create key**.\n\n![API keys page with empty table state and New API key modal](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/new-api-key-modal.webp)\n\n## Copy the Key (Shown Once)\n\nAfter creating a key, the **Your API key** modal appears with the full key\nvalue and a **Copy** button.\n\n<Admonition type=\"warning\" title=\"Shown Once\">\n  The full key value is shown only once. Copy it and store it securely before\n  clicking **Continue**.\n</Admonition>\n\n1. Click **Copy**.\n2. Click **Continue** to return to the API keys table.\n\n![API keys page with table row and Your API key modal for one-time copy](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/your-api-key-modal.webp)\n\n## Security Best Practices\n\n<Admonition type=\"danger\" title=\"Never Commit API Keys\">\n  Never commit API keys to version control. Treat API keys like passwords.\n</Admonition>\n\n- Store keys in environment variables or a secure secrets manager\n- Rotate keys immediately if you suspect exposure\n- Use separate keys for development, staging, and production\n"
  },
  {
    "path": "docs/memori-cloud/dashboard/memories.mdx",
    "content": "---\ntitle: Memories\ndescription: Explore memories and entities in table and graph views, then drill into details, associations, and retrieval behavior.\n---\n\n# Memories\n\nUse **Memories** in [app.memorilabs.ai](https://app.memorilabs.ai) to inspect all memories created by your configured applications and agents.\n\nThe page supports two ways to explore data:\n\n- **Table** view for row-based inspection\n- **Knowledge graph** view for relationship-first exploration\n\n## Start in Table View\n\nThe default table flow uses two tabs:\n\n- **Memories** for individual memory rows\n- **Subjects** for extracted entities/subjects and their counts\n\nUse **Semantic search** to find relevant rows quickly, then open the view menu to switch between **Table** and **Knowledge graph**.\n\n### Memories tab\n\n- **Summary**: The memory text captured from conversations\n- **Entity ID**: The associated entity for that memory\n- **Recalls**: Number of times this memory was retrieved\n- **# of mentions**: How often this memory was mentioned in context\n- **Last retrieved**: Most recent retrieval time\n- **Created at**: When the memory was first created\n\n![Memories table view with semantic search and table or knowledge graph switcher](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memories-overview-table.webp)\n\n### Subjects tab\n\n- **Subject**: Extracted subject/entity value\n- **Type**: Entity class (for example `GPE`, `PRODUCT`, `ORGANIZATION`, `MONEY`)\n- **Memory count**: Number of memories linked to that subject\n- **Created at**: Subject creation date\n\nThe **All types** filter helps narrow the table by subject type.\n\n![Subjects table view with all types filter and subject memory counts](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memories-detail-drawer.webp)\n\n## Open Memory Details\n\n1. In **Memories**, click any row.\n2. Review the **Memory details** panel.\n3. Use the embedded **Subjects** graph and list to inspect associations.\n\nThe **Details** section includes:\n\n- **Memory ID**\n- **Entity ID**\n- **Importance score**\n- **Created at**\n- **Last retrieved**\n- **Recalls**\n- **# of mentions**\n\n![Memory details drawer showing details fields and associated subjects graph](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memories-subjects-table.webp)\n\n## Switch to Knowledge Graph\n\n1. Open the view switcher and select **Knowledge graph**.\n2. Use the **Entities** tab to explore nodes and relationships.\n3. Filter with **Search memories** and time range (for example **All time**).\n\n![Entities knowledge graph view with search and time filter](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/entities-knowledge-graph.webp)\n\n## Recommended Exploration Workflow\n\n1. Start in **Memories** table to validate recency and retrieval metrics.\n2. Open **Memory details** to verify entity associations and subject links.\n3. Switch to **Knowledge graph** to follow cross-entity relationships.\n4. Open an entity and trace back through **Associated memories**.\n"
  },
  {
    "path": "docs/memori-cloud/dashboard/overview.mdx",
    "content": "---\ntitle: Dashboard Overview\ndescription: An overview of the Memori Cloud dashboard at app.memorilabs.ai for managing API keys, testing memories, and monitoring usage.\n---\n\n# Dashboard Overview\n\nThe Memori Cloud dashboard at [app.memorilabs.ai](https://app.memorilabs.ai) is where you manage API keys, test memory in real-time, and monitor usage.\n\n## Dashboard Sections\n\n### API Keys\n\nCreate and manage your Memori API keys. The free plan unlocks 5,000 memories created and 15,000 recalled per\nmonth. [Manage API Keys](/docs/memori-cloud/dashboard/api-keys).\n\n### Playground\n\nChat with an AI and watch memories get extracted in real-time. The fastest way\nto understand how Memori works.\n[Try the Playground](/docs/memori-cloud/dashboard/playground).\n\n## Memory Usage\n\n| Plan                    | Memories Created / month | Memories Recalled / month | Requirements                                               |\n| ----------------------- | ------------------------ | ------------------------- | ---------------------------------------------------------- |\n| **Free (with API key)** | 5,000                    | 15,000                    | [Sign up and create an API key](https://app.memorilabs.ai) |\n| **Starter**             | 25,000                   | 100,000                   | [Sign up and subscribe](/pricing)                          |\n| **Pro**                 | 150,000                  | 500,000                   | [Sign up and subscribe](/pricing)                          |\n| **Enterprise**          | Custom                   | Custom                    | [Contact us](/pricing)                                     |\n\n## Getting Started\n\n1. **Sign up** — Go to [app.memorilabs.ai](https://app.memorilabs.ai) and create an account\n2. **Create an API key** — Navigate to API Keys and generate your first key\n3. **Try the Playground** — Chat with an AI and see memories being extracted live\n4. **Integrate** — Copy your API key into your application code\n\n```python\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my-agent\")\n```\n"
  },
  {
    "path": "docs/memori-cloud/dashboard/playground.mdx",
    "content": "---\ntitle: Playground\ndescription: Use Playground to validate memory extraction end to end with chat, extracted memories, and the memory graph.\n---\n\n# Playground\n\nUse **Playground** in [app.memorilabs.ai](https://app.memorilabs.ai) to test how conversation turns become extracted memories and relationship graphs.\n\n## Playground Layout\n\nThe screen is split into three working areas:\n\n- Left navigation: **Memories**, **API keys**, **Playground**, **Documentation**, **Settings**\n- Center panel: **Playground** conversation and composer\n- Right column: **Memory usage**, **Extracted memories** with **Clean**, and **Memory graph** with expand\n\nThis layout keeps input, extracted output, and relationship visualization on one page so you can validate memory behavior quickly.\n\n![Playground overview with conversation, memory usage, extracted memories, and memory graph](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/playground-memory-graph-expanded.webp)\n\n## Core Workflow\n\n1. Chat in **Playground** with user details or preferences.\n2. Watch new records appear in **Extracted memories**.\n3. Verify those same facts and links in **Memory graph**.\n4. Use the graph expand control for deeper inspection when needed.\n\n## Memory Graph\n\n**Memory graph** is a relationship view over the same memory set shown in **Extracted memories**.\n\n- Nodes represent subjects and entities (for example: `User`, `Emma`, `Queens`, `NYC Juniors`).\n- Edges show relation text (for example: `has name`, `lives in`, `plays as`).\n- Use the expand icon to open a larger graph canvas for graph-first review.\n\n![Memory graph card in Playground](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/playground-overview.webp)\n\n## Extracted Memories and Persistence\n\nThe **Extracted memories** list shows normalized memory statements such as:\n\n- `Northeastern University located in Boston, Massachusetts`\n- `User plays as libero`\n- `User played club volleyball growing up for NYC Juniors`\n\nThese memories are persisted as records in the database and are generated asynchronously through advanced augmentation after conversation turns are processed.\n\n![Extracted memories panel with Clean action](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/playground-extracted-memories.webp)\n"
  },
  {
    "path": "docs/memori-cloud/getting-started/installation.mdx",
    "content": "---\ntitle: Installation\ndescription: Install Memori and set up your API key for the Memori Cloud.\n---\n\n# Installation\n\nGet Memori installed and connected to Memori Cloud in a few steps.\n\n## Python\n\n### Install Memori\n\n<CodeGroup title=\"Install Memori (Python)\">\n\n```bash {{ title: 'pip' }}\npip install memori\n```\n\n```bash {{ title: 'poetry' }}\npoetry add memori\n```\n\n```bash {{ title: 'uv' }}\nuv add memori\n```\n\n</CodeGroup>\n\n### Install Your LLM Provider\n\nInstall the SDK for your preferred LLM provider:\n\n<CodeGroup title=\"LLM Provider SDKs (Python)\">\n\n```bash {{ title: 'OpenAI' }}\npip install openai\n```\n\n```bash {{ title: 'Anthropic' }}\npip install anthropic\n```\n\n```bash {{ title: 'Google Gemini' }}\npip install google-genai\n```\n\n```bash {{ title: 'xAI Grok' }}\npip install openai\n```\n\n```bash {{ title: 'Nebius AI Studio' }}\npip install openai\n```\n\n```bash {{ title: 'AWS Bedrock' }}\npip install langchain-aws\n```\n\n```bash {{ title: 'LangChain' }}\npip install langchain-openai\n```\n\n```bash {{ title: 'Pydantic AI' }}\npip install pydantic-ai\n```\n\n</CodeGroup>\n\n## TypeScript\n\n<Note>\n  The TypeScript SDK is for Memori Cloud only. For self-hosted setups, use the [Python SDK](/docs/memori-byodb/getting-started/installation).\n</Note>\n\n### Install Memori\n\n<CodeGroup title=\"Install Memori (TypeScript)\">\n\n```bash {{ title: 'npm' }}\nnpm install @memorilabs/memori\n```\n\n```bash {{ title: 'yarn' }}\nyarn add @memorilabs/memori\n```\n\n```bash {{ title: 'pnpm' }}\npnpm add @memorilabs/memori\n```\n\n</CodeGroup>\n\n### Install Your LLM Provider\n\n<CodeGroup title=\"LLM Provider SDKs (TypeScript)\">\n\n```bash {{ title: 'OpenAI' }}\nnpm install openai\n```\n\n```bash {{ title: 'Anthropic' }}\nnpm install @anthropic-ai/sdk\n```\n\n```bash {{ title: 'Google Gemini' }}\nnpm install @google/genai\n```\n\n</CodeGroup>\n\n## Set Your API Key\n\nSign up at [app.memorilabs.ai](https://app.memorilabs.ai) to get your Memori API key. The SDK reads it automatically from the environment:\n\n<Properties>\n  <Property name=\"MEMORI_API_KEY\" type=\"string\" required>\n    Your Memori API key from [app.memorilabs.ai](https://app.memorilabs.ai)\n    connects your application to Memori Cloud.\n  </Property>\n</Properties>\n\nSet this as an environment variable. The Memori SDK reads it automatically on initialization.\n\n<CodeGroup title=\"API Key Setup\">\n\n```bash {{ title: 'Environment Variable' }}\nexport MEMORI_API_KEY=\"your-memori-api-key\"\n```\n\n```bash {{ title: '.env File' }}\n# Create a .env file in your project root\nMEMORI_API_KEY=your-memori-api-key\nOPENAI_API_KEY=your-openai-api-key\n```\n\n</CodeGroup>\n\n## Set Your LLM Provider Key\n\nYou'll also need an API key for your LLM provider:\n\n```bash\n# OpenAI\nexport OPENAI_API_KEY=\"your-openai-key\"\n\n# Anthropic\nexport ANTHROPIC_API_KEY=\"your-anthropic-key\"\n\n# Google Gemini\nexport GOOGLE_API_KEY=\"your-google-key\"\n```\n\n## Verify Installation\n\n<CodeGroup title=\"Verify Installation\">\n\n```bash {{ title: 'Python' }}\npip show memori\n```\n\n```bash {{ title: 'TypeScript' }}\nnpx tsx -e \"import { Memori } from '@memorilabs/memori'; console.log('Memori installed successfully')\"\n```\n\n</CodeGroup>\n"
  },
  {
    "path": "docs/memori-cloud/getting-started/python-quickstart.mdx",
    "content": "---\ntitle: Python SDK Quickstart\ndescription: Get started with Memori Cloud in under 3 minutes.\n---\n\n# Python SDK Quickstart\n\nGet started with Memori in under 3 minutes using Python. No database setup required — just your Memori API key and your favorite LLM provider.\n\n<Note>\n  In this example, we'll use Memori with OpenAI. Check out our [Integration\n  guides](/docs/memori-cloud/llm/overview) for other LLM providers and\n  frameworks.\n</Note>\n\n\n## Prerequisites\n\n- Python 3.10 or higher\n- An OpenAI API key\n- A Memori API key from [app.memorilabs.ai](https://app.memorilabs.ai)\n\n## Step 1: Install Libraries\n\nInstall Memori and the OpenAI SDK:\n\n<CodeGroup title=\"Install Memori\">\n\n```bash {{ title: 'pip' }}\npip install memori openai\n```\n\n```bash {{ title: 'poetry' }}\npoetry add memori openai\n```\n\n```bash {{ title: 'uv' }}\nuv add memori openai\n```\n\n</CodeGroup>\n\n## Step 2: Set Environment Variables\n\nSet your API keys as environment variables:\n\n```bash\nexport MEMORI_API_KEY=\"your-memori-api-key\"\nexport OPENAI_API_KEY=\"your-openai-api-key\"\n```\n\n## Step 3: Run Your First Memori Application\n\nCreate a new Python file `quickstart.py` and add the following code:\n\n### Setup & Configuration\n\nImport libraries and initialize Memori with your API key and OpenAI client.\n\n- Memori reads your `MEMORI_API_KEY` from the environment automatically\n- `llm.register()` wraps your LLM client for automatic memory capture\n- `attribution()` links memories to a specific user and process\n\n```python\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"test-ai-agent\")\n```\n\n### First Conversation\n\nTell the LLM a fact about yourself. Memori automatically captures the\nconversation and processes it through Advanced Augmentation.\n\n```python\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"My favorite color is blue.\"}\n    ]\n)\nprint(response.choices[0].message.content + \"\\n\")\n```\n\n### Memory Recall\n\nCreate a completely new client and Memori instance — no prior context\ncarried over. Memori automatically injects relevant facts via semantic\nsearch, so the second response should correctly recall your favorite\ncolor. This verifies recall from stored memories, not prior in-memory message history.\n\n```python\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"test-ai-agent\")\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"What's my favorite color?\"}\n    ]\n)\nprint(response.choices[0].message.content + \"\\n\")\n```\n\n## Step 4: Run the Application\n\nExecute your Python file:\n\n```bash\npython quickstart.py\n```\n\nYou should see the AI respond to both questions, with the second response correctly recalling that your favorite color is blue!\n\n## Step 5: Check the Dashboard\n\nVisit [app.memorilabs.ai](https://app.memorilabs.ai) to see your memory usage and try the Graph Explorer to interact with your memories visually.\n"
  },
  {
    "path": "docs/memori-cloud/getting-started/typescript-quickstart.mdx",
    "content": "---\ntitle: Typescript SDK Quickstart\ndescription: Get started with Memori Cloud and the TypeScript SDK in under 3 minutes.\n---\n\n# Typescript SDK Quickstart\n\nGet started with Memori in under 3 minutes using TypeScript. No database setup required — just your Memori API key and your favorite LLM provider.\n\n<Note>\n  In this example, we'll use Memori with OpenAI. Check out our [Integration\n  guides](/docs/memori-cloud/llm/overview) for other LLM providers.\n</Note>\n\n## Prerequisites\n\n- Node.js 18 or higher\n- An OpenAI API key\n- A Memori API key from [app.memorilabs.ai](https://app.memorilabs.ai)\n\n## Step 1: Install Libraries\n\nInstall Memori and the OpenAI SDK:\n\n<CodeGroup title=\"Install Memori\">\n\n```bash {{ title: 'npm' }}\nnpm install @memorilabs/memori openai\n```\n\n```bash {{ title: 'yarn' }}\nyarn add @memorilabs/memori openai\n```\n\n```bash {{ title: 'pnpm' }}\npnpm add @memorilabs/memori openai\n```\n\n</CodeGroup>\n\n## Step 2: Set Environment Variables\n\nSet your API keys as environment variables:\n\n```bash\nexport MEMORI_API_KEY=\"your-memori-api-key\"\nexport OPENAI_API_KEY=\"your-openai-api-key\"\n```\n\n## Step 3: Run Your First Memori Application\n\nCreate a new file `quickstart.ts` and add the following code:\n\n### Setup & Configuration\n\nImport libraries and initialize Memori with your API key and OpenAI client.\n\n- Memori reads your `MEMORI_API_KEY` from the environment automatically\n- `llm.register()` wraps your LLM client for automatic memory capture\n- `attribution()` links memories to a specific user and process\n\n```typescript\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'test-ai-agent');\n```\n\n### First Conversation\n\nTell the LLM a fact about yourself. Memori automatically captures the\nconversation and processes it through Advanced Augmentation.\n\n```typescript\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [\n    { role: 'user', content: 'My favorite color is blue.' }\n  ],\n});\nconsole.log(response.choices[0].message.content + '\\n');\n```\n\n### Memory Recall\n\nCreate a completely new client and Memori instance — no prior context\ncarried over. Memori automatically injects relevant facts via semantic\nsearch, so the second response should correctly recall your favorite\ncolor. This verifies recall from stored memories, not prior in-memory message history.\n\n```typescript\nconst client2 = new OpenAI();\n\nconst mem2 = new Memori().llm.register(client2);\nmem2.attribution('user_123', 'test-ai-agent');\n\nconst response2 = await client2.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [\n    { role: 'user', content: \"What's my favorite color?\" }\n  ],\n});\nconsole.log(response2.choices[0].message.content + '\\n');\n```\n\n## Step 4: Run the Application\n\nExecute your TypeScript file:\n\n```bash\nnpx tsx quickstart.ts\n```\n\nYou should see the AI respond to both questions, with the second response correctly recalling that your favorite color is blue!\n\n## Step 5: Check the Dashboard\n\nVisit [app.memorilabs.ai](https://app.memorilabs.ai) to see your memory usage and try the Graph Explorer to interact with your memories visually.\n"
  },
  {
    "path": "docs/memori-cloud/getting-started/use-cases.mdx",
    "content": "---\ntitle: Use Cases\ndescription: Common use cases and applications for Memori.\n---\n\n# Use Cases\n\nMemori is designed for any application where AI agents need to remember context across conversations. Here are the most common use cases.\n\n## Customer Support Chatbots\n\nBuild support bots that remember customer history, preferences, and past issues. No more \"Can you repeat your account number?\" — Memori recalls context automatically.\n\n**Benefits:**\n\n- Remember customer preferences and history\n- Recall previous support tickets and resolutions\n- Personalize responses based on past interactions\n- Track issues across multiple sessions\n\n<CodeGroup title=\"Customer Support\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\n\n# Each customer gets their own memory space\nmem.attribution(\n    entity_id=\"customer_456\",\n    process_id=\"support_bot\"\n)\n\n# Memori automatically recalls relevant context\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\n        \"role\": \"user\",\n        \"content\": \"I'm having that issue again\"\n    }]\n)\n# Memori injects: \"Customer previously reported\n# login timeout issues on 2024-01-15\"\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\n\n// Each customer gets their own memory space\nmem.attribution('customer_456', 'support_bot');\n\n// Memori automatically recalls relevant context\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [{\n    role: 'user',\n    content: \"I'm having that issue again\",\n  }],\n});\n// Memori injects: \"Customer previously reported\n// login timeout issues on 2024-01-15\"\n```\n\n</CodeGroup>\n\n## Personalized AI Assistants\n\nCreate AI assistants that learn and adapt to each user over time. Memori builds a profile of preferences, skills, and context that makes every interaction more relevant.\n\n**Benefits:**\n\n- Learn coding preferences and tech stack\n- Remember project context across sessions\n- Adapt communication style to user preferences\n- Build long-term user profiles automatically\n\n<CodeGroup title=\"Personalized AI\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom anthropic import Anthropic\n\nclient = Anthropic()\nmem = Memori().llm.register(client)\n\nmem.attribution(\n    entity_id=\"developer_789\",\n    process_id=\"code_assistant\"\n)\n\n# Over time, Memori learns:\n# - \"Uses Python 3.12 with FastAPI\"\n# - \"Prefers type hints and dataclasses\"\n# - \"Works on e-commerce platform\"\nresponse = client.messages.create(\n    model=\"claude-sonnet-4-5-20250929\",\n    max_tokens=1024,\n    messages=[{\n        \"role\": \"user\",\n        \"content\": \"How should I structure this endpoint?\"\n    }]\n)\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport Anthropic from '@anthropic-ai/sdk';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new Anthropic();\nconst mem = new Memori().llm.register(client);\n\nmem.attribution('developer_789', 'code_assistant');\n\n// Over time, Memori learns:\n// - \"Uses TypeScript with Express\"\n// - \"Prefers strict types and interfaces\"\n// - \"Works on e-commerce platform\"\nconst response = await client.messages.create({\n  model: 'claude-sonnet-4-5-20250929',\n  max_tokens: 1024,\n  messages: [{\n    role: 'user',\n    content: 'How should I structure this endpoint?',\n  }],\n});\n```\n\n</CodeGroup>\n\n## Multi-Agent Workflows\n\nCoordinate multiple AI agents that share context through Memori. Each agent contributes to a shared memory space while maintaining its own process identity and conversation history.\n\n**Benefits:**\n\n- Share context between specialized agents\n- Track which agent contributed what information\n- Maintain conversation continuity across handoffs\n- Build collective knowledge graphs\n\n<CodeGroup title=\"Multi-Agent\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\nmem = Memori().llm.register(client)\n\n# Research agent gathers information\nmem.attribution(\n    entity_id=\"project_alpha\",\n    process_id=\"research_agent\"\n)\nclient.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\n        \"role\": \"user\",\n        \"content\": \"Research competitor pricing\"\n    }]\n)\n\n# Analysis agent recalls research findings\nmem.attribution(\n    entity_id=\"project_alpha\",\n    process_id=\"analysis_agent\"\n)\n# Memori shares context across agents\n# for the same entity\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\n\n// Research agent gathers information\nmem.attribution('project_alpha', 'research_agent');\nawait client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [{\n    role: 'user',\n    content: 'Research competitor pricing',\n  }],\n});\n\n// Analysis agent recalls research findings\nmem.attribution('project_alpha', 'analysis_agent');\n// Memori shares context across agents\n// for the same entity\n```\n\n</CodeGroup>\n\n<Note>\n  For runnable versions of these examples and more, see the [examples\n  folder](https://github.com/MemoriLabs/Memori/tree/main/examples) on GitHub.\n</Note>\n"
  },
  {
    "path": "docs/memori-cloud/index.mdx",
    "content": "---\ntitle: Introduction\ndescription: Memori gives your AI agents structured, persistent memory — no database setup required.\n---\n\n# What is Memori?\n\n__Memori__ is a memory layer for LLM applications, agents, and copilots. It continuously captures interactions, extracts structured knowledge, and intelligently ranks, decays, and retrieves the relevant memories. So your AI remembers the right things at the right time across every session.\n\n## Why Memori Cloud?\n\nWith the Memori Cloud platform: [app.memorilabs.ai](https://app.memorilabs.ai), you skip all database configuration. Sign up, get a Memori API key, and start building AI agents with memory in minutes.\n\n### LLM Provider Support\n\nOpenAI, Anthropic, Gemini, and Grok (xAI) via direct SDK wrappers. Bedrock is\nsupported via LangChain `ChatBedrock`. OpenAI-compatible providers (Nebius,\nDeepseek, NVIDIA NIM, Azure OpenAI, and more) work through OpenAI's `base_url`\nparameter. Supports sync, async, streamed, and unstreamed modes, plus LangChain\n, Agno, and Pydantic AI.\n\n### Zero Configuration\n\nNo database setup needed. Connect your LLM client with an API key and start\nbuilding memories immediately.\n\n### Framework Integration\n\nNative support for LangChain, Agno, and Pydantic AI with seamless integration\ninto your existing workflows.\n\n### Advanced Augmentation\n\nBackground AI processing extracts facts, preferences, and relationships from\nyour conversations automatically.\n\n### Intelligent Recall\n\nIntelligent Recall surfaces the right memories at the right time. Memories are\nranked by relevance and importance, with intelligent decay so older or less\nrelevant facts recede — so your AI stays contextually aware without clutter.\nUse manual recall when you need to display memories in your UI, build custom\nprompts, or debug. See [How Memori\nWorks](/docs/memori-cloud/concepts/how-memory-works#how-recall-works) for\nautomatic vs manual recall and tuning.\n\n<CodeGroup title=\"Quick Start\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom openai import OpenAI\n\n# Requires MEMORI_API_KEY and OPENAI_API_KEY in your environment\nclient = OpenAI()\nmem = Memori().llm.register(client)\n\n# Track conversations by user and process\nmem.attribution(entity_id=\"user_123\", process_id=\"support_agent\")\n\n# All conversations automatically persisted and recalled\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"My favorite color is blue.\"}]\n)\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\n// Requires MEMORI_API_KEY and OPENAI_API_KEY in your environment\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\n\n// Track conversations by user and process\nmem.attribution('user_123', 'support_agent');\n\n// All conversations automatically persisted and recalled\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [{ role: 'user', content: 'My favorite color is blue.' }],\n});\n```\n\n</CodeGroup>\n\n## Core Concepts\n\n| Concept          | Description                                           | Example                                  |\n| ---------------- | ----------------------------------------------------- | ---------------------------------------- |\n| **Entity**       | Person, place, or thing (like a user)                 | `entity_id=\"user_123\"`                   |\n| **Process**      | Your agent, LLM interaction, or program               | `process_id=\"support_agent\"`             |\n| **Session**      | Groups LLM interactions together                      | Auto-generated UUID, manually manageable |\n| **Augmentation** | Background AI enhancement of memories                 | Auto-runs after wrapped LLM calls        |\n| **Recall**       | Retrieve relevant memories from previous interactions | Auto-injects recalled memories           |\n\n## Architecture Overview\n\nThe diagram has three lanes: your app, the Memori SDK, and Memori Cloud. Your app calls the LLM normally, Memori captures context on the response path, and memory processing continues in the background.\n\nSynchronous capture: conversation messages are sent to Memori Cloud while your normal LLM flow continues.\n\nRecall injection: relevant memories are fetched from managed storage and injected into later prompts.\n\nAsync augmentation: Memori Cloud extracts facts, preferences, rules, events, and relationships from conversations without blocking your app.\n\n![\"Memori Cloud architecture\"](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/docs/memori-cloud-architecture-detail.webp)\n"
  },
  {
    "path": "docs/memori-cloud/llm/agno.mdx",
    "content": "---\ntitle: Agno\ndescription: Using Memori with Agno agents on Memori Cloud.\n---\n\n# Agno\n\nMemori Cloud integrates with Agno at the model layer. Register your Agno model with `llm.register(...)` and Memori captures `run()`, `arun()`, and streamed responses automatically.\n\n<Note>\n  TypeScript support for Agno is coming soon. The TypeScript SDK currently supports [OpenAI](/docs/memori-cloud/llm/openai), [Anthropic](/docs/memori-cloud/llm/anthropic), and [Gemini](/docs/memori-cloud/llm/gemini).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"Agno Integration\">\n\n```python {{ title: 'Sync' }}\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom memori import Memori\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori().llm.register(openai_chat=model)\nmem.attribution(entity_id=\"user_123\", process_id=\"agno_agent\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nresponse = agent.run(\"Hello!\", session_id=\"support-session\")\nprint(response.content)\n```\n\n```python {{ title: 'Async' }}\nimport asyncio\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom memori import Memori\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori().llm.register(openai_chat=model)\nmem.attribution(entity_id=\"user_123\", process_id=\"agno_agent\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nasync def main():\n    response = await agent.arun(\"Hello!\", session_id=\"support-session\")\n    print(response.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom memori import Memori\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori().llm.register(openai_chat=model)\nmem.attribution(entity_id=\"user_123\", process_id=\"agno_agent\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nstream = agent.run(\"Hello!\", session_id=\"support-session\", stream=True)\nfor chunk in stream:\n    if hasattr(chunk, \"content\") and chunk.content:\n        print(chunk.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Different Providers\n\nAgno supports multiple model families. Use the matching registration keyword in Memori.\n\n| Package                 | Model Class  | Registration Keyword |\n| ----------------------- | ------------ | -------------------- |\n| `agno.models.openai`    | `OpenAIChat` | `openai_chat=model`  |\n| `agno.models.anthropic` | `Claude`     | `claude=model`       |\n| `agno.models.google`    | `Gemini`     | `gemini=model`       |\n| `agno.models.xai`       | `xAI`        | `xai=model`          |\n\n<CodeGroup title=\"Agno Providers\">\n\n```python {{ title: 'OpenAI' }}\nfrom agno.models.openai import OpenAIChat\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\nmem = Memori().llm.register(openai_chat=model)\n```\n\n```python {{ title: 'Anthropic' }}\nfrom agno.models.anthropic import Claude\n\nmodel = Claude(id=\"claude-sonnet-4-20250514\")\nmem = Memori().llm.register(claude=model)\n```\n\n```python {{ title: 'Google Gemini' }}\nfrom agno.models.google import Gemini\n\nmodel = Gemini(id=\"gemini-2.0-flash-exp\")\nmem = Memori().llm.register(gemini=model)\n```\n\n```python {{ title: 'xAI' }}\nfrom agno.models.xai import xAI\n\nmodel = xAI(id=\"grok-3\")\nmem = Memori().llm.register(xai=model)\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                   |\n| ------------ | ------------------------ |\n| **Sync**     | `agent.run()`            |\n| **Async**    | `await agent.arun()`     |\n| **Streamed** | `agent.run(stream=True)` |\n"
  },
  {
    "path": "docs/memori-cloud/llm/anthropic.mdx",
    "content": "---\ntitle: Anthropic\ndescription: Using Memori with Anthropic Claude models on Memori Cloud.\n---\n\n# Anthropic\n\nMemori Cloud supports all Anthropic Claude models. The `max_tokens` parameter is required for all Anthropic API calls.\n\n## Quick Start\n\n<CodeGroup title=\"Anthropic Integration\">\n\n```python {{ title: 'Python' }}\nfrom anthropic import Anthropic\nfrom memori import Memori\n\nclient = Anthropic()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"claude_assistant\")\n\nresponse = client.messages.create(\n    model=\"claude-sonnet-4-5-20250929\",\n    max_tokens=1024,\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.content[0].text)\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport Anthropic from '@anthropic-ai/sdk';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new Anthropic();\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'claude_assistant');\n\nconst response = await client.messages.create({\n  model: 'claude-sonnet-4-5-20250929',\n  max_tokens: 1024,\n  messages: [{ role: 'user', content: 'Hello!' }],\n});\nconsole.log(response.content[0].text);\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Python                           | TypeScript                             |\n| ------------ | -------------------------------- | -------------------------------------- |\n| **Sync**     | `client.messages.create()`       | —                                      |\n| **Async**    | `await client.messages.create()` | `await client.messages.create()`       |\n| **Streamed** | `client.messages.stream()`       | `stream: true` parameter               |\n\n## Additional Modes\n\n### Async (Python)\n\n```python\nimport asyncio\nfrom anthropic import AsyncAnthropic\nfrom memori import Memori\n\nclient = AsyncAnthropic()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"claude_assistant\")\n\nasync def main():\n    response = await client.messages.create(\n        model=\"claude-sonnet-4-5-20250929\",\n        max_tokens=1024,\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.content[0].text)\n\nasyncio.run(main())\n```\n\n### Streaming\n\n<CodeGroup title=\"Streaming\">\n\n```python {{ title: 'Python' }}\nfrom anthropic import Anthropic\nfrom memori import Memori\n\nclient = Anthropic()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"claude_assistant\")\n\nwith client.messages.stream(\n    model=\"claude-sonnet-4-5-20250929\",\n    max_tokens=1024,\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n) as stream:\n    for text in stream.text_stream:\n        print(text, end=\"\")\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport Anthropic from '@anthropic-ai/sdk';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new Anthropic();\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'claude_assistant');\n\nconst stream = await client.messages.create({\n  model: 'claude-sonnet-4-5-20250929',\n  max_tokens: 1024,\n  stream: true,\n  messages: [{ role: 'user', content: 'Hello!' }],\n});\nfor await (const event of stream) {\n  if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {\n    process.stdout.write(event.delta.text);\n  }\n}\n```\n\n</CodeGroup>\n\n## System Prompts\n\nAnthropic supports a top-level `system` parameter separate from the `messages` array. Memori captures both.\n\n```python\nfrom anthropic import Anthropic\nfrom memori import Memori\n\nclient = Anthropic()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"claude_assistant\")\n\nresponse = client.messages.create(\n    model=\"claude-sonnet-4-5-20250929\",\n    max_tokens=1024,\n    system=\"You are a helpful coding assistant.\",\n    messages=[\n        {\"role\": \"user\", \"content\": \"Explain Python decorators.\"}\n    ]\n)\nprint(response.content[0].text)\n```\n\n### TypeScript\n\n```typescript\nimport Anthropic from '@anthropic-ai/sdk';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new Anthropic();\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'claude_assistant');\n\nconst response = await client.messages.create({\n  model: 'claude-sonnet-4-5-20250929',\n  max_tokens: 1024,\n  system: 'You are a helpful coding assistant.',\n  messages: [\n    { role: 'user', content: 'Explain TypeScript generics.' },\n  ],\n});\nconsole.log(response.content[0].text);\n```\n"
  },
  {
    "path": "docs/memori-cloud/llm/aws-bedrock.mdx",
    "content": "---\ntitle: AWS Bedrock\ndescription: Using Memori with AWS Bedrock models via the LangChain ChatBedrock adapter on Memori Cloud.\n---\n\n# AWS Bedrock\n\nMemori supports AWS Bedrock through `langchain-aws`. Register using the `chatbedrock` keyword to access Claude, Llama, Mistral, and other Bedrock models.\n\n<Note>\n  TypeScript support for AWS Bedrock is coming soon. The TypeScript SDK currently supports [OpenAI](/docs/memori-cloud/llm/openai), [Anthropic](/docs/memori-cloud/llm/anthropic), and [Gemini](/docs/memori-cloud/llm/gemini).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"Bedrock Integration\">\n\n```python {{ title: 'Sync' }}\nfrom langchain_aws import ChatBedrock\nfrom memori import Memori\n\nclient = ChatBedrock(\n    model_id=\"anthropic.claude-sonnet-4-5-20250929\",\n    region_name=\"us-east-1\"\n)\n\nmem = Memori().llm.register(chatbedrock=client)\nmem.attribution(entity_id=\"user_123\", process_id=\"bedrock_agent\")\n\nresponse = client.invoke(\"Hello!\")\nprint(response.content)\n```\n\n```python {{ title: 'Async' }}\nimport asyncio\nfrom langchain_aws import ChatBedrock\nfrom memori import Memori\n\nclient = ChatBedrock(\n    model_id=\"anthropic.claude-sonnet-4-5-20250929\",\n    region_name=\"us-east-1\"\n)\n\nmem = Memori().llm.register(chatbedrock=client)\nmem.attribution(entity_id=\"user_123\", process_id=\"bedrock_agent\")\n\nasync def main():\n    response = await client.ainvoke(\"Hello!\")\n    print(response.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nfrom langchain_aws import ChatBedrock\nfrom memori import Memori\n\nclient = ChatBedrock(\n    model_id=\"anthropic.claude-sonnet-4-5-20250929\",\n    region_name=\"us-east-1\"\n)\n\nmem = Memori().llm.register(chatbedrock=client)\nmem.attribution(entity_id=\"user_123\", process_id=\"bedrock_agent\")\n\nfor chunk in client.stream(\"Hello!\"):\n    print(chunk.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                   |\n| ------------ | ------------------------ |\n| **Sync**     | `client.invoke()`        |\n| **Async**    | `await client.ainvoke()` |\n| **Streamed** | `client.stream()`        |\n\n## Available Models\n\n| Model                 | Model ID                                    |\n| --------------------- | ------------------------------------------- |\n| **Claude 3.5 Sonnet** | `anthropic.claude-3-5-sonnet-20241022-v2:0` |\n| **Claude 3 Haiku**    | `anthropic.claude-3-haiku-20240307-v1:0`    |\n| **Llama 3.1 70B**     | `meta.llama3-1-70b-instruct-v1:0`           |\n| **Mistral Large**     | `mistral.mistral-large-2407-v1:0`           |\n"
  },
  {
    "path": "docs/memori-cloud/llm/deepseek.mdx",
    "content": "---\ntitle: DeepSeek\ndescription: Using Memori with DeepSeek models via the OpenAI-compatible API on Memori Cloud.\n---\n\n# DeepSeek\n\nDeepSeek provides an OpenAI-compatible API. Use the `openai` Python package with `base_url=\"https://api.deepseek.com\"` — no special adapter needed.\n\n<Note>\n  TypeScript support for DeepSeek is coming soon. The TypeScript SDK currently supports [OpenAI](/docs/memori-cloud/llm/openai), [Anthropic](/docs/memori-cloud/llm/anthropic), and [Gemini](/docs/memori-cloud/llm/gemini).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"DeepSeek Integration\">\n\n```python {{ title: 'Sync' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"https://api.deepseek.com\",\n    api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"deepseek_assistant\")\n\nresponse = client.chat.completions.create(\n    model=\"deepseek-chat\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n```python {{ title: 'Async' }}\nimport os, asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\n\nclient = AsyncOpenAI(\n    base_url=\"https://api.deepseek.com\",\n    api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"deepseek_assistant\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"deepseek-chat\",\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"https://api.deepseek.com\",\n    api_key=os.getenv(\"DEEPSEEK_API_KEY\"),\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"deepseek_assistant\")\n\nstream = client.chat.completions.create(\n    model=\"deepseek-chat\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n    stream=True\n)\nfor chunk in stream:\n    if chunk.choices[0].delta.content:\n        print(chunk.choices[0].delta.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                                   |\n| ------------ | ---------------------------------------- |\n| **Sync**     | `client.chat.completions.create()`       |\n| **Async**    | `await client.chat.completions.create()` |\n| **Streamed** | `stream=True` parameter                  |\n"
  },
  {
    "path": "docs/memori-cloud/llm/gemini.mdx",
    "content": "---\ntitle: Google Gemini\ndescription: Using Memori with Google Gemini models on Memori Cloud.\n---\n\n# Google Gemini\n\nMemori integrates with Google Gemini via the `google-genai` SDK. Register the `GenerativeModel` instance and all `generate_content()` calls are automatically captured.\n\n## Quick Start\n\n<CodeGroup title=\"Gemini Integration\">\n\n```python {{ title: 'Python' }}\nimport os\nfrom memori import Memori\nimport google.generativeai as genai\n\ngenai.configure(api_key=os.getenv(\"GOOGLE_API_KEY\"))\nclient = genai.GenerativeModel(\"gemini-2.0-flash-exp\")  # Use any Gemini model\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"gemini_assistant\")\n\nresponse = client.generate_content(\"Hello!\")\nprint(response.text)\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport { GoogleGenAI } from '@google/genai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'gemini_assistant');\n\nconst response = await client.models.generateContent({\n  model: 'gemini-2.0-flash',\n  contents: 'Hello!',\n});\nconsole.log(response.text);\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Python                                  | TypeScript                                      |\n| ------------ | --------------------------------------- | ----------------------------------------------- |\n| **Sync**     | `client.generate_content()`             | —                                               |\n| **Async**    | `await client.generate_content_async()` | `await client.models.generateContent()`         |\n| **Streamed** | `stream=True` parameter                 | `client.models.generateContentStream()`         |\n\n## Additional Modes\n\n### Async (Python)\n\n```python\nimport os, asyncio\nfrom memori import Memori\nimport google.generativeai as genai\n\ngenai.configure(api_key=os.getenv(\"GOOGLE_API_KEY\"))\nclient = genai.GenerativeModel(\"gemini-2.0-flash-exp\")  # Use any Gemini model\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"gemini_assistant\")\n\nasync def main():\n    response = await client.generate_content_async(\"Hello!\")\n    print(response.text)\n\nasyncio.run(main())\n```\n\n### Streaming\n\n<CodeGroup title=\"Streaming\">\n\n```python {{ title: 'Python' }}\nimport os\nfrom memori import Memori\nimport google.generativeai as genai\n\ngenai.configure(api_key=os.getenv(\"GOOGLE_API_KEY\"))\nclient = genai.GenerativeModel(\"gemini-2.0-flash-exp\")  # Use any Gemini model\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"gemini_assistant\")\n\nresponse = client.generate_content(\"Hello!\", stream=True)\nfor chunk in response:\n    print(chunk.text, end=\"\")\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport { GoogleGenAI } from '@google/genai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'gemini_assistant');\n\nconst stream = await client.models.generateContentStream({\n  model: 'gemini-2.0-flash',\n  contents: 'Hello!',\n});\nfor await (const chunk of stream) {\n  process.stdout.write(chunk.text ?? '');\n}\n```\n\n</CodeGroup>\n\n## Multi-Turn Conversations\n\nUse `start_chat()` for multi-turn interactions. Memori tracks the full conversation automatically.\n\n```python\nchat = client.start_chat()\nresponse = chat.send_message(\"My name is Alice.\")\nprint(response.text)\n\nresponse = chat.send_message(\"What's my name?\")\nprint(response.text)\n```\n\n### TypeScript\n\nMulti-turn conversations in TypeScript use the standard messages array pattern:\n\n```typescript\nimport { GoogleGenAI } from '@google/genai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'gemini_assistant');\n\nconst response = await client.models.generateContent({\n  model: 'gemini-2.0-flash',\n  contents: [\n    { role: 'user', parts: [{ text: 'My name is Alice.' }] },\n  ],\n});\nconsole.log(response.text);\n\nconst response2 = await client.models.generateContent({\n  model: 'gemini-2.0-flash',\n  contents: [\n    { role: 'user', parts: [{ text: 'My name is Alice.' }] },\n    { role: 'model', parts: [{ text: response.text ?? '' }] },\n    { role: 'user', parts: [{ text: \"What's my name?\" }] },\n  ],\n});\nconsole.log(response2.text);\n```\n"
  },
  {
    "path": "docs/memori-cloud/llm/langchain.mdx",
    "content": "---\ntitle: LangChain\ndescription: Using Memori with LangChain chat models on Memori Cloud.\n---\n\n# LangChain\n\nMemori Cloud supports any LangChain chat model. Each class has its own registration keyword: `ChatOpenAI` for OpenAI, `ChatAnthropic` for Anthropic, `ChatBedrock` for BedRock, `ChatGoogleGenerativeAI` for Google Gen AI models.\n\n<Note>\n  TypeScript support for LangChain is coming soon. The TypeScript SDK currently supports [OpenAI](/docs/memori-cloud/llm/openai), [Anthropic](/docs/memori-cloud/llm/anthropic), and [Gemini](/docs/memori-cloud/llm/gemini).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"LangChain Integration\">\n\n```python {{ title: 'Sync' }}\nfrom langchain_openai import ChatOpenAI\nfrom memori import Memori\n\nclient = ChatOpenAI(model=\"gpt-4o-mini\")\n\nmem = Memori().llm.register(chatopenai=client)\nmem.attribution(entity_id=\"user_123\", process_id=\"langchain_agent\")\n\nresponse = client.invoke(\"Hello!\")\nprint(response.content)\n```\n\n```python {{ title: 'Async' }}\nimport asyncio\nfrom langchain_openai import ChatOpenAI\nfrom memori import Memori\n\nclient = ChatOpenAI(model=\"gpt-4o-mini\")\n\nmem = Memori().llm.register(chatopenai=client)\nmem.attribution(entity_id=\"user_123\", process_id=\"langchain_agent\")\n\nasync def main():\n    response = await client.ainvoke(\"Hello!\")\n    print(response.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nfrom langchain_openai import ChatOpenAI\nfrom memori import Memori\n\nclient = ChatOpenAI(model=\"gpt-4o-mini\")\n\nmem = Memori().llm.register(chatopenai=client)\nmem.attribution(entity_id=\"user_123\", process_id=\"langchain_agent\")\n\nfor chunk in client.stream(\"Hello!\"):\n    print(chunk.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Different Providers\n\n| Package                  | Chat Model               | Registration Keyword     |\n| ------------------------ | ------------------------ | ------------------------ |\n| `langchain-openai`       | `ChatOpenAI`             | `chatopenai=client`      |\n| `langchain-anthropic`    | `ChatAnthropic`          | `chatanthropic=client`   |\n| `langchain-google-genai` | `ChatGoogleGenerativeAI` | `chatgooglegenai=client` |\n| `langchain-aws`          | `ChatBedrock`            | `chatbedrock=client`     |\n\n<CodeGroup title=\"LangChain Providers\">\n\n```python {{ title: 'Anthropic' }}\nfrom langchain_anthropic import ChatAnthropic\nfrom memori import Memori\n\nclient = ChatAnthropic(model=\"claude-sonnet-4-5-20250929\")\nmem = Memori().llm.register(chatopenai=client)\n```\n\n```python {{ title: 'Google Gemini' }}\nfrom langchain_google_genai import ChatGoogleGenerativeAI\nfrom memori import Memori\n\nclient = ChatGoogleGenerativeAI(model=\"gemini-2.0-flash-exp\")\nmem = Memori().llm.register(chatgooglegenai=client)\n```\n\n```python {{ title: 'AWS Bedrock' }}\nfrom langchain_aws import ChatBedrock\nfrom memori import Memori\n\nclient = ChatBedrock(model_id=\"anthropic.claude-sonnet-4-5-20250929\", region_name=\"us-east-1\")\nmem = Memori().llm.register(chatbedrock=client)\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                   |\n| ------------ | ------------------------ |\n| **Sync**     | `client.invoke()`        |\n| **Async**    | `await client.ainvoke()` |\n| **Streamed** | `client.stream()`        |\n"
  },
  {
    "path": "docs/memori-cloud/llm/nebius.mdx",
    "content": "---\ntitle: Nebius AI Studio\ndescription: Using Memori with Nebius AI Studio models via the OpenAI-compatible API on Memori Cloud.\n---\n\n# Nebius AI Studio\n\nNebius AI Studio provides an OpenAI-compatible API. Use the `openai` Python package with `base_url=\"https://api.studio.nebius.com/v1/\"` — no special adapter needed.\n\n<Note>\n  TypeScript support for Nebius AI Studio is coming soon. The TypeScript SDK currently supports [OpenAI](/docs/memori-cloud/llm/openai), [Anthropic](/docs/memori-cloud/llm/anthropic), and [Gemini](/docs/memori-cloud/llm/gemini).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"Nebius AI Studio Integration\">\n\n```python {{ title: 'Sync' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"nebius_assistant\")\n\nresponse = client.chat.completions.create(\n    model=\"meta-llama/Llama-3.3-70B-Instruct\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n```python {{ title: 'Async' }}\nimport os, asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\n\nclient = AsyncOpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"nebius_assistant\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"meta-llama/Llama-3.3-70B-Instruct\",\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"nebius_assistant\")\n\nstream = client.chat.completions.create(\n    model=\"meta-llama/Llama-3.3-70B-Instruct\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n    stream=True\n)\nfor chunk in stream:\n    if chunk.choices[0].delta.content:\n        print(chunk.choices[0].delta.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                                   |\n| ------------ | ---------------------------------------- |\n| **Sync**     | `client.chat.completions.create()`       |\n| **Async**    | `await client.chat.completions.create()` |\n| **Streamed** | `stream=True` parameter                  |\n"
  },
  {
    "path": "docs/memori-cloud/llm/openai.mdx",
    "content": "---\ntitle: OpenAI\ndescription: Using Memori with OpenAI models including GPT-4o, GPT-4.1, and the Responses API on Memori Cloud.\n---\n\n# OpenAI\n\nMemori supports all OpenAI Chat Completions and Responses APIs. Both sync and async clients are fully supported.\n\n## Quick Start\n\n<CodeGroup title=\"OpenAI Integration\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'my_agent');\n\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [{ role: 'user', content: 'Hello!' }],\n});\nconsole.log(response.choices[0].message.content);\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode              | Python                                   | TypeScript                               |\n| ----------------- | ---------------------------------------- | ---------------------------------------- |\n| **Sync**          | `client.chat.completions.create()`       | —                                        |\n| **Async**         | `await client.chat.completions.create()` | `await client.chat.completions.create()` |\n| **Streamed**      | `stream=True` parameter                  | `stream: true` parameter                 |\n| **Responses API** | `client.responses.create()`              | —                                        |\n\n## Additional Modes\n\n### Async (Python)\n\n```python\nimport asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\n\nclient = AsyncOpenAI()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n### Streaming\n\n<CodeGroup title=\"Streaming\">\n\n```python {{ title: 'Python' }}\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nstream = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n    stream=True\n)\nfor chunk in stream:\n    if chunk.choices[0].delta.content:\n        print(chunk.choices[0].delta.content, end=\"\")\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'my_agent');\n\nconst stream = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages: [{ role: 'user', content: 'Hello!' }],\n  stream: true,\n});\nfor await (const chunk of stream) {\n  if (chunk.choices[0]?.delta?.content) {\n    process.stdout.write(chunk.choices[0].delta.content);\n  }\n}\n```\n\n</CodeGroup>\n\n### Responses API (Python)\n\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nresponse = client.responses.create(\n    model=\"gpt-4o-mini\",\n    input=\"Hello!\",\n    instructions=\"You are a helpful assistant.\"\n)\nprint(response.output_text)\n```\n\n## Multi-Turn Conversations\n\nMemori automatically captures each interaction and links them within the same session.\n\n<CodeGroup title=\"Multi-Turn Conversations\">\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nmessages = [\n    {\"role\": \"user\", \"content\": \"My name is Alice.\"}\n]\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=messages\n)\nmessages.append({\n    \"role\": \"assistant\",\n    \"content\": response.choices[0].message.content\n})\n\nmessages.append({\n    \"role\": \"user\",\n    \"content\": \"What's my name?\"\n})\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=messages\n)\nprint(response.choices[0].message.content)\n```\n\n```typescript\nimport OpenAI from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\nconst client = new OpenAI();\n\nconst mem = new Memori().llm.register(client);\nmem.attribution('user_123', 'my_agent');\n\nconst messages: OpenAI.ChatCompletionMessageParam[] = [\n  { role: 'user', content: 'My name is Alice.' },\n];\n\nconst response = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages,\n});\nmessages.push({\n  role: 'assistant',\n  content: response.choices[0].message.content!,\n});\n\nmessages.push({\n  role: 'user',\n  content: \"What's my name?\",\n});\nconst response2 = await client.chat.completions.create({\n  model: 'gpt-4o-mini',\n  messages,\n});\nconsole.log(response2.choices[0].message.content);\n```\n</CodeGroup>"
  },
  {
    "path": "docs/memori-cloud/llm/overview.mdx",
    "content": "---\ntitle: Integration Overview\ndescription: Memori is LLM-agnostic. Register any supported client and Memori handles memory capture, augmentation, and recall automatically.\n---\n\n# Integration Overview\n\nMemori Cloud works with all major LLM providers and frameworks. Register any supported client and Memori handles memory capture, augmentation, and recall automatically — with your Memori API key and provider credentials, no database setup required.\n\n## Supported Providers\n\n| Provider                                              | Integration        | Python Install                        | TypeScript Install                                  |\n| ----------------------------------------------------- | ------------------ | ------------------------------------- | --------------------------------------------------- |\n| **[OpenAI](/docs/memori-cloud/llm/openai)**           | Direct SDK wrapper | `pip install memori openai`           | `npm install @memorilabs/memori openai`              |\n| **[Anthropic](/docs/memori-cloud/llm/anthropic)**     | Direct SDK wrapper | `pip install memori anthropic`        | `npm install @memorilabs/memori @anthropic-ai/sdk`   |\n| **[Google Gemini](/docs/memori-cloud/llm/gemini)**    | Direct SDK wrapper | `pip install memori google-genai`     | `npm install @memorilabs/memori @google/genai`       |\n| **[xAI Grok](/docs/memori-cloud/llm/xai-grok)**       | OpenAI-compatible  | `pip install memori openai`           | Coming soon                                         |\n| **[Nebius AI Studio](/docs/memori-cloud/llm/nebius)** | OpenAI-compatible  | `pip install memori openai`           | Coming soon                                         |\n| **[DeepSeek](/docs/memori-cloud/llm/deepseek)**       | OpenAI-compatible  | `pip install memori openai`           | Coming soon                                         |\n| **[AWS Bedrock](/docs/memori-cloud/llm/aws-bedrock)** | LangChain adapter  | `pip install memori langchain-aws`    | Coming soon                                         |\n| **[LangChain](/docs/memori-cloud/llm/langchain)**     | Framework support  | `pip install memori langchain-openai` | Coming soon                                         |\n| **[Agno](/docs/memori-cloud/llm/agno)**               | Framework support  | `pip install memori agno`             | Coming soon                                         |\n| **[Pydantic AI](/docs/memori-cloud/llm/pydantic-ai)** | Framework support  | `pip install memori pydantic-ai`      | Coming soon                                         |\n\nAll providers support sync, async, streamed, and unstreamed modes.\n\n## Pydantic AI\n\nRegister the `Agent` instance directly — Memori wraps `run_sync` and `run` automatically.\n\n```python\nfrom memori import Memori\nfrom pydantic_ai import Agent\n\nagent = Agent(\"openai:gpt-4o-mini\")\n\nmem = Memori().llm.register(agent)\nmem.attribution(entity_id=\"user_123\", process_id=\"pydantic_agent\")\n\nresult = agent.run_sync(\"Hello!\")\nprint(result.output)\n```\n\n## OpenAI-Compatible Providers\n\nAny provider with an OpenAI-compatible API works by setting a custom `base_url`. Dedicated guides: [xAI Grok](/docs/memori-cloud/llm/xai-grok), [Nebius AI Studio](/docs/memori-cloud/llm/nebius), [DeepSeek](/docs/memori-cloud/llm/deepseek). Same pattern works for Azure OpenAI, NVIDIA NIM, and others.\n\n```python\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nresponse = client.chat.completions.create(\n    model=\"meta-llama/Llama-3.3-70B-Instruct\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n## OpenAI Responses API\n\n```python\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI()\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"my_agent\")\n\nresponse = client.responses.create(\n    model=\"gpt-4o-mini\",\n    input=\"Hello!\",\n    instructions=\"You are a helpful assistant.\"\n)\nprint(response.output_text)\n```\n"
  },
  {
    "path": "docs/memori-cloud/llm/pydantic-ai.mdx",
    "content": "---\ntitle: Pydantic AI\ndescription: Using Memori with Pydantic AI agents on Memori Cloud.\n---\n\n# Pydantic AI\n\nMemori integrates at the agent level — register the `Agent` instance and Memori wraps `run_sync` and `run` automatically. Works with any Pydantic AI-supported model.\n\n<Note>\n  TypeScript support for Pydantic AI is coming soon. The TypeScript SDK currently supports [OpenAI](/docs/memori-cloud/llm/openai), [Anthropic](/docs/memori-cloud/llm/anthropic), and [Gemini](/docs/memori-cloud/llm/gemini).\n</Note>\n\n## Quick Start\n\n```python\nfrom memori import Memori\nfrom pydantic_ai import Agent\n\nagent = Agent(\"openai:gpt-4o-mini\")\n\nmem = Memori().llm.register(agent)\nmem.attribution(entity_id=\"user_123\", process_id=\"pydantic_agent\")\n\nresult = agent.run_sync(\"Hello!\")\nprint(result.output)\n```\n\n## Async Usage\n\n```python\nimport asyncio\nfrom memori import Memori\nfrom pydantic_ai import Agent\n\nagent = Agent(\"openai:gpt-4o-mini\")\n\nmem = Memori().llm.register(agent)\nmem.attribution(entity_id=\"user_123\", process_id=\"pydantic_agent\")\n\nasync def main():\n    result = await agent.run(\"Hello!\")\n    print(result.output)\n\nasyncio.run(main())\n```\n\n## Different Providers\n\nPydantic AI supports multiple providers via model strings. Registration is identical regardless of provider.\n\n<CodeGroup title=\"Pydantic AI Providers\">\n\n```python {{ title: 'OpenAI' }}\nagent = Agent(\"openai:gpt-4o-mini\")\nmem = Memori().llm.register(agent)\n```\n\n```python {{ title: 'Anthropic' }}\nagent = Agent(\"anthropic:claude-sonnet-4-5-20250929\")\nmem = Memori().llm.register(agent)\n```\n\n```python {{ title: 'Google Gemini' }}\nagent = Agent(\"google-gla:gemini-2.0-flash-exp\")\nmem = Memori().llm.register(agent)\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                      |\n| ------------ | --------------------------- |\n| **Sync**     | `agent.run_sync()`          |\n| **Async**    | `await agent.run()`         |\n| **Streamed** | Via agent streaming support |\n"
  },
  {
    "path": "docs/memori-cloud/llm/xai-grok.mdx",
    "content": "---\ntitle: xAI Grok\ndescription: Using Memori with xAI Grok models via the OpenAI-compatible API on Memori Cloud.\n---\n\n# xAI Grok\n\nxAI provides an OpenAI-compatible API. Use the `openai` Python package with `base_url=\"https://api.x.ai/v1\"` — no special adapter needed.\n\n<Note>\n  TypeScript support for xAI Grok is coming soon. The TypeScript SDK currently supports [OpenAI](/docs/memori-cloud/llm/openai), [Anthropic](/docs/memori-cloud/llm/anthropic), and [Gemini](/docs/memori-cloud/llm/gemini).\n</Note>\n\n## Quick Start\n\n<CodeGroup title=\"xAI Grok Integration\">\n\n```python {{ title: 'Sync' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"https://api.x.ai/v1\",\n    api_key=os.getenv(\"XAI_API_KEY\")\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"grok_assistant\")\n\nresponse = client.chat.completions.create(\n    model=\"grok-2-latest\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n)\nprint(response.choices[0].message.content)\n```\n\n```python {{ title: 'Async' }}\nimport os, asyncio\nfrom memori import Memori\nfrom openai import AsyncOpenAI\n\nclient = AsyncOpenAI(\n    base_url=\"https://api.x.ai/v1\",\n    api_key=os.getenv(\"XAI_API_KEY\")\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"grok_assistant\")\n\nasync def main():\n    response = await client.chat.completions.create(\n        model=\"grok-2-latest\",\n        messages=[{\"role\": \"user\", \"content\": \"Hello!\"}]\n    )\n    print(response.choices[0].message.content)\n\nasyncio.run(main())\n```\n\n```python {{ title: 'Streaming' }}\nimport os\nfrom memori import Memori\nfrom openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"https://api.x.ai/v1\",\n    api_key=os.getenv(\"XAI_API_KEY\")\n)\n\nmem = Memori().llm.register(client)\nmem.attribution(entity_id=\"user_123\", process_id=\"grok_assistant\")\n\nstream = client.chat.completions.create(\n    model=\"grok-2-latest\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n    stream=True\n)\nfor chunk in stream:\n    if chunk.choices[0].delta.content:\n        print(chunk.choices[0].delta.content, end=\"\")\n```\n\n</CodeGroup>\n\n## Supported Modes\n\n| Mode         | Method                                   |\n| ------------ | ---------------------------------------- |\n| **Sync**     | `client.chat.completions.create()`       |\n| **Async**    | `await client.chat.completions.create()` |\n| **Streamed** | `stream=True` parameter                  |\n"
  },
  {
    "path": "docs/memori-cloud/mcp/agent-skills.mdx",
    "content": "---\ntitle: Agent Skills\ndescription: Add a SKILL.md file to teach your IDE agent when and how to use Memori MCP tools for reliable recall and memory storage.\n---\n\n# Agent Skills\n\nIn addition to the MCP server config, we strongly recommend adding an **Agent Skills file** (`SKILL.md`) that teaches your IDE agent _when_ and _how_ to use the Memori MCP tools.\n\n## Why Add a Skills File\n\n- **Reliability** — the agent consistently calls `recall` at the start of a turn and `advanced_augmentation` after responding\n- **Lower prompt friction** — you don't need to re-explain the memory workflow every session\n- **Focused context** — most clients only load the full skill instructions when the skill is triggered\n\n## The SKILL.md File\n\nCreate a `SKILL.md` file with the content below. This teaches the agent the full decision flow for when to recall, when to store memories, and what to skip.\n\n````markdown\n---\nname: memori-mcp-usage\ndescription: Decides when to call Memori MCP tools for recall and advanced augmentation. Use when the user mentions remembering, preferences, \"last time\", personalization, prior context, recurring patterns, or when consistency across turns matters.\n---\n\n# Memori MCP usage\n\n### Tool contract\n- `entity_id` and `process_id` are provided via MCP headers (do not pass as tool args unless the tool schema requires it).\n- `session_id` is generated by the server.\n\n## Decision flow per turn (follow in this order)\n\n1. Is this a trivial, administrative, or closing message?\n   Examples: \"thanks\", \"ok\", \"got it\", \"goodbye\", \"ignore everything before\"\n   → Skip both recall **and** augment. Answer directly.\n\n2. Does the user explicitly say **not** to remember / store / save / log / keep anything from this turn?\n   → Recall if the question needs prior context (step 3), but **never** augment this turn.\n\n3. Should we recall prior memory? Check conditions below.\n   → Yes → Call `recall` (usually once per turn)\n   → Use results to ground / personalize the answer\n   → No useful memories? → Proceed normally\n\n4. After drafting the final assistant response → Should we augment? Check conditions below.\n   → Yes → Call `advanced_augmentation` with the full turn `[user message, assistant response]`\n   → No → Skip\n\nSkip memory calls entirely for very short / purely reactive turns unless personalization is clearly relevant.\n\n## When to recall (`recall`)\n\nCall `recall` when **any** of these are true:\n\n- User references prior context: \"as I said\", \"last time\", \"you already know\", \"same as before\", \"remember when\", \"like we did\"\n- Preferences or style matter: tone, verbosity, length, formatting, units, structure, templates, language, emoji usage, etc.\n- Personal / project constraints apply: timezone, location, environment, setup, repo conventions, tooling stack, recurring workflows\n- Consistency across answers is important: decision patterns, naming conventions, approval thresholds, recurring questions\n- User asks to be reminded of something they previously told you\n\nQuery strategies (ranked best to worst):\n\n1. **Best**: user's exact latest message (preserves nuance, wording, typos, personal style)\n2. **Good**: very short rephrased intent when message is long/noisy\n3. **Avoid**: overly generic queries (\"preferences\", \"remember\", \"info about me\") — poor precision\n   **Avoid**: dumping full conversation history as query\n\nPrefer **one** recall per turn in almost all cases.\n\n## When to write (`advanced_augmentation`)\n\nCall `advanced_augmentation` **after** drafting your response **only** when the turn reveals a **durable, stable fact or preference** the user would want applied in future sessions. Ask yourself: *\"Would this information still be useful to recall weeks from now, in a completely different conversation?\"* If the answer is no, skip augment.\n\nAugment-worthy signals:\n\n- Explicit preferences (\"always do X\", \"from now on\", \"default to...\", \"prefer...\")\n- Identity / profile facts user wants persisted (timezone, role, typical environment, location hints)\n- Long-lived project or workflow context (naming conventions, tooling choices, architecture decisions, coding principles)\n- Relationships or team structure (\"Alice owns the auth service\", \"report to Bob\")\n\n### Do NOT call advanced_augmentation\n\n- Secrets, API keys, tokens, passwords (even partially redacted)\n- Large logs, stack traces, error dumps\n- Clearly ephemeral info: one-time codes, temp links, live prices\n- Debugging marked temporary (\"just this once\", \"for this error only\")\n- Role-play, hypotheticals, fiction\n- Purely reactive chit-chat with no lasting preference\n- Session activity / progress updates: test results, lint output, command exit codes\n- Task management actions: to-do completions, progress checkpoints, commit messages\n- Routine code changes: refactoring steps, file renames, import cleanups\n- Tool output narration: summarizing what a tool returned\n- Conversation-scoped decisions: one-off choices that don't represent a lasting rule\n\n**Rule of thumb**: if the information describes *what happened during this session* rather than *a fact or preference that transcends sessions*, do not augment.\n\n### Examples\n\n**Should augment:**\n- \"From now on keep answers short, include TL;DR, max 250 words\"\n- \"My location is Paris, France; I prefer Central time and 12-hour format\"\n- \"This project uses ruff + mypy + pytest; strongly prefer KISS/DRY/YAGNI\"\n\n**Should NOT augment:**\n- \"All 57 tests passed after the refactor\"\n- \"Renamed `_utils.py` to `_helpers.py`\"\n- \"The linter found 3 errors, now fixed\"\n\n**Should NOT use Memori tools at all:**\n- \"Thanks man!\"\n- \"Temp login code is 493021\"\n- \"What's the weather like in KC right now?\"\n````\n\n## Installing Skills by Client\n\n### Cursor\n\nCursor supports skill folders containing a `SKILL.md`:\n\n- **Project (recommended for teams):** `.cursor/skills/memori-mcp/SKILL.md`\n- **Personal (all projects):** `~/.cursor/skills/memori-mcp/SKILL.md`\n\n```bash\nmkdir -p .cursor/skills/memori-mcp\n# Add SKILL.md content to .cursor/skills/memori-mcp/SKILL.md\n```\n\nRestart Cursor so it re-discovers skills.\n\n### Claude Code\n\nClaude Code skills live in either:\n\n- **Personal:** `~/.claude/skills/memori-mcp/SKILL.md`\n- **Project:** `.claude/skills/memori-mcp/SKILL.md`\n\n```bash\nmkdir -p .claude/skills/memori-mcp\n# Add SKILL.md content to .claude/skills/memori-mcp/SKILL.md\n```\n\nRestart Claude Code or reload the session.\n\n### OpenAI Codex\n\nCodex scans repo-level skills under `.agents/skills`:\n\n```bash\nmkdir -p .agents/skills/memori-mcp\n# Add SKILL.md content to .agents/skills/memori-mcp/SKILL.md\n```\n\nRestart Codex if it doesn't pick up changes automatically.\n\n### Warp\n\nWarp supports custom agent rules that serve the same purpose as a skills file. Add the SKILL.md content to your Warp rules configuration:\n\n- **Project:** `.warp/rules/memori-mcp.md`\n- **Global:** `~/.warp/rules/memori-mcp.md`\n\n```bash\nmkdir -p .warp/rules\n# Add SKILL.md content to .warp/rules/memori-mcp.md\n```\n\nRestart Warp so the rules take effect.\n\n### Antigravity\n\nAntigravity skills can be stored at:\n\n- **Workspace:** `<workspace>/.agent/skills/memori-mcp/SKILL.md`\n- **Global:** `~/.gemini/antigravity/skills/memori-mcp/SKILL.md`\n\n```bash\nmkdir -p .agent/skills/memori-mcp\n# Add SKILL.md content to .agent/skills/memori-mcp/SKILL.md\n```\n\n### LangChain\n\nLangChain agents use Memori MCP to persist user preferences and project context across invocations. The integration logic lives in your application layer using the `langchain-mcp-adapters` package.\n\nTo implement this, instantiate `MultiServerMCPClient` per request with the end-user's ID set as `X-Memori-Entity-Id`, as shown on the Client Setup page.\n\n### Slack\n\nA Slack bot backed by an LLM uses Memori MCP to persist user preferences across all channels. As Slack is a cloud based service, the integration logic lives in your bot's backend.\n\nTo implement this, set the headers dynamically in your server's outgoing MCP tool calls using the Slack User ID from the incoming event payload as shown on the Client Setup page.\n\n### Notion\n\nCustom Notion integrations use Memori MCP to maintain a consistent memory of a user's writing style and project context across different pages and databases.\n\nTo implement this, your middleware must map the Notion User ID to the Memori headers as shown on the Client Setup page.\nThis ensures that regardless of which workspace or page the user is on, the AI assistant retrieves the same persistent preferences.\n\n\n## How It All Works Together\n\nOnce both the MCP config and skills file are installed:\n\n- The **MCP config** ensures the tools exist (`recall`, `advanced_augmentation`)\n- The **skills file** ensures the agent uses them _consistently and correctly_ — recall before responding, store after responding\n\n<Admonition type=\"tip\" title=\"Test Your Setup\">\n  After installing both, try telling your agent a preference like \"I always use tabs over spaces.\" In a later session, ask it to write code — it should recall and apply the preference automatically.\n</Admonition>\n"
  },
  {
    "path": "docs/memori-cloud/mcp/client-setup.mdx",
    "content": "---\ntitle: Client Setup\ndescription: Configure the Memori MCP server in Cursor, Claude Code, Codex, Warp, Antigravity, and other MCP clients with custom connectors.\n---\n\n# Client Setup\n\nConnect your IDE agent to the Memori MCP server. Each client uses a local MCP configuration with your API key and attribution headers.\n\n## Prerequisites\n\n- A Memori API key from [app.memorilabs.ai](https://app.memorilabs.ai)\n- An `entity_id` (identifies the end user) and optional `process_id` (identifies the agent or workflow)\n\nSet these as environment variables or replace the placeholders directly in the config:\n\n```bash\nexport MEMORI_API_KEY=\"your-memori-api-key\"\nexport MEMORI_ENTITY_ID=\"user_123\"\nexport MEMORI_PROCESS_ID=\"my_agent\" # optional\n```\n\n## Cursor\n\nCreate `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project-level):\n\n```json\n{\n  \"mcpServers\": {\n    \"memori\": {\n      \"url\": \"https://api.memorilabs.ai/mcp/\",\n      \"headers\": {\n        \"X-Memori-API-Key\": \"${MEMORI_API_KEY}\",\n        \"X-Memori-Entity-Id\": \"${MEMORI_ENTITY_ID}\",\n        \"X-Memori-Process-Id\": \"${MEMORI_PROCESS_ID}\"\n      }\n    }\n  }\n}\n```\n\nRestart Cursor after saving the file.\n\n## Claude Code\n\nYou can configure via the CLI or a `.mcp.json` file.\n\n### CLI\n\n```bash\nclaude mcp add --transport http memori https://api.memorilabs.ai/mcp/ \\\n  --header \"X-Memori-API-Key: ${MEMORI_API_KEY}\" \\\n  --header \"X-Memori-Entity-Id: ${MEMORI_ENTITY_ID}\" \\\n  --header \"X-Memori-Process-Id: ${MEMORI_PROCESS_ID}\"\n```\n\n### Project Config\n\nCreate `.mcp.json` in your project root:\n\n```json\n{\n  \"mcpServers\": {\n    \"memori\": {\n      \"type\": \"http\",\n      \"url\": \"https://api.memorilabs.ai/mcp/\",\n      \"headers\": {\n        \"X-Memori-API-Key\": \"${MEMORI_API_KEY}\",\n        \"X-Memori-Entity-Id\": \"${MEMORI_ENTITY_ID}\",\n        \"X-Memori-Process-Id\": \"${MEMORI_PROCESS_ID}\"\n      }\n    }\n  }\n}\n```\n\nRun `/mcp` inside Claude Code to verify the server status.\n\n## OpenAI Codex\n\nCodex reads MCP server configuration from `~/.codex/config.toml`. Add this:\n\n```toml\n[mcp_servers.memori]\nenabled = true\nurl = \"https://api.memorilabs.ai/mcp/\"\n\n[mcp_servers.memori.http_headers]\nX-Memori-API-Key = \"${MEMORI_API_KEY}\"\nX-Memori-Entity-Id = \"${MEMORI_ENTITY_ID}\"\nX-Memori-Process-Id = \"${MEMORI_PROCESS_ID}\"\n```\n\n`X-Memori-Process-Id` is optional.\n\nYou can also add the server from the Codex UI: **Settings > Settings > MCP Servers > + Add Server**.\n\n## Warp\n\nWarp cloud agents support MCP JSON config with remote URLs. Add the Memori server to your Warp MCP configuration:\n\n```json\n{\n  \"memori\": {\n    \"serverUrl\": \"https://api.memorilabs.ai/mcp/\",\n    \"headers\": {\n      \"X-Memori-API-Key\": \"your-memori-api-key\",\n      \"X-Memori-Entity-Id\": \"user_123\",\n      \"X-Memori-Process-Id\": \"my_agent\"\n    }\n  }\n}\n```\n\n`X-Memori-Process-Id` is optional.\n\n## Antigravity\n\nOpen **Manage MCP Servers** and edit `mcp_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"memori\": {\n      \"serverUrl\": \"https://api.memorilabs.ai/mcp/\",\n      \"headers\": {\n        \"X-Memori-API-Key\": \"your-memori-api-key\",\n        \"X-Memori-Entity-Id\": \"user_123\",\n        \"X-Memori-Process-Id\": \"my_agent\"\n      }\n    }\n  }\n}\n```\n\nSave and restart Antigravity so the tool list refreshes.\n\n## LangChain\n\nSet headers dynamically based on the current user session:\n\n```python\nfrom langchain_mcp_adapters.client import MultiServerMCPClient\n\nclient = MultiServerMCPClient({\n    \"memori\": {\n        \"transport\": \"streamable_http\",\n        \"url\": \"https://api.memorilabs.ai/mcp/\",\n        \"headers\": {\n            \"X-Memori-API-Key\": \"your-memori-api-key\",\n            \"X-Memori-Entity-Id\": \"user_123\",\n            \"X-Memori-Process-Id\": \"langchain_agent\"\n        }\n    }\n})\n\n# Load the tools to be used by your LangChain agent\ntools = await client.get_tools()\n```\n\n## Slack\n\nSet headers dynamically per request using the Slack user ID from the event payload:\n\n```ts\nconst memoriHeaders = {\n  \"X-Memori-API-Key\": process.env.MEMORI_API_KEY,\n  \"X-Memori-Entity-Id\": slackEvent.user,         // e.g. \"U04ABCDEF\"\n  \"X-Memori-Process-Id\": \"supportbot\",\n};\n```\n\nPass these headers in every MCP tool call. `session_id` is derived automatically from `entity_id` and the current UTC hour.\nUse `process_id` to isolate memories by integration or workspace so preferences from a personal workspace don't bleed into a team workspace.\n\n## Notion\n\nSet the entity and process IDs from the Notion API user object:\n\n```ts\nconst memoriHeaders = {\n  \"X-Memori-API-Key\": process.env.MEMORI_API_KEY,\n  \"X-Memori-Entity-Id\": notionUser.id,              // e.g. \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"\n  \"X-Memori-Process-Id\": \"notion_writing_assistant\",\n};\n```\n\nPass these headers in every MCP tool call. `session_id` is derived automatically from `entity_id` and the current UTC hour.\nUse `process_id` to isolate memories by integration or workspace so preferences from a personal workspace don't bleed into a team workspace.\n\n## Verifying the Connection\n\nAfter configuring any client:\n\n1. Confirm the MCP server shows as **connected** in your client's UI\n2. Check that `recall` and `advanced_augmentation` appear in the tools list\n3. Send a test message — `recall` should return a response (even if empty for new entities)\n4. Verify `advanced_augmentation` returns `memory being created`\n\nIf you receive 401 errors, double-check your `X-Memori-API-Key` value. See [Troubleshooting](/docs/memori-cloud/support/troubleshooting) for more help."
  },
  {
    "path": "docs/memori-cloud/mcp/overview.mdx",
    "content": "---\ntitle: Memori MCP Overview\ndescription: Use the Model Context Protocol (MCP) to connect AI agents directly to Memori for real-time recall and memory storage.\n---\n\n# Memori MCP (Model Context Protocol)\n\nMCP (Model Context Protocol) is a standard way for AI agents to connect to external tools and APIs. With Memori's MCP server, your agent can retrieve relevant memories before answering and store durable facts after responding — keeping context across sessions without any SDK integration.\n\n## Why Use MCP\n\nWithout MCP, your agent only sees the current conversation. With MCP, it can:\n\n- **Store** stable user facts and preferences after answering using the `advanced_augmentation` tool\n- **Recall** relevant memories before answering using the `recall` tool\n- **Maintain context** across sessions using `entity_id`, `process_id`, and `session_id`\n\nMCP is ideal for IDE agents (Cursor, Claude Code, Codex, Warp, Antigravity) and other MCP-compatible clients where you want persistent memory without writing integration code.\n\n## Server Details\n\n| Property      | Value                                    |\n| ------------- | ---------------------------------------- |\n| **Server**    | Memori MCP                               |\n| **Endpoint**  | `https://api.memorilabs.ai/mcp/`         |\n| **Transport** | Stateless HTTP                           |\n| **Auth**      | API key via request headers              |\n\n### Headers\n\n<Properties>\n  <Property name=\"X-Memori-API-Key\" type=\"string\" required>\n    Your Memori API key from [app.memorilabs.ai](https://app.memorilabs.ai).\n  </Property>\n  <Property name=\"X-Memori-Entity-Id\" type=\"string\" required>\n    Stable end-user or entity identifier (e.g. `user_123`).\n  </Property>\n  <Property name=\"X-Memori-Process-Id\" type=\"string\">\n    Optional process, app, or workflow identifier (e.g. `my_agent`) for memory isolation.\n  </Property>\n</Properties>\n\n<Note>\n  `session_id` is derived automatically as `<entity_id>-<UTC year-month-day:hour>`. You do not need to provide it.\n</Note>\n\n## Tools\n\n### `recall`\n\nFetches relevant memories at the start of a user turn.\n\n<Properties>\n  <Property name=\"query\" type=\"string\" required>\n    The latest user message — typically passed verbatim.\n  </Property>\n</Properties>\n\n`entity_id` is read from MCP headers. `process_id` is read when provided. Returns a structured memory payload with relevant context.\n\n### `advanced_augmentation`\n\nStores durable memory after the agent has drafted a response.\n\n<Properties>\n  <Property name=\"user_message\" type=\"string\" required>\n    The user's message for this turn.\n  </Property>\n  <Property name=\"assistant_response\" type=\"string\" required>\n    The assistant's response for this turn.\n  </Property>\n</Properties>\n\n`entity_id` is read from MCP headers. `process_id` is read when provided. Returns a confirmation: `memory being created`.\n\n## Example Agent Flow\n\nGiven the user message: _\"I prefer Python and use uv for dependency management.\"_\n\n1. Agent calls `recall` with the user message as `query`\n2. Agent composes a response using any returned facts\n3. Agent sends the response to the user\n4. Agent calls `advanced_augmentation` with the `user_message` and `assistant_response`\n\nOn a later turn like _\"Write a hello world script\"_, the agent recalls the Python + uv preference and personalizes its response.\n\n## Validation Checklist\n\nAfter configuring your client, verify the setup:\n\n- MCP server shows as connected and healthy in your client UI\n- Tools list includes `recall` and `advanced_augmentation`\n- Calls return non-401 responses\n- `recall` returns memories for known entities\n- `advanced_augmentation` returns `memory being created`\n"
  },
  {
    "path": "docs/memori-cloud/openclaw/overview.mdx",
    "content": "---\ntitle: OpenClaw Overview\ndescription: Give your OpenClaw agents structured, persistent memory with the Memori plugin — auto-recall and auto-capture across every session.\n---\n\n# OpenClaw Overview\n\nBy default, OpenClaw agents forget everything between sessions. The Memori plugin fixes that. It watches conversations, extracts what matters, and brings it back when relevant — automatically.\n\n## The Problem with OpenClaw's Built-In Memory\n\nOpenClaw ships with a basic memory layer, but it has fundamental limitations that break down at scale:\n\n| Limitation | What happens |\n| --- | --- |\n| **Flat markdown files** | Memory lives in plain text files with no structure or relationships. The agent reads and writes large chunks of text with no way to index, query, or deduplicate facts. |\n| **Context compaction** | As sessions grow longer, important details begin to disappear. Memory gets lost when context is compressed to stay within token limits. |\n| **No relationship reasoning** | OpenClaw retrieves semantically similar text, but cannot understand relationships between facts or connect them to each other. |\n| **Cross-project noise** | When working across multiple projects, memories are not isolated. Searches return irrelevant results from other contexts, polluting the agent's responses. |\n| **No user isolation** | Users do not have their own isolated memories. Memories bleed across users and data is mixed, creating privacy and relevance issues. |\n\n## What Changes When You Add Memori?\n\nThe Memori plugin replaces OpenClaw's flat-file memory workflow with managed, structured memory that is scoped by `entity_id`, `process_id`, and `session_id` and enriched automatically through OpenClaw's existing hooks.\n\n### 1. Structured memory storage\n\nInstead of raw markdown blobs, Memori stores conversations, facts, preferences, and knowledge-graph triples as structured records tied to an entity, process, and session. Facts are extracted as subject-predicate-object relationships, deduplicated over time, and connected into a graph so related memories stay queryable instead of being buried in text files.\n\n### 2. Advanced Augmentation\n\nAfter each conversation, Memori processes the user and assistant exchange asynchronously in the background, identifies facts, preferences, skills, and attributes, generates embeddings for semantic search, and updates the knowledge graph without blocking the agent's response path.\n\n### 3. Intelligent Recall\n\nBefore the agent responds, Memori searches the current entity's stored facts and knowledge graph, ranks memories by semantic relevance and importance, and injects the most useful context into the prompt so durable knowledge survives context-window compression.\n\n### 4. Production-ready observability\n\nMemori Cloud gives you dashboard visibility into memory creation, recalls, cache hit rate, sessions, quota usage, top subjects, per-memory retrieval metrics, and knowledge-graph relationships, so you can inspect what was stored and how recall is behaving in production.\n\n> The plugin still remains drop-in: OpenClaw handles the agent loop, while Memori adds recall, augmentation, sanitization, and observability around it.\n\n\n## How It Works\n\nThe plugin hooks into two points in OpenClaw's event lifecycle:\n\n### 1. `before_prompt_build` — Intelligent Recall\n\nWhen a user sends a message, the plugin:\n\n1. Intercepts the event and extracts the user's prompt\n2. Sanitizes the prompt (strips metadata, timestamps, system messages)\n3. Queries the Memori API for relevant memories\n4. Prepends matching memories to the agent's system context\n\nThe agent then responds with full awareness of prior context.\n\n### 2. `agent_end` — Advanced Augmentation\n\nAfter the agent finishes responding, the plugin:\n\n1. Extracts the last user and assistant messages\n2. Sanitizes both (removes thinking blocks, metadata fences, timestamps)\n3. Sends the cleaned exchange to the Memori API\n4. Memori extracts durable facts, updates stale ones, and merges duplicates\n\nThis runs asynchronously and never blocks the agent's response.\n"
  },
  {
    "path": "docs/memori-cloud/openclaw/quickstart.mdx",
    "content": "---\ntitle: Quickstart\ndescription: Install and configure the Memori plugin for OpenClaw in under 5 minutes.\n---\n\n# OpenClaw Quickstart\n\nGet persistent memory running in your OpenClaw gateway in three steps.\n\n## Prerequisites\n\n- [OpenClaw](https://openclaw.ai) `v2026.3.2` or later\n- A Memori API key from [app.memorilabs.ai](https://app.memorilabs.ai)\n- An Entity ID to attribute memories to (e.g., a user ID or agent name)\n\n## 1. Install and Enable\n\n```bash\n# Install the plugin from npm\nopenclaw plugins install @memorilabs/openclaw-memori\n\n# Enable it in your workspace\nopenclaw plugins enable openclaw-memori\n\n# Restart the gateway to load the plugin\nopenclaw gateway restart\n```\n\n## 2. Configure\n\nYou need two values: your Memori API key and an Entity ID.\n\n### Option A: Via CLI (Recommended)\n\n```bash\nopenclaw config set plugins.entries.openclaw-memori.config.apiKey \"YOUR_MEMORI_API_KEY\"\nopenclaw config set plugins.entries.openclaw-memori.config.entityId \"your-app-user-id\"\n```\n\n### Option B: Via `openclaw.json`\n\nAdd the following to `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"plugins\": {\n    \"entries\": {\n      \"openclaw-memori\": {\n        \"enabled\": true,\n        \"config\": {\n          \"apiKey\": \"your-memori-api-key\",\n          \"entityId\": \"your-app-user-id\"\n        }\n      }\n    }\n  }\n}\n```\n\n### Configuration Options\n\n<Properties>\n  <Property name=\"apiKey\" type=\"string\" required>\n    Your Memori API key from [app.memorilabs.ai](https://app.memorilabs.ai).\n  </Property>\n  <Property name=\"entityId\" type=\"string\" required>\n    The unique identifier for the entity (user, agent, or tenant) to attribute memories to.\n  </Property>\n</Properties>\n\n## 3. Verify\n\nAfter configuring, restart the gateway and check the logs:\n\n```bash\nopenclaw gateway restart\nopenclaw gateway logs --filter \"[Memori]\"\n```\n\nYou should see:\n\n```\n[Memori] === INITIALIZING PLUGIN ===\n[Memori] Tracking Entity ID: your-app-user-id\n```\n\n### Test the Memory Loop\n\n1. Send a message with a preference:\n   > \"I always use TypeScript and prefer functional patterns.\"\n\n2. Check the logs for a successful augmentation:\n   ```\n   [Memori] === AUGMENTATION HOOK START ===\n   [Memori] Capturing conversation turn...\n   [Memori] Augmentation successful!\n   ```\n\n3. In a new session, ask a question that should trigger recall:\n   > \"Write a hello world script.\"\n\n4. The agent should recall and apply your TypeScript preference automatically:\n   ```\n   [Memori] === RECALL HOOK START ===\n   [Memori] Executing SDK Recall...\n   [Memori] Successfully injected memory context.\n   ```\n\n## What Happens Under the Hood\n\nOnce installed, the plugin works automatically on every conversation turn:\n\n| Phase | Hook | What the plugin does |\n| --- | --- | --- |\n| **Before response** | `before_prompt_build` | Queries Memori for relevant memories and injects them into the agent's context |\n| **After response** | `agent_end` | Extracts the user/assistant exchange, sanitizes it, and sends it to Memori for fact extraction |\n\nNo changes to your agent's code or prompts are required. The plugin handles everything through OpenClaw's event lifecycle.\n\n<Admonition type=\"tip\" title=\"Multi-Agent Gateways\">\n  The plugin is fully stateless and thread-safe. You can run it across multiple agents in the same gateway without any shared state or concurrency issues.\n</Admonition>\n"
  },
  {
    "path": "docs/memori-cloud/support/faq.mdx",
    "content": "---\ntitle: FAQ\ndescription: Frequently asked questions about Memori Cloud.\n---\n\n# FAQ\n\n## What is Memori?\n\n__Memori__ is a memory layer for LLM applications, agents, and copilots. It continuously captures interactions, extracts structured knowledge, and intelligently ranks, decays, and retrieves the relevant memories. So your AI remembers the right things at the right time across every session.\n\n## Which languages are supported?\n\nMemori Cloud supports both **Python** and **TypeScript** SDKs. The Python SDK (`memori`) works with both Memori Cloud and BYODB. The TypeScript SDK (`@memorilabs/memori`) is currently Cloud-only. Both SDKs support OpenAI, Anthropic, and Gemini, with additional provider support in Python.\n\n## Memori Cloud vs Memori BYODB?\n\n**Memori Cloud** — managed storage, dashboard UI, just add an API key. Available for Python and TypeScript. **Memori BYODB** — bring your own database & maintain full control. Python only.\n\n## Which LLM providers are supported?\n\n**Python:** OpenAI, Anthropic, Google Gemini, xAI Grok, Deepseek, Nebius AI Studio, AWS Bedrock, LangChain, Agno, and Pydantic AI. All support sync, async, and streaming.\n\n**TypeScript:** OpenAI, Anthropic, and Google Gemini. More providers coming soon.\n\nSee the [Integration Overview](/docs/memori-cloud/llm/overview).\n\n## Do I need to set up a database?\n\nNo. The Memori Cloud platform manages all storage. Just initialize with `mem = Memori()`.\n\n## Is there a free tier?\n\nYes. 5000 memory read & writes per month. Sign up at [app.memorilabs.ai](https://app.memorilabs.ai).\n\n## What counts as a \"memory\"?\n\nEach fact, preference, or relationship extracted by Advanced Augmentation counts as one memory. A single conversation can generate multiple memories.\n\n## Does it support async?\n\nYes. In Python, use your provider's async client (e.g. `AsyncOpenAI`). TypeScript is natively async — all SDK calls return Promises.\n\n## How does augmentation work?\n\nIt runs in the background after each conversation and extracts facts, preferences, and relationships while minimizing impact on your LLM response path. See [Advanced Augmentation](/docs/memori-cloud/concepts/advanced-augmentation).\n\n## Can I use multiple LLM providers?\n\nYes. Register multiple clients on the same Memori instance. Memories are shared across providers since they're scoped to entities, not to providers.\n\n## Can I migrate between Memori Cloud and Memori BYODB?\n\nYes. In Python, both use the same SDK — just remove `conn=SessionLocal` for Memori Cloud or add it for Memori BYODB. Existing memories don't auto-transfer. The TypeScript SDK is Cloud-only.\n\n## Still have questions?\n\n- [Troubleshooting guide](/docs/memori-cloud/support/troubleshooting)\n- [GitHub Issues](https://github.com/MemoriLabs/Memori/issues)\n- [Discord](https://discord.gg/abD4eGym6v)\n"
  },
  {
    "path": "docs/memori-cloud/support/troubleshooting.mdx",
    "content": "---\ntitle: Troubleshooting\ndescription: Common issues and solutions when using Memori Cloud.\n---\n\n# Troubleshooting\n\nQuick fixes for the most common Memori issues.\n\n## Installation\n\n### Python\n\n**If `pip install memori` fails** — Requires Python 3.10+.\nRun `python --version` to check, then `pip install --upgrade pip && pip install memori`.\n\n### TypeScript\n\n**If `npm install @memorilabs/memori` fails** — Requires Node.js 18+.\nRun `node --version` to check, then update Node.js and retry.\n\n**Module resolution errors** — Ensure your `tsconfig.json` uses `\"moduleResolution\": \"node\"` or `\"bundler\"`. The SDK ships ESM with TypeScript declarations.\n\n## API Key Issues\n\n**Invalid or missing API key** — Set the `MEMORI_API_KEY` environment variable:\n\n```bash\nexport MEMORI_API_KEY=\"your-memori-api-key\"\n```\n\n**Quota exceeded** — Upgrade your account at [app.memorilabs.ai/settings/billing](https://app.memorilabs.ai/settings/billing).\n\n## No Memories Being Created\n\n1. **Register your LLM client** — conversations aren't captured without registration:\n\n<CodeGroup title=\"Register Client\">\n\n```python {{ title: 'Python' }}\nclient = OpenAI()\nmem = Memori().llm.register(client)\n```\n\n```typescript {{ title: 'TypeScript' }}\nconst client = new OpenAI();\nconst mem = new Memori().llm.register(client);\n```\n\n</CodeGroup>\n\n2. **Set attribution** before LLM calls — without it, no memories are stored:\n\n<CodeGroup title=\"Set Attribution\">\n\n```python {{ title: 'Python' }}\nmem.attribution(entity_id=\"user_123\", process_id=\"my_app\")\n```\n\n```typescript {{ title: 'TypeScript' }}\nmem.attribution('user_123', 'my_app');\n```\n\n</CodeGroup>\n\n## Recall Returns Empty\n\n- Verify `entity_id` matches what was used when memories were created\n- Increase limit: `mem.recall(\"query\", limit=10)`\n- Lower threshold: `mem.config.recall_relevance_threshold = 0.05`\n\n## Performance\n\n**Network timeouts** — Increase timeout: `mem.config.request_secs_timeout = 10` and retries: `mem.config.request_num_backoff = 10`.\n\n## Debug Logging\n\nEnable debug logging to inspect request flow, attribution, and augmentation behavior when troubleshooting missing memories or API errors. Use it temporarily in development, since logs can include sensitive request metadata.\n\n<CodeGroup title=\"Debug Logging\">\n\n```python {{ title: 'Python' }}\nimport logging\nlogging.basicConfig(level=logging.DEBUG, format=\"%(asctime)s | %(name)s | %(levelname)s | %(message)s\")\n\nfrom memori import Memori\nmem = Memori()\n```\n\n```typescript {{ title: 'TypeScript' }}\nimport { Memori } from '@memorilabs/memori';\nconst mem = new Memori({ debug: true });\n```\n\n</CodeGroup>\n\n## Getting Help\n\n- [GitHub Issues](https://github.com/MemoriLabs/Memori/issues)\n- [Discord](https://discord.gg/abD4eGym6v)\n- [Examples](https://github.com/MemoriLabs/Memori/tree/main/examples)\n\nInclude your language, runtime version (Python/Node.js), Memori version (`pip show memori` or check `package.json`), and full error trace.\n"
  },
  {
    "path": "examples/agno/README.md",
    "content": "# Memori + Agno Example\n\nExample showing how to use Memori with Agno agents to add persistent memory across conversations.\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Set your OpenAI API key**:\n   Create a `.env` file:\n   ```bash\n   OPENAI_API_KEY=your_api_key_here\n   ```\n\n3. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **Agno integration**: Use Memori with Agno's agent framework\n- **Persistent memory**: Conversations are stored in SQLite and recalled automatically\n- **Context awareness**: The agent remembers details from earlier in the conversation\n- **Customer support use case**: Shows a realistic scenario where memory is valuable\n"
  },
  {
    "path": "examples/agno/main.py",
    "content": "\"\"\"\nMemori + Agno + SQLite Example\n\nDemonstrates how Memori adds persistent memory to Agno agents.\n\"\"\"\n\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom dotenv import load_dotenv\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom memori import Memori\n\nload_dotenv()\n\ndb_path = os.getenv(\"DATABASE_PATH\", \"memori_agno.db\")\nengine = create_engine(f\"sqlite:///{db_path}\")\nSession = sessionmaker(bind=engine)\n\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori(conn=Session).llm.register(openai_chat=model)\nmem.attribution(entity_id=\"customer-456\", process_id=\"support-agent\")\nmem.config.storage.build()\n\nagent = Agent(\n    model=model,\n    instructions=[\n        \"You are a helpful customer support agent.\",\n        \"Remember customer preferences and history from previous conversations.\",\n    ],\n    markdown=True,\n)\n\nif __name__ == \"__main__\":\n    print(\"Customer: Hi, I'd like to order a large pepperoni pizza with extra cheese\")\n    response1 = agent.run(\n        \"Hi, I'd like to order a large pepperoni pizza with extra cheese\"\n    )\n    print(f\"Agent: {response1.content}\\n\")\n\n    print(\"Customer: Actually, can you remind me what I just ordered?\")\n    response2 = agent.run(\"Actually, can you remind me what I just ordered?\")\n    print(f\"Agent: {response2.content}\\n\")\n\n    print(\"Customer: Perfect! And what size was that again?\")\n    response3 = agent.run(\"Perfect! And what size was that again?\")\n    print(f\"Agent: {response3.content}\")\n\n    # Advanced Augmentation runs asynchronously to efficiently\n    # create memories. For this example, a short lived command\n    # line program, we need to wait for it to finish.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/agno/pyproject.toml",
    "content": "[project]\nname = \"memori-agno-example\"\nversion = \"0.1.0\"\nrequires-python = \">=3.9\"\ndependencies = [\n    \"memori\",\n    \"agno\",\n    \"sqlalchemy\",\n    \"python-dotenv\",\n]\n"
  },
  {
    "path": "examples/cockroachdb/README.md",
    "content": "# Memori + CockroachDB Example\n\n**Memori + CockroachDB** brings durable, distributed memory to AI - instantly, globally, and at any scale. Memori transforms conversations into structured, queryable intelligence, while CockroachDB keeps that memory available, resilient, and consistently accurate across regions. Deploy and scale effortlessly from prototype to production with zero downtime on enterprise-grade infrastructure. Give your AI a foundation to remember, reason, and evolve - with the simplicity of cloud and the reliability and power of distributed SQL.\n\n## Getting Started\n\nInstall Memori:\n\n```bash\npip install memori\n```\n\nSign up for [CockroachDB Cloud](https://www.cockroachlabs.com/product/cloud/).\n\nYou may need to record the database connection string for your implementation. Once you've signed up, your database is provisioned and ready for use with Memori.\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Set environment variables**:\n   ```bash\n   export OPENAI_API_KEY=your_api_key_here\n   export COCKROACHDB_CONNECTION_STRING=postgresql://user:password@host:26257/defaultdb?sslmode=verify-full\n   ```\n\n3. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **Serverless CockroachDB**: Connect to CockroachDB's cloud serverless Postgres with zero database management\n- **Automatic persistence**: All conversation messages are automatically stored in your CockroachDB database\n- **Context preservation**: Memori injects relevant conversation history into each LLM call\n- **Interactive chat**: Type messages and see how Memori maintains context across the conversation\n"
  },
  {
    "path": "examples/cockroachdb/main.py",
    "content": "\"\"\"\nQuickstart: Memori + OpenAI + CockroachDB\n\nDemonstrates how Memori adds memory across conversations.\n\"\"\"\n\nimport os\n\nimport psycopg2\nfrom openai import OpenAI\n\nfrom memori import Memori\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n\n\ndef get_conn():\n    return psycopg2.connect(os.getenv(\"COCKROACHDB_CONNECTION_STRING\"))\n\n\nmem = Memori(conn=get_conn).llm.register(client)\nmem.attribution(entity_id=\"user-123\", process_id=\"my-app\")\nmem.config.storage.build()\n\nif __name__ == \"__main__\":\n    print(\"You: My favorite color is blue and I live in Paris\")\n    response1 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[\n            {\"role\": \"user\", \"content\": \"My favorite color is blue and I live in Paris\"}\n        ],\n    )\n    print(f\"AI: {response1.choices[0].message.content}\\n\")\n\n    print(\"You: What's my favorite color?\")\n    response2 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n    )\n    print(f\"AI: {response2.choices[0].message.content}\\n\")\n\n    print(\"You: What city do I live in?\")\n    response3 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What city do I live in?\"}],\n    )\n    print(f\"AI: {response3.choices[0].message.content}\")\n\n    # Advanced Augmentation runs asynchronously to efficiently\n    # create memories. For this example, a short lived command\n    # line program, we need to wait for it to finish.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/cockroachdb/pyproject.toml",
    "content": "[project]\nname = \"memori-cockroachdb-example\"\nversion = \"0.1.0\"\ndescription = \"Memori SDK example with CockroachDB\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"memori>=3.0.0\",\n    \"openai>=2.6.1\",\n    \"psycopg2-binary>=2.9.11\",\n    \"python-dotenv>=1.2.1\",\n]\n"
  },
  {
    "path": "examples/digitalocean/README.md",
    "content": "# Memori + DigitalOcean Gradient Example\n\nExample showing how to use Memori with DigitalOcean Gradient AI Agents to add persistent memory across conversations.\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Set environment variables**:\n   Create a `.env` file:\n   ```bash\n   AGENT_ENDPOINT=your_gradient_agent_endpoint\n   AGENT_ACCESS_KEY=your_gradient_access_key\n   DATABASE_CONNECTION_STRING=postgresql+psycopg2://user:password@localhost:5432/dbname\n   ```\n\n3. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **DigitalOcean Gradient integration**: Use Memori with DigitalOcean's Gradient AI platform\n- **Persistent memory**: Conversations are stored in PostgreSQL and recalled automatically\n- **OpenAI-compatible API**: Gradient agents use OpenAI's API format for easy integration\n- **Context awareness**: The agent remembers details from earlier in the conversation\n"
  },
  {
    "path": "examples/digitalocean/main.py",
    "content": "\"\"\"\nMemori + DigitalOcean Gradient AI Example\n\nDemonstrates how Memori adds persistent memory to DigitalOcean Gradient AI Agents.\n\"\"\"\n\nimport os\n\nfrom dotenv import load_dotenv\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom memori import Memori\n\nload_dotenv()\n\nagent_endpoint = os.getenv(\"AGENT_ENDPOINT\")\nagent_access_key = os.getenv(\"AGENT_ACCESS_KEY\")\n\nif not agent_endpoint or not agent_access_key:\n    raise ValueError(\"AGENT_ENDPOINT and AGENT_ACCESS_KEY must be set in .env\")\n\nbase_url = (\n    agent_endpoint\n    if agent_endpoint.endswith(\"/api/v1/\")\n    else f\"{agent_endpoint}/api/v1/\"\n)\nclient = OpenAI(base_url=base_url, api_key=agent_access_key)\n\ndatabase_connection_string = os.getenv(\"DATABASE_CONNECTION_STRING\")\nif not database_connection_string:\n    raise ValueError(\"DATABASE_CONNECTION_STRING must be set in .env\")\n\nengine = create_engine(database_connection_string)\nSession = sessionmaker(bind=engine)\n\nmem = Memori(conn=Session).llm.register(client)\nmem.attribution(entity_id=\"user-123\", process_id=\"gradient-agent\")\nmem.config.storage.build()\n\nif __name__ == \"__main__\":\n    print(\"You: My favorite color is blue and I live in Paris\")\n    response1 = client.chat.completions.create(\n        model=\"n/a\",\n        messages=[\n            {\"role\": \"user\", \"content\": \"My favorite color is blue and I live in Paris\"}\n        ],\n    )\n    print(f\"AI: {response1.choices[0].message.content}\\n\")\n\n    print(\"You: What's my favorite color?\")\n    response2 = client.chat.completions.create(\n        model=\"n/a\",\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n    )\n    print(f\"AI: {response2.choices[0].message.content}\\n\")\n\n    print(\"You: What city do I live in?\")\n    response3 = client.chat.completions.create(\n        model=\"n/a\",\n        messages=[{\"role\": \"user\", \"content\": \"What city do I live in?\"}],\n    )\n    print(f\"AI: {response3.choices[0].message.content}\")\n\n    # Advanced Augmentation runs asynchronously to efficiently\n    # create memories. For this example, a short lived command\n    # line program, we need to wait for it to finish.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/digitalocean/pyproject.toml",
    "content": "[project]\nname = \"memori-digitalocean-example\"\nversion = \"0.1.0\"\ndescription = \"Memori SDK example with DigitalOcean Gradient AI\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"memori\",\n    \"openai>=2.6.1\",\n    \"SQLAlchemy>=2.0.0\",\n    \"psycopg2-binary>=2.9.0\",\n    \"python-dotenv>=1.2.1\",\n]\n"
  },
  {
    "path": "examples/mongodb/README.md",
    "content": "# Memori + MongoDB Example\n\nExample showing how to use Memori with MongoDB.\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Set environment variables**:\n   ```bash\n   export OPENAI_API_KEY=your_api_key_here\n   export MONGODB_CONNECTION_STRING=mongodb+srv://user:password@cluster.mongodb.net/dbname\n   ```\n\n3. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **NoSQL flexibility**: Store conversation data in MongoDB's document model\n- **Automatic persistence**: All conversation messages are automatically stored in MongoDB collections\n- **Context preservation**: Memori injects relevant conversation history into each LLM call\n- **Interactive chat**: Type messages and see how Memori maintains context across the conversation\n- **Cloud-ready**: Works seamlessly with MongoDB Atlas free tier\n"
  },
  {
    "path": "examples/mongodb/main.py",
    "content": "\"\"\"\nQuickstart: Memori + OpenAI + MongoDB\n\nDemonstrates how Memori adds memory across conversations.\n\"\"\"\n\nimport os\n\nfrom openai import OpenAI\nfrom pymongo import MongoClient\n\nfrom memori import Memori\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n\nmongo_client = MongoClient(os.getenv(\"MONGODB_CONNECTION_STRING\"))\ndb = mongo_client[\"memori\"]\n\nmem = Memori(conn=lambda: db).llm.register(client)\nmem.attribution(entity_id=\"user-123\", process_id=\"my-app\")\nmem.config.storage.build()\n\nif __name__ == \"__main__\":\n    print(\"You: My favorite color is blue and I live in Paris\")\n    response1 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[\n            {\"role\": \"user\", \"content\": \"My favorite color is blue and I live in Paris\"}\n        ],\n    )\n    print(f\"AI: {response1.choices[0].message.content}\\n\")\n\n    print(\"You: What's my favorite color?\")\n    response2 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n    )\n    print(f\"AI: {response2.choices[0].message.content}\\n\")\n\n    print(\"You: What city do I live in?\")\n    response3 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What city do I live in?\"}],\n    )\n    print(f\"AI: {response3.choices[0].message.content}\")\n\n    # Advanced Augmentation runs asynchronously to efficiently\n    # create memories. For this example, a short lived command\n    # line program, we need to wait for it to finish.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/mongodb/pyproject.toml",
    "content": "[project]\nname = \"memori-mongodb-example\"\nversion = \"0.1.0\"\ndescription = \"Memori SDK example with MongoDB\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"memori>=3.0.0\",\n    \"openai>=2.6.1\",\n    \"pymongo>=4.7.0\",\n    \"python-dotenv>=1.2.1\",\n]\n"
  },
  {
    "path": "examples/nebius/README.md",
    "content": "# Memori + Nebius AI Studio Example\n\nExample showing how to use Memori with Nebius AI Studio to add persistent memory across conversations.\n\n[Nebius AI Studio](https://nebius.com/ai-studio) provides an OpenAI-compatible API for state-of-the-art open-source models including Llama, Qwen, DeepSeek, and more. Since Nebius uses the OpenAI SDK, it works seamlessly with Memori's automatic instrumentation.\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Get a Nebius API key**:\n   - Sign up at [https://studio.nebius.ai/](https://studio.nebius.ai/)\n   - Get your API key from the dashboard\n\n3. **Set your API key**:\n   Create a `.env` file:\n   ```bash\n   NEBIUS_API_KEY=your_api_key_here\n   ```\n\n4. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **Nebius AI Studio integration**: Use Memori with Nebius's OpenAI-compatible API\n- **Open-source models**: Access to models like Llama 3.1 70B with persistent memory\n- **Automatic registration**: Simply use `.llm.register(client)` - Memori auto-detects Nebius\n- **Persistent memory**: Conversations are stored in SQLite and recalled automatically\n- **Context awareness**: The LLM remembers details from earlier in the conversation\n\n## Available Models\n\nNebius AI Studio supports 60+ open-source models. Check the [model catalog](https://studio.nebius.ai/) for the full list including:\n- Meta Llama 3.1 (8B, 70B, 405B)\n- Qwen 2.5\n- DeepSeek\n- Mistral\n- And many more\n\n## Alternative Base URLs\n\nNebius offers multiple endpoints:\n- Studio API: `https://api.studio.nebius.com/v1/`\n- Token Factory: `https://api.tokenfactory.nebius.com/`\n\nSimply change the `base_url` parameter to use a different endpoint.\n"
  },
  {
    "path": "examples/nebius/main.py",
    "content": "\"\"\"\nMemori + Nebius AI Studio + SQLite Example\n\nDemonstrates how Memori adds persistent memory to Nebius AI Studio LLMs.\nNebius AI Studio provides an OpenAI-compatible API with state-of-the-art open-source models.\n\"\"\"\n\nimport os\n\nfrom dotenv import load_dotenv\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom memori import Memori\n\nload_dotenv()\n\ndb_path = os.getenv(\"DATABASE_PATH\", \"memori_nebius.db\")\nengine = create_engine(f\"sqlite:///{db_path}\")\nSession = sessionmaker(bind=engine)\n\nclient = OpenAI(\n    base_url=\"https://api.studio.nebius.com/v1/\",\n    api_key=os.getenv(\"NEBIUS_API_KEY\"),\n)\n\nmem = Memori(conn=Session).llm.register(client)\nmem.attribution(entity_id=\"user-789\", process_id=\"nebius-chat-app\")\nmem.config.storage.build()\n\nif __name__ == \"__main__\":\n    print(\"User: My favorite color is blue and I live in Paris\")\n    response1 = client.chat.completions.create(\n        model=\"meta-llama/Llama-3.3-70B-Instruct\",\n        messages=[\n            {\n                \"role\": \"user\",\n                \"content\": \"My favorite color is blue and I live in Paris.\",\n            }\n        ],\n    )\n    print(f\"Assistant: {response1.choices[0].message.content}\\n\")\n\n    print(\"User: What's my favorite color?\")\n    response2 = client.chat.completions.create(\n        model=\"meta-llama/Llama-3.3-70B-Instruct\",\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n    )\n    print(f\"Assistant: {response2.choices[0].message.content}\\n\")\n\n    print(\"User: Where do I live?\")\n    response3 = client.chat.completions.create(\n        model=\"meta-llama/Llama-3.3-70B-Instruct\",\n        messages=[{\"role\": \"user\", \"content\": \"Where do I live?\"}],\n    )\n    print(f\"Assistant: {response3.choices[0].message.content}\")\n\n    # Advanced Augmentation runs asynchronously to efficiently\n    # create memories. For this example, a short lived command\n    # line program, we need to wait for it to finish.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/nebius/pyproject.toml",
    "content": "[project]\nname = \"memori-nebius-example\"\nversion = \"0.1.0\"\nrequires-python = \">=3.9\"\ndependencies = [\n    \"memori\",\n    \"openai\",\n    \"sqlalchemy\",\n    \"python-dotenv\",\n]\n"
  },
  {
    "path": "examples/neon/README.md",
    "content": "# Memori + Neon Example\n\nSign up for [Neon serverless Postgres](https://neon.tech).\n\nYou may need to record the database connection string for your implementation. Once you've signed up, your database is provisioned and ready for use with Memori.\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Set environment variables**:\n   ```bash\n   export OPENAI_API_KEY=your_api_key_here\n   export NEON_CONNECTION_STRING=postgresql://user:pass@ep-xyz-123.us-east-2.aws.neon.tech/dbname?sslmode=require\n   ```\n\n3. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **Serverless PostgreSQL**: Connect to Neon's serverless Postgres with zero database management\n- **Automatic persistence**: All conversation messages are automatically stored in your Neon database\n- **Context preservation**: Memori injects relevant conversation history into each LLM call\n- **Interactive chat**: Type messages and see how Memori maintains context across the conversation\n"
  },
  {
    "path": "examples/neon/main.py",
    "content": "\"\"\"\nQuickstart: Memori + OpenAI + Neon\n\nDemonstrates how Memori adds memory across conversations.\n\"\"\"\n\nimport os\n\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom memori import Memori\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n\nneon_connection_string = os.getenv(\"NEON_CONNECTION_STRING\")\nif not neon_connection_string:\n    raise ValueError(\"NEON_CONNECTION_STRING must be set in the environment\")\n\nengine = create_engine(neon_connection_string)\nSession = sessionmaker(bind=engine)\n\nmem = Memori(conn=Session).llm.register(client)\nmem.attribution(entity_id=\"user-123\", process_id=\"my-app\")\nmem.config.storage.build()\n\nif __name__ == \"__main__\":\n    print(\"You: My favorite color is blue and I live in Paris\")\n    response1 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[\n            {\"role\": \"user\", \"content\": \"My favorite color is blue and I live in Paris\"}\n        ],\n    )\n    print(f\"AI: {response1.choices[0].message.content}\\n\")\n\n    print(\"You: What's my favorite color?\")\n    response2 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n    )\n    print(f\"AI: {response2.choices[0].message.content}\\n\")\n\n    print(\"You: What city do I live in?\")\n    response3 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What city do I live in?\"}],\n    )\n    print(f\"AI: {response3.choices[0].message.content}\")\n\n    # Advanced Augmentation runs asynchronously to efficiently\n    # create memories. For this example, a short lived command\n    # line program, we need to wait for it to finish.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/neon/pyproject.toml",
    "content": "[project]\nname = \"memori-neon-example\"\nversion = \"0.1.0\"\ndescription = \"Memori SDK example with Neon serverless Postgres\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"memori\",\n    \"openai>=2.6.1\",\n    \"SQLAlchemy>=2.0.0\",\n    \"psycopg[binary]>=3.2.0\",\n    \"psycopg2-binary>=2.9.0\",\n    \"python-dotenv>=1.2.1\",\n]\n\n[tool.uv.sources]\nmemori = { path = \"../../..\", editable = true }\n"
  },
  {
    "path": "examples/oceanbase/README.md",
    "content": "# Memori + OceanBase Example\n\nExample showing how to use Memori with OceanBase (or SeekDB).\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Set environment variables**:\n   ```bash\n   export OPENAI_API_KEY=your_api_key_here\n   export DATABASE_CONNECTION_STRING=mysql+oceanbase://root:@localhost:2881/memori_test?charset=utf8mb4\n   ```\n\n3. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **OceanBase integration**: Connect to OceanBase or SeekDB using the pyobvector dialect\n- **Automatic persistence**: Conversation messages are stored in OceanBase tables\n- **Context preservation**: Memori injects relevant history into each LLM call\n"
  },
  {
    "path": "examples/oceanbase/main.py",
    "content": "\"\"\"\nQuickstart: Memori + OpenAI + OceanBase\n\nDemonstrates how Memori adds memory across conversations.\n\"\"\"\n\nimport os\n\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.dialects import registry\nfrom sqlalchemy.orm import sessionmaker\n\nfrom memori import Memori\n\nclient = OpenAI(\n    api_key=os.getenv(\"OPENAI_API_KEY\"),\n    base_url=os.getenv(\"OPENAI_BASE_URL\"),\n)\n\nregistry.register(\"mysql.oceanbase\", \"pyobvector.schema.dialect\", \"OceanBaseDialect\")\n\ndatabase_connection_string = os.getenv(\"DATABASE_CONNECTION_STRING\")\nif not database_connection_string:\n    raise ValueError(\"DATABASE_CONNECTION_STRING must be set in the environment\")\n\nengine = create_engine(database_connection_string, pool_pre_ping=True)\nSession = sessionmaker(bind=engine)\n\nmem = Memori(conn=Session).llm.register(client)\nmem.attribution(entity_id=\"user-123\", process_id=\"my-app\")\nmem.config.storage.build()\n\nif __name__ == \"__main__\":\n    model = os.getenv(\"OPENAI_MODEL\", \"qwen-plus\")\n    print(\"You: My favorite color is blue and I live in Paris\")\n    response1 = client.chat.completions.create(\n        model=model,\n        messages=[\n            {\"role\": \"user\", \"content\": \"My favorite color is blue and I live in Paris\"}\n        ],\n    )\n    print(f\"AI: {response1.choices[0].message.content}\\n\")\n\n    print(\"You: What's my favorite color?\")\n    response2 = client.chat.completions.create(\n        model=model,\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n    )\n    print(f\"AI: {response2.choices[0].message.content}\\n\")\n\n    print(\"You: What city do I live in?\")\n    response3 = client.chat.completions.create(\n        model=model,\n        messages=[{\"role\": \"user\", \"content\": \"What city do I live in?\"}],\n    )\n    print(f\"AI: {response3.choices[0].message.content}\")\n\n    # Wait for background augmentation in short-lived scripts.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/postgres/README.md",
    "content": "# Memori + PostgreSQL Example\n\nExample showing how to use Memori with PostgreSQL.\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Set environment variables**:\n   ```bash\n   export OPENAI_API_KEY=your_api_key_here\n   export DATABASE_CONNECTION_STRING=postgresql+psycopg://user:password@localhost:5432/dbname\n   ```\n\n3. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **PostgreSQL integration**: Connect to any PostgreSQL database (local, AWS RDS, or other managed database services)\n- **Automatic persistence**: All conversation messages are automatically stored in your database\n- **Context preservation**: Memori injects relevant conversation history into each LLM call\n- **Interactive chat**: Type messages and see how Memori maintains context across the conversation\n"
  },
  {
    "path": "examples/postgres/main.py",
    "content": "\"\"\"\nQuickstart: Memori + OpenAI + PostgreSQL\n\nDemonstrates how Memori adds memory across conversations.\n\"\"\"\n\nimport os\n\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom memori import Memori\n\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n\ndatabase_connection_string = os.getenv(\"DATABASE_CONNECTION_STRING\")\nif not database_connection_string:\n    raise ValueError(\"DATABASE_CONNECTION_STRING must be set in the environment\")\n\nengine = create_engine(database_connection_string)\nSession = sessionmaker(bind=engine)\n\nmem = Memori(conn=Session).llm.register(client)\nmem.attribution(entity_id=\"user-123\", process_id=\"my-app\")\nmem.config.storage.build()\n\nif __name__ == \"__main__\":\n    print(\"You: My favorite color is blue and I live in Paris\")\n    response1 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[\n            {\"role\": \"user\", \"content\": \"My favorite color is blue and I live in Paris\"}\n        ],\n    )\n    print(f\"AI: {response1.choices[0].message.content}\\n\")\n\n    print(\"You: What's my favorite color?\")\n    response2 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n    )\n    print(f\"AI: {response2.choices[0].message.content}\\n\")\n\n    print(\"You: What city do I live in?\")\n    response3 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What city do I live in?\"}],\n    )\n    print(f\"AI: {response3.choices[0].message.content}\")\n\n    # Advanced Augmentation runs asynchronously to efficiently\n    # create memories. For this example, a short lived command\n    # line program, we need to wait for it to finish.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/postgres/pyproject.toml",
    "content": "[project]\nname = \"memori-postgres-example\"\nversion = \"0.1.0\"\ndescription = \"Memori SDK example with PostgreSQL\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"memori>=3.0.0\",\n    \"openai>=2.6.1\",\n    \"SQLAlchemy>=2.0.0\",\n    \"psycopg[binary]>=3.2.0\",\n    \"python-dotenv>=1.2.1\",\n]\n"
  },
  {
    "path": "examples/sqlite/README.md",
    "content": "# Memori + SQLite Example\n\nExample showing how to use Memori with SQLite.\n\n## Quick Start\n\n1. **Install dependencies**:\n   ```bash\n   uv sync\n   ```\n\n2. **Set environment variables**:\n   ```bash\n   export OPENAI_API_KEY=your_api_key_here\n   ```\n\n3. **Run the example**:\n   ```bash\n   uv run python main.py\n   ```\n\n## What This Example Demonstrates\n\n- **Automatic persistence**: All conversation messages are automatically stored in the SQLite database\n- **Context preservation**: Memori injects relevant conversation history into each LLM call\n- **Interactive chat**: Type messages and see how Memori maintains context across the conversation\n- **Portable**: The database file can be copied, backed up, or shared easily\n"
  },
  {
    "path": "examples/sqlite/main.py",
    "content": "\"\"\"\nQuickstart: Memori + OpenAI + SQLite\n\nDemonstrates how Memori adds memory across conversations.\n\"\"\"\n\nimport os\n\nfrom openai import OpenAI\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom memori import Memori\n\n# Setup OpenAI\nclient = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\", \"<your_api_key_here>\"))\n\n# Setup SQLite\nengine = create_engine(\"sqlite:///memori.db\")\nSession = sessionmaker(bind=engine)\n\n# Setup Memori - that's it!\nmem = Memori(conn=Session).llm.register(client)\nmem.attribution(entity_id=\"user-123\", process_id=\"my-app\")\nmem.config.storage.build()\n\nif __name__ == \"__main__\":\n    # First conversation - establish facts\n    print(\"You: My favorite color is blue and I live in Paris\")\n    response1 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[\n            {\"role\": \"user\", \"content\": \"My favorite color is blue and I live in Paris\"}\n        ],\n    )\n    print(f\"AI: {response1.choices[0].message.content}\\n\")\n\n    # Second conversation - Memori recalls context automatically\n    print(\"You: What's my favorite color?\")\n    response2 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n    )\n    print(f\"AI: {response2.choices[0].message.content}\\n\")\n\n    # Third conversation - context is maintained\n    print(\"You: What city do I live in?\")\n    response3 = client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What city do I live in?\"}],\n    )\n    print(f\"AI: {response3.choices[0].message.content}\")\n\n    # Advanced Augmentation runs asynchronously to efficiently\n    # create memories. For this example, a short lived command\n    # line program, we need to wait for it to finish.\n    mem.augmentation.wait()\n"
  },
  {
    "path": "examples/sqlite/pyproject.toml",
    "content": "[project]\nname = \"memori-sqlite-example\"\nversion = \"0.1.0\"\ndescription = \"Memori SDK example with SQLite\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"memori>=3.0.0\",\n    \"openai>=2.6.1\",\n    \"SQLAlchemy>=2.0.0\",\n    \"python-dotenv>=1.2.1\",\n]\n"
  },
  {
    "path": "integrations/openclaw/.prettierrc.json",
    "content": "{\n  \"semi\": true,\n  \"trailingComma\": \"es5\",\n  \"singleQuote\": true,\n  \"printWidth\": 100,\n  \"tabWidth\": 2,\n  \"arrowParens\": \"always\"\n}\n"
  },
  {
    "path": "integrations/openclaw/README.md",
    "content": "[![Memori Labs](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/banner.png)](https://memorilabs.ai/)\n\n<p align=\"center\">\n  <strong>The memory fabric for enterprise AI</strong>\n</p>\n\n<p align=\"center\">\n  <i>Give OpenClaw persistent, structured memory with Memori. Capture what matters, recall it when relevant, and move from lightweight experimentation to production-ready memory infrastructure.</i>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@memorilabs/openclaw-memori\">\n    <img src=\"https://img.shields.io/npm/v/@memorilabs/openclaw-memori.svg\" alt=\"NPM version\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@memorilabs/openclaw-memori\">\n    <img src=\"https://img.shields.io/npm/dm/@memorilabs/openclaw-memori.svg\" alt=\"NPM Downloads\">\n  </a>\n  <a href=\"https://opensource.org/license/apache-2-0\">\n    <img src=\"https://img.shields.io/badge/license-Apache%202.0-blue\" alt=\"License\">\n  </a>\n  <a href=\"https://discord.gg/abD4eGym6v\">\n    <img src=\"https://img.shields.io/discord/1042405378304004156?logo=discord\" alt=\"Discord\">\n  </a>\n</p>\n\n---\n\n## Why Memori for OpenClaw?\n\nOpenClaw ships with a simple file-first memory system designed for lightweight experimentation. As deployments scale into production environments, teams often run into memory problems that need more structured, deterministic infrastructure.\n\nMemori provides a drop-in memory layer purpose-built for agentic systems running OpenClaw in production. It works through OpenClaw's plugin lifecycle, so you get persistent, structured memory without changing your agent logic.\n\n## Common Challenges with Default OpenClaw Memory\n\n### 1. Fact conflicts in long-running agents\n\nOpenClaw stores memory as plain markdown files. When facts change or contradict over time, there is no deterministic conflict resolution or lifecycle management.\n\nMemori introduces structured memory with update logic, decay policies, and deterministic fact handling.\n\n### 2. Context loss from token limits\n\nAs sessions grow, context must be compacted to fit within model token limits. Important details can be dropped during compression.\n\nMemori stores memory outside the prompt and retrieves the right facts at query time, eliminating compaction loss.\n\n### 3. No relationship reasoning\n\nOpenClaw retrieves semantically similar text but does not model relationships between entities.\n\nMemori builds structured memory graphs that let agents reason across linked facts, not just retrieve similar chunks.\n\n### 4. Cross-project noise\n\nWhen multiple projects share memory storage, irrelevant context can bleed across workflows.\n\nMemori supports scoped memory namespaces to isolate projects and workflows.\n\n### 5. No user-level isolation\n\nDefault memory systems do not provide deterministic isolation across users.\n\nMemori enforces user-scoped memory boundaries for secure multi-user deployments.\n\n## What Changes When You Add Memori?\n\nThe Memori plugin replaces OpenClaw's flat-file memory workflow with managed, structured memory that is scoped by `entity_id`, `process_id`, and `session_id` and enriched automatically through OpenClaw's existing hooks.\n\n| Capability | What changes |\n| --- | --- |\n| **Structured memory storage** | Instead of raw markdown blobs, Memori stores conversations, facts, preferences, and knowledge-graph triples as structured records tied to an entity, process, and session. Facts are extracted as subject-predicate-object relationships, deduplicated over time, and connected into a graph so related memories stay queryable instead of being buried in text files. |\n| **Advanced Augmentation** | After each conversation, Memori processes the user and assistant exchange asynchronously in the background, identifies facts, preferences, skills, and attributes, generates embeddings for semantic search, and updates the knowledge graph without blocking the agent's response path. |\n| **Intelligent Recall** | Before the agent responds, Memori searches the current entity's stored facts and knowledge graph, ranks memories by semantic relevance and importance, and injects the most useful context into the prompt so durable knowledge survives context-window compression. |\n| **Production-ready observability** | Memori Cloud gives you dashboard visibility into memory creation, recalls, cache hit rate, sessions, quota usage, top subjects, per-memory retrieval metrics, and knowledge-graph relationships, so you can inspect what was stored and how recall is behaving in production. |\n\nThe plugin still remains drop-in: OpenClaw handles the agent loop, while Memori adds recall, augmentation, sanitization, and observability around it.\n\n\n## Quickstart\n\nGet persistent memory running in your OpenClaw gateway in three steps.\n\n### Prerequisites\n\n- [OpenClaw](https://openclaw.ai) `v2026.3.2` or later\n- A Memori API key from [app.memorilabs.ai](https://app.memorilabs.ai)\n- An Entity ID to attribute memories to, such as a user ID, tenant ID, or agent name\n\n### 1. Install and Enable\n\nRun the following commands in your terminal to install and enable the plugin:\n\n```bash\n# 1. Install the plugin from npm\nopenclaw plugins install @memorilabs/openclaw-memori\n\n# 2. Enable it in your workspace\nopenclaw plugins enable openclaw-memori\n\n# 3. Restart the OpenClaw gateway\nopenclaw gateway restart\n```\n\n### 2. Configure\n\nThe plugin needs your Memori API key and an Entity ID to function. You can configure this via the OpenClaw CLI or your `openclaw.json` file.\n\n### Option A: Via OpenClaw CLI (Recommended)\n\n```bash\nopenclaw config set plugins.entries.openclaw-memori.config.apiKey \"YOUR_MEMORI_API_KEY\"\nopenclaw config set plugins.entries.openclaw-memori.config.entityId \"your-app-user-id\"\n```\n\n### Option B: Via `openclaw.json`\n\nAdd the following to your `~/.openclaw/openclaw.json` file:\n\n```json\n{\n  \"plugins\": {\n    \"entries\": {\n      \"openclaw-memori\": {\n        \"enabled\": true,\n        \"config\": {\n          \"apiKey\": \"your-memori-api-key\",\n          \"entityId\": \"your-app-user-id\"\n        }\n      }\n    }\n  }\n}\n```\n\n### Configuration Options\n\n| Option     | Type     | Required | Description                                                                                         |\n| ---------- | -------- | -------- | --------------------------------------------------------------------------------------------------- |\n| `apiKey`   | `string` | **Yes**  | Your Memori API key.                                                                                |\n| `entityId` | `string` | **Yes**  | The unique identifier for the entity (e.g., user, agent, or tenant) to attribute these memories to. |\n\n### 3. Verify\n\nRestart the gateway and inspect the logs:\n\n```bash\nopenclaw gateway restart\nopenclaw gateway logs --filter \"[Memori]\"\n```\n\nYou should see:\n\n```text\n[Memori] === INITIALIZING PLUGIN ===\n[Memori] Tracking Entity ID: your-app-user-id\n```\n\nTo test the full memory loop:\n\n1. Send a message with a durable preference:\n   `I always use TypeScript and prefer functional patterns.`\n2. Confirm augmentation ran:\n   `Augmentation successful!`\n3. Start a new session and ask:\n   `Write a hello world script.`\n4. Confirm recall ran:\n   `Successfully injected memory context.`\n\n## How It Works\n\nThis plugin integrates with OpenClaw's event lifecycle to provide persistent memory without interfering with the agent's core logic:\n\n1. **`before_prompt_build` (Intelligent Recall):** When a user sends a message, the plugin intercepts the event, queries the Memori API, and safely prepends relevant memories to the agent's system context.\n2. **`agent_end` (Advanced Augmentation):** Once the agent finishes generating its response, the plugin captures the final `user` and `assistant` messages, sanitizes them, and sends them to the Memori integration endpoint for long-term storage and entity mapping.\n\n## Contributing\n\nWe welcome contributions from the community! Please see our [Contributing Guidelines](https://github.com/MemoriLabs/Memori/blob/main/CONTRIBUTING.md) for details on code style, standards, and submitting pull requests.\n\nTo build from source:\n\n```bash\n# Clone the repository\ngit clone https://github.com/memorilabs/openclaw-memori.git\ncd openclaw-memori\n\n# Install dependencies and build\nnpm install\nnpm run build\n\n# Run formatting, linting, and type checking\nnpm run check\n```\n\n---\n\n## Support\n\n- **Documentation**: [https://memorilabs.ai/docs](https://memorilabs.ai/docs)\n- **Discord**: [https://discord.gg/abD4eGym6v](https://discord.gg/abD4eGym6v)\n- **Issues**: [GitHub Issues](https://github.com/memorilabs/openclaw-memori/issues)\n\n---\n\n## License\n\nApache 2.0 - see [LICENSE](https://github.com/MemoriLabs/Memori/blob/main/LICENSE)\n"
  },
  {
    "path": "integrations/openclaw/eslint.config.js",
    "content": "import eslint from '@eslint/js';\nimport tseslint from 'typescript-eslint';\nimport prettier from 'eslint-config-prettier';\nimport tsdoc from 'eslint-plugin-tsdoc';\n\nexport default tseslint.config(\n  eslint.configs.recommended,\n  ...tseslint.configs.strictTypeChecked,\n  prettier,\n  {\n    languageOptions: {\n      parserOptions: {\n        projectService: true,\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n    plugins: {\n      tsdoc: tsdoc,\n    },\n    rules: {\n      '@typescript-eslint/no-unused-vars': [\n        'error',\n        { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },\n      ],\n      '@typescript-eslint/no-explicit-any': 'warn',\n      '@typescript-eslint/no-extraneous-class': 'off',\n      '@typescript-eslint/restrict-template-expressions': 'off',\n      'tsdoc/syntax': 'warn',\n    },\n  },\n  {\n    files: ['tests/**/*.ts'],\n    rules: {\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/no-unsafe-assignment': 'off',\n      '@typescript-eslint/no-unsafe-member-access': 'off',\n      '@typescript-eslint/no-unsafe-argument': 'off',\n      '@typescript-eslint/no-unsafe-return': 'off',\n      '@typescript-eslint/no-unsafe-call': 'off',\n      '@typescript-eslint/require-await': 'off',\n      '@typescript-eslint/no-non-null-assertion': 'off',\n      '@typescript-eslint/unbound-method': 'off',\n    },\n  },\n  {\n    ignores: [\n      'dist/**',\n      'node_modules/**',\n      '*.config.js',\n      '*.config.ts',\n      'tests/**',\n      '**/*.test.ts',\n      '**/*.spec.ts',\n      'coverage',\n    ],\n  }\n);\n"
  },
  {
    "path": "integrations/openclaw/openclaw.plugin.json",
    "content": "{\n  \"id\": \"openclaw-memori\",\n  \"name\": \"Memori System\",\n  \"version\": \"0.0.2\",\n  \"description\": \"Hosted memory backend\",\n  \"kind\": \"memory\",\n  \"main\": \"dist/index.js\",\n  \"uiHints\": {\n    \"apiKey\": {\n      \"label\": \"Memori API Key\",\n      \"sensitive\": true,\n      \"placeholder\": \"Enter your Memori API key...\",\n      \"help\": \"API key from memorilabs.ai (or use MEMORI_API_KEY env var)\"\n    },\n    \"entityId\": {\n      \"label\": \"Entity ID\",\n      \"placeholder\": \"e.g., your-app-user-id\",\n      \"help\": \"Required. The unique identifier to attribute these memories to.\"\n    }\n  },\n  \"configSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"apiKey\": {\n        \"type\": \"string\",\n        \"title\": \"API Key\",\n        \"description\": \"Memori API Key\"\n      },\n      \"entityId\": {\n        \"type\": \"string\",\n        \"title\": \"Entity ID\",\n        \"description\": \"Required. Hardcode a specific Entity ID for memories.\"\n      }\n    },\n    \"required\": []\n  }\n}\n"
  },
  {
    "path": "integrations/openclaw/package-lock.json",
    "content": "{\n  \"name\": \"@memorilabs/openclaw-memori\",\n  \"version\": \"0.0.4\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"@memorilabs/openclaw-memori\",\n      \"version\": \"0.0.4\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@memorilabs/memori\": \"^0.0.5\"\n      },\n      \"devDependencies\": {\n        \"@eslint/js\": \"^9.0.0\",\n        \"@types/node\": \"^20.0.0\",\n        \"@vitest/coverage-v8\": \"^4.0.18\",\n        \"eslint\": \"^9.0.0\",\n        \"eslint-config-prettier\": \"^10.1.8\",\n        \"eslint-plugin-tsdoc\": \"^0.5.1\",\n        \"prettier\": \"^3.0.0\",\n        \"typescript\": \"^5.0.0\",\n        \"typescript-eslint\": \"^8.0.0\",\n        \"vitest\": \"^4.0.18\"\n      },\n      \"engines\": {\n        \"node\": \">=22.0.0\"\n      },\n      \"peerDependencies\": {\n        \"openclaw\": \"^2026.3.2\"\n      }\n    },\n    \"node_modules/@agentclientprotocol/sdk\": {\n      \"version\": \"0.16.1\",\n      \"resolved\": \"https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.16.1.tgz\",\n      \"integrity\": \"sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"peerDependencies\": {\n        \"zod\": \"^3.25.0 || ^4.0.0\"\n      }\n    },\n    \"node_modules/@anthropic-ai/sdk\": {\n      \"version\": \"0.73.0\",\n      \"resolved\": \"https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz\",\n      \"integrity\": \"sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"json-schema-to-ts\": \"^3.1.1\"\n      },\n      \"bin\": {\n        \"anthropic-ai-sdk\": \"bin/cli\"\n      },\n      \"peerDependencies\": {\n        \"zod\": \"^3.25.0 || ^4.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"zod\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@aws-crypto/crc32\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz\",\n      \"integrity\": \"sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-crypto/util\": \"^5.2.0\",\n        \"@aws-sdk/types\": \"^3.222.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=16.0.0\"\n      }\n    },\n    \"node_modules/@aws-crypto/sha256-browser\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz\",\n      \"integrity\": \"sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-crypto/sha256-js\": \"^5.2.0\",\n        \"@aws-crypto/supports-web-crypto\": \"^5.2.0\",\n        \"@aws-crypto/util\": \"^5.2.0\",\n        \"@aws-sdk/types\": \"^3.222.0\",\n        \"@aws-sdk/util-locate-window\": \"^3.0.0\",\n        \"@smithy/util-utf8\": \"^2.0.0\",\n        \"tslib\": \"^2.6.2\"\n      }\n    },\n    \"node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz\",\n      \"integrity\": \"sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz\",\n      \"integrity\": \"sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/is-array-buffer\": \"^2.2.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz\",\n      \"integrity\": \"sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/util-buffer-from\": \"^2.2.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@aws-crypto/sha256-js\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz\",\n      \"integrity\": \"sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-crypto/util\": \"^5.2.0\",\n        \"@aws-sdk/types\": \"^3.222.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=16.0.0\"\n      }\n    },\n    \"node_modules/@aws-crypto/supports-web-crypto\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz\",\n      \"integrity\": \"sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      }\n    },\n    \"node_modules/@aws-crypto/util\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz\",\n      \"integrity\": \"sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.222.0\",\n        \"@smithy/util-utf8\": \"^2.0.0\",\n        \"tslib\": \"^2.6.2\"\n      }\n    },\n    \"node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz\",\n      \"integrity\": \"sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz\",\n      \"integrity\": \"sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/is-array-buffer\": \"^2.2.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz\",\n      \"integrity\": \"sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/util-buffer-from\": \"^2.2.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/client-bedrock\": {\n      \"version\": \"3.1007.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/client-bedrock/-/client-bedrock-3.1007.0.tgz\",\n      \"integrity\": \"sha512-49hH8o6ALKkCiBUgg20HkwxNamP1yYA/n8Si73Z438EqhZGpCfScP3FfxVhrfD5o+4bV4Whi9BTzPKCa/PfUww==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-crypto/sha256-browser\": \"5.2.0\",\n        \"@aws-crypto/sha256-js\": \"5.2.0\",\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/credential-provider-node\": \"^3.972.19\",\n        \"@aws-sdk/middleware-host-header\": \"^3.972.7\",\n        \"@aws-sdk/middleware-logger\": \"^3.972.7\",\n        \"@aws-sdk/middleware-recursion-detection\": \"^3.972.7\",\n        \"@aws-sdk/middleware-user-agent\": \"^3.972.20\",\n        \"@aws-sdk/region-config-resolver\": \"^3.972.7\",\n        \"@aws-sdk/token-providers\": \"3.1007.0\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@aws-sdk/util-endpoints\": \"^3.996.4\",\n        \"@aws-sdk/util-user-agent-browser\": \"^3.972.7\",\n        \"@aws-sdk/util-user-agent-node\": \"^3.973.5\",\n        \"@smithy/config-resolver\": \"^4.4.10\",\n        \"@smithy/core\": \"^3.23.9\",\n        \"@smithy/fetch-http-handler\": \"^5.3.13\",\n        \"@smithy/hash-node\": \"^4.2.11\",\n        \"@smithy/invalid-dependency\": \"^4.2.11\",\n        \"@smithy/middleware-content-length\": \"^4.2.11\",\n        \"@smithy/middleware-endpoint\": \"^4.4.23\",\n        \"@smithy/middleware-retry\": \"^4.4.40\",\n        \"@smithy/middleware-serde\": \"^4.2.12\",\n        \"@smithy/middleware-stack\": \"^4.2.11\",\n        \"@smithy/node-config-provider\": \"^4.3.11\",\n        \"@smithy/node-http-handler\": \"^4.4.14\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/smithy-client\": \"^4.12.3\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"@smithy/url-parser\": \"^4.2.11\",\n        \"@smithy/util-base64\": \"^4.3.2\",\n        \"@smithy/util-body-length-browser\": \"^4.2.2\",\n        \"@smithy/util-body-length-node\": \"^4.2.3\",\n        \"@smithy/util-defaults-mode-browser\": \"^4.3.39\",\n        \"@smithy/util-defaults-mode-node\": \"^4.2.42\",\n        \"@smithy/util-endpoints\": \"^3.3.2\",\n        \"@smithy/util-middleware\": \"^4.2.11\",\n        \"@smithy/util-retry\": \"^4.2.11\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/client-bedrock-runtime\": {\n      \"version\": \"3.1007.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1007.0.tgz\",\n      \"integrity\": \"sha512-X7iWTQAZrCvQH2lfrZktVPfR3jdLPNtI4zkk4NA/vXzW5k8VNgdVuWUSm8cAzIXnhV3YThvDpLhEk87igNyGWQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-crypto/sha256-browser\": \"5.2.0\",\n        \"@aws-crypto/sha256-js\": \"5.2.0\",\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/credential-provider-node\": \"^3.972.19\",\n        \"@aws-sdk/eventstream-handler-node\": \"^3.972.10\",\n        \"@aws-sdk/middleware-eventstream\": \"^3.972.7\",\n        \"@aws-sdk/middleware-host-header\": \"^3.972.7\",\n        \"@aws-sdk/middleware-logger\": \"^3.972.7\",\n        \"@aws-sdk/middleware-recursion-detection\": \"^3.972.7\",\n        \"@aws-sdk/middleware-user-agent\": \"^3.972.20\",\n        \"@aws-sdk/middleware-websocket\": \"^3.972.12\",\n        \"@aws-sdk/region-config-resolver\": \"^3.972.7\",\n        \"@aws-sdk/token-providers\": \"3.1007.0\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@aws-sdk/util-endpoints\": \"^3.996.4\",\n        \"@aws-sdk/util-user-agent-browser\": \"^3.972.7\",\n        \"@aws-sdk/util-user-agent-node\": \"^3.973.5\",\n        \"@smithy/config-resolver\": \"^4.4.10\",\n        \"@smithy/core\": \"^3.23.9\",\n        \"@smithy/eventstream-serde-browser\": \"^4.2.11\",\n        \"@smithy/eventstream-serde-config-resolver\": \"^4.3.11\",\n        \"@smithy/eventstream-serde-node\": \"^4.2.11\",\n        \"@smithy/fetch-http-handler\": \"^5.3.13\",\n        \"@smithy/hash-node\": \"^4.2.11\",\n        \"@smithy/invalid-dependency\": \"^4.2.11\",\n        \"@smithy/middleware-content-length\": \"^4.2.11\",\n        \"@smithy/middleware-endpoint\": \"^4.4.23\",\n        \"@smithy/middleware-retry\": \"^4.4.40\",\n        \"@smithy/middleware-serde\": \"^4.2.12\",\n        \"@smithy/middleware-stack\": \"^4.2.11\",\n        \"@smithy/node-config-provider\": \"^4.3.11\",\n        \"@smithy/node-http-handler\": \"^4.4.14\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/smithy-client\": \"^4.12.3\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"@smithy/url-parser\": \"^4.2.11\",\n        \"@smithy/util-base64\": \"^4.3.2\",\n        \"@smithy/util-body-length-browser\": \"^4.2.2\",\n        \"@smithy/util-body-length-node\": \"^4.2.3\",\n        \"@smithy/util-defaults-mode-browser\": \"^4.3.39\",\n        \"@smithy/util-defaults-mode-node\": \"^4.2.42\",\n        \"@smithy/util-endpoints\": \"^3.3.2\",\n        \"@smithy/util-middleware\": \"^4.2.11\",\n        \"@smithy/util-retry\": \"^4.2.11\",\n        \"@smithy/util-stream\": \"^4.5.17\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/core\": {\n      \"version\": \"3.973.19\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.19.tgz\",\n      \"integrity\": \"sha512-56KePyOcZnKTWCd89oJS1G6j3HZ9Kc+bh/8+EbvtaCCXdP6T7O7NzCiPuHRhFLWnzXIaXX3CxAz0nI5My9spHQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@aws-sdk/xml-builder\": \"^3.972.10\",\n        \"@smithy/core\": \"^3.23.9\",\n        \"@smithy/node-config-provider\": \"^4.3.11\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/signature-v4\": \"^5.3.11\",\n        \"@smithy/smithy-client\": \"^4.12.3\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"@smithy/util-base64\": \"^4.3.2\",\n        \"@smithy/util-middleware\": \"^4.2.11\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-env\": {\n      \"version\": \"3.972.17\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.17.tgz\",\n      \"integrity\": \"sha512-MBAMW6YELzE1SdkOniqr51mrjapQUv8JXSGxtwRjQV0mwVDutVsn22OPAUt4RcLRvdiHQmNBDEFP9iTeSVCOlA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-http\": {\n      \"version\": \"3.972.19\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.19.tgz\",\n      \"integrity\": \"sha512-9EJROO8LXll5a7eUFqu48k6BChrtokbmgeMWmsH7lBb6lVbtjslUYz/ShLi+SHkYzTomiGBhmzTW7y+H4BxsnA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/fetch-http-handler\": \"^5.3.13\",\n        \"@smithy/node-http-handler\": \"^4.4.14\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/smithy-client\": \"^4.12.3\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"@smithy/util-stream\": \"^4.5.17\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-ini\": {\n      \"version\": \"3.972.18\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.18.tgz\",\n      \"integrity\": \"sha512-vthIAXJISZnj2576HeyLBj4WTeX+I7PwWeRkbOa0mVX39K13SCGxCgOFuKj2ytm9qTlLOmXe4cdEnroteFtJfw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/credential-provider-env\": \"^3.972.17\",\n        \"@aws-sdk/credential-provider-http\": \"^3.972.19\",\n        \"@aws-sdk/credential-provider-login\": \"^3.972.18\",\n        \"@aws-sdk/credential-provider-process\": \"^3.972.17\",\n        \"@aws-sdk/credential-provider-sso\": \"^3.972.18\",\n        \"@aws-sdk/credential-provider-web-identity\": \"^3.972.18\",\n        \"@aws-sdk/nested-clients\": \"^3.996.8\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/credential-provider-imds\": \"^4.2.11\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.6\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-login\": {\n      \"version\": \"3.972.18\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.18.tgz\",\n      \"integrity\": \"sha512-kINzc5BBxdYBkPZ0/i1AMPMOk5b5QaFNbYMElVw5QTX13AKj6jcxnv/YNl9oW9mg+Y08ti19hh01HhyEAxsSJQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/nested-clients\": \"^3.996.8\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.6\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-node\": {\n      \"version\": \"3.972.19\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.19.tgz\",\n      \"integrity\": \"sha512-yDWQ9dFTr+IMxwanFe7+tbN5++q8psZBjlUwOiCXn1EzANoBgtqBwcpYcHaMGtn0Wlfj4NuXdf2JaEx1lz5RaQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/credential-provider-env\": \"^3.972.17\",\n        \"@aws-sdk/credential-provider-http\": \"^3.972.19\",\n        \"@aws-sdk/credential-provider-ini\": \"^3.972.18\",\n        \"@aws-sdk/credential-provider-process\": \"^3.972.17\",\n        \"@aws-sdk/credential-provider-sso\": \"^3.972.18\",\n        \"@aws-sdk/credential-provider-web-identity\": \"^3.972.18\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/credential-provider-imds\": \"^4.2.11\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.6\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-process\": {\n      \"version\": \"3.972.17\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.17.tgz\",\n      \"integrity\": \"sha512-c8G8wT1axpJDgaP3xzcy+q8Y1fTi9A2eIQJvyhQ9xuXrUZhlCfXbC0vM9bM1CUXiZppFQ1p7g0tuUMvil/gCPg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.6\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-sso\": {\n      \"version\": \"3.972.18\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.18.tgz\",\n      \"integrity\": \"sha512-YHYEfj5S2aqInRt5ub8nDOX8vAxgMvd84wm2Y3WVNfFa/53vOv9T7WOAqXI25qjj3uEcV46xxfqdDQk04h5XQA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/nested-clients\": \"^3.996.8\",\n        \"@aws-sdk/token-providers\": \"3.1005.0\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.6\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers\": {\n      \"version\": \"3.1005.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1005.0.tgz\",\n      \"integrity\": \"sha512-vMxd+ivKqSxU9bHx5vmAlFKDAkjGotFU56IOkDa5DaTu1WWwbcse0yFHEm9I537oVvodaiwMl3VBwgHfzQ2rvw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/nested-clients\": \"^3.996.8\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.6\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/credential-provider-web-identity\": {\n      \"version\": \"3.972.18\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.18.tgz\",\n      \"integrity\": \"sha512-OqlEQpJ+J3T5B96qtC1zLLwkBloechP+fezKbCH0sbd2cCc0Ra55XpxWpk/hRj69xAOYtHvoC4orx6eTa4zU7g==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/nested-clients\": \"^3.996.8\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.6\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/eventstream-handler-node\": {\n      \"version\": \"3.972.10\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.10.tgz\",\n      \"integrity\": \"sha512-g2Z9s6Y4iNh0wICaEqutgYgt/Pmhv5Ev9G3eKGFe2w9VuZDhc76vYdop6I5OocmpHV79d4TuLG+JWg5rQIVDVA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/eventstream-codec\": \"^4.2.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/middleware-eventstream\": {\n      \"version\": \"3.972.7\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.7.tgz\",\n      \"integrity\": \"sha512-VWndapHYCfwLgPpCb/xwlMKG4imhFzKJzZcKOEioGn7OHY+6gdr0K7oqy1HZgbLa3ACznZ9fku+DzmAi8fUC0g==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/middleware-host-header\": {\n      \"version\": \"3.972.7\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz\",\n      \"integrity\": \"sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/middleware-logger\": {\n      \"version\": \"3.972.7\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz\",\n      \"integrity\": \"sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/middleware-recursion-detection\": {\n      \"version\": \"3.972.7\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz\",\n      \"integrity\": \"sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@aws/lambda-invoke-store\": \"^0.2.2\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/middleware-user-agent\": {\n      \"version\": \"3.972.20\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.20.tgz\",\n      \"integrity\": \"sha512-3kNTLtpUdeahxtnJRnj/oIdLAUdzTfr9N40KtxNhtdrq+Q1RPMdCJINRXq37m4t5+r3H70wgC3opW46OzFcZYA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@aws-sdk/util-endpoints\": \"^3.996.4\",\n        \"@smithy/core\": \"^3.23.9\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"@smithy/util-retry\": \"^4.2.11\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/middleware-websocket\": {\n      \"version\": \"3.972.12\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.12.tgz\",\n      \"integrity\": \"sha512-iyPP6FVDKe/5wy5ojC0akpDFG1vX3FeCUU47JuwN8xfvT66xlEI8qUJZPtN55TJVFzzWZJpWL78eqUE31md08Q==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@aws-sdk/util-format-url\": \"^3.972.7\",\n        \"@smithy/eventstream-codec\": \"^4.2.11\",\n        \"@smithy/eventstream-serde-browser\": \"^4.2.11\",\n        \"@smithy/fetch-http-handler\": \"^5.3.13\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/signature-v4\": \"^5.3.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"@smithy/util-base64\": \"^4.3.2\",\n        \"@smithy/util-hex-encoding\": \"^4.2.2\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 14.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/nested-clients\": {\n      \"version\": \"3.996.8\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.8.tgz\",\n      \"integrity\": \"sha512-6HlLm8ciMW8VzfB80kfIx16PBA9lOa9Dl+dmCBi78JDhvGlx3I7Rorwi5PpVRkL31RprXnYna3yBf6UKkD/PqA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-crypto/sha256-browser\": \"5.2.0\",\n        \"@aws-crypto/sha256-js\": \"5.2.0\",\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/middleware-host-header\": \"^3.972.7\",\n        \"@aws-sdk/middleware-logger\": \"^3.972.7\",\n        \"@aws-sdk/middleware-recursion-detection\": \"^3.972.7\",\n        \"@aws-sdk/middleware-user-agent\": \"^3.972.20\",\n        \"@aws-sdk/region-config-resolver\": \"^3.972.7\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@aws-sdk/util-endpoints\": \"^3.996.4\",\n        \"@aws-sdk/util-user-agent-browser\": \"^3.972.7\",\n        \"@aws-sdk/util-user-agent-node\": \"^3.973.5\",\n        \"@smithy/config-resolver\": \"^4.4.10\",\n        \"@smithy/core\": \"^3.23.9\",\n        \"@smithy/fetch-http-handler\": \"^5.3.13\",\n        \"@smithy/hash-node\": \"^4.2.11\",\n        \"@smithy/invalid-dependency\": \"^4.2.11\",\n        \"@smithy/middleware-content-length\": \"^4.2.11\",\n        \"@smithy/middleware-endpoint\": \"^4.4.23\",\n        \"@smithy/middleware-retry\": \"^4.4.40\",\n        \"@smithy/middleware-serde\": \"^4.2.12\",\n        \"@smithy/middleware-stack\": \"^4.2.11\",\n        \"@smithy/node-config-provider\": \"^4.3.11\",\n        \"@smithy/node-http-handler\": \"^4.4.14\",\n        \"@smithy/protocol-http\": \"^5.3.11\",\n        \"@smithy/smithy-client\": \"^4.12.3\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"@smithy/url-parser\": \"^4.2.11\",\n        \"@smithy/util-base64\": \"^4.3.2\",\n        \"@smithy/util-body-length-browser\": \"^4.2.2\",\n        \"@smithy/util-body-length-node\": \"^4.2.3\",\n        \"@smithy/util-defaults-mode-browser\": \"^4.3.39\",\n        \"@smithy/util-defaults-mode-node\": \"^4.2.42\",\n        \"@smithy/util-endpoints\": \"^3.3.2\",\n        \"@smithy/util-middleware\": \"^4.2.11\",\n        \"@smithy/util-retry\": \"^4.2.11\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/region-config-resolver\": {\n      \"version\": \"3.972.7\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz\",\n      \"integrity\": \"sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/config-resolver\": \"^4.4.10\",\n        \"@smithy/node-config-provider\": \"^4.3.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/token-providers\": {\n      \"version\": \"3.1007.0\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1007.0.tgz\",\n      \"integrity\": \"sha512-kKvVyr53vvVc5k6RbvI6jhafxufxO2SkEw8QeEzJqwOXH/IMY7Cm0IyhnBGdqj80iiIIiIM2jGe7Fn3TIdwdrw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/core\": \"^3.973.19\",\n        \"@aws-sdk/nested-clients\": \"^3.996.8\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/property-provider\": \"^4.2.11\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.6\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/types\": {\n      \"version\": \"3.973.5\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz\",\n      \"integrity\": \"sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/util-endpoints\": {\n      \"version\": \"3.996.4\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz\",\n      \"integrity\": \"sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"@smithy/url-parser\": \"^4.2.11\",\n        \"@smithy/util-endpoints\": \"^3.3.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/util-format-url\": {\n      \"version\": \"3.972.7\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.7.tgz\",\n      \"integrity\": \"sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/querystring-builder\": \"^4.2.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/util-locate-window\": {\n      \"version\": \"3.965.5\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz\",\n      \"integrity\": \"sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws-sdk/util-user-agent-browser\": {\n      \"version\": \"3.972.7\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz\",\n      \"integrity\": \"sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"bowser\": \"^2.11.0\",\n        \"tslib\": \"^2.6.2\"\n      }\n    },\n    \"node_modules/@aws-sdk/util-user-agent-node\": {\n      \"version\": \"3.973.5\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.5.tgz\",\n      \"integrity\": \"sha512-Dyy38O4GeMk7UQ48RupfHif//gqnOPbq/zlvRssc11E2mClT+aUfc3VS2yD8oLtzqO3RsqQ9I3gOBB4/+HjPOw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-sdk/middleware-user-agent\": \"^3.972.20\",\n        \"@aws-sdk/types\": \"^3.973.5\",\n        \"@smithy/node-config-provider\": \"^4.3.11\",\n        \"@smithy/types\": \"^4.13.0\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      },\n      \"peerDependencies\": {\n        \"aws-crt\": \">=1.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"aws-crt\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@aws-sdk/xml-builder\": {\n      \"version\": \"3.972.10\",\n      \"resolved\": \"https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz\",\n      \"integrity\": \"sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.0\",\n        \"fast-xml-parser\": \"5.4.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@aws/lambda-invoke-store\": {\n      \"version\": \"0.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz\",\n      \"integrity\": \"sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@babel/helper-string-parser\": {\n      \"version\": \"7.27.1\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz\",\n      \"integrity\": \"sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helper-validator-identifier\": {\n      \"version\": \"7.28.5\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz\",\n      \"integrity\": \"sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/parser\": {\n      \"version\": \"7.29.0\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz\",\n      \"integrity\": \"sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/types\": \"^7.29.0\"\n      },\n      \"bin\": {\n        \"parser\": \"bin/babel-parser.js\"\n      },\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/@babel/runtime\": {\n      \"version\": \"7.28.6\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz\",\n      \"integrity\": \"sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/types\": {\n      \"version\": \"7.29.0\",\n      \"resolved\": \"https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz\",\n      \"integrity\": \"sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/helper-string-parser\": \"^7.27.1\",\n        \"@babel/helper-validator-identifier\": \"^7.28.5\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@bcoe/v8-coverage\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz\",\n      \"integrity\": \"sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@borewit/text-codec\": {\n      \"version\": \"0.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz\",\n      \"integrity\": \"sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Borewit\"\n      }\n    },\n    \"node_modules/@buape/carbon\": {\n      \"version\": \"0.0.0-beta-20260216184201\",\n      \"resolved\": \"https://registry.npmjs.org/@buape/carbon/-/carbon-0.0.0-beta-20260216184201.tgz\",\n      \"integrity\": \"sha512-u5mgYcigfPVqT7D9gVTGd+3YSflTreQmrWog7ORbb0z5w9eT8ft4rJOdw9fGwr75zMu9kXpSBaAcY2eZoJFSdA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \"^25.0.9\",\n        \"discord-api-types\": \"0.38.37\"\n      },\n      \"optionalDependencies\": {\n        \"@cloudflare/workers-types\": \"4.20260120.0\",\n        \"@discordjs/voice\": \"0.19.0\",\n        \"@hono/node-server\": \"1.19.9\",\n        \"@types/bun\": \"1.3.9\",\n        \"@types/ws\": \"8.18.1\",\n        \"ws\": \"8.19.0\"\n      }\n    },\n    \"node_modules/@buape/carbon/node_modules/@discordjs/voice\": {\n      \"version\": \"0.19.0\",\n      \"resolved\": \"https://registry.npmjs.org/@discordjs/voice/-/voice-0.19.0.tgz\",\n      \"integrity\": \"sha512-UyX6rGEXzVyPzb1yvjHtPfTlnLvB5jX/stAMdiytHhfoydX+98hfympdOwsnTktzr+IRvphxTbdErgYDJkEsvw==\",\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/ws\": \"^8.18.1\",\n        \"discord-api-types\": \"^0.38.16\",\n        \"prism-media\": \"^1.3.5\",\n        \"tslib\": \"^2.8.1\",\n        \"ws\": \"^8.18.3\"\n      },\n      \"engines\": {\n        \"node\": \">=22.12.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/discordjs/discord.js?sponsor\"\n      }\n    },\n    \"node_modules/@buape/carbon/node_modules/@types/node\": {\n      \"version\": \"25.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz\",\n      \"integrity\": \"sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"undici-types\": \"~7.18.0\"\n      }\n    },\n    \"node_modules/@buape/carbon/node_modules/discord-api-types\": {\n      \"version\": \"0.38.37\",\n      \"resolved\": \"https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.37.tgz\",\n      \"integrity\": \"sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"workspaces\": [\n        \"scripts/actions/documentation\"\n      ]\n    },\n    \"node_modules/@buape/carbon/node_modules/opusscript\": {\n      \"version\": \"0.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz\",\n      \"integrity\": \"sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==\",\n      \"extraneous\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@buape/carbon/node_modules/prism-media\": {\n      \"version\": \"1.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz\",\n      \"integrity\": \"sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==\",\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"peer\": true,\n      \"peerDependencies\": {\n        \"@discordjs/opus\": \">=0.8.0 <1.0.0\",\n        \"ffmpeg-static\": \"^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0\",\n        \"node-opus\": \"^0.3.3\",\n        \"opusscript\": \"^0.0.8\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@discordjs/opus\": {\n          \"optional\": true\n        },\n        \"ffmpeg-static\": {\n          \"optional\": true\n        },\n        \"node-opus\": {\n          \"optional\": true\n        },\n        \"opusscript\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@buape/carbon/node_modules/undici-types\": {\n      \"version\": \"7.18.2\",\n      \"resolved\": \"https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz\",\n      \"integrity\": \"sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@cacheable/memory\": {\n      \"version\": \"2.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz\",\n      \"integrity\": \"sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@cacheable/utils\": \"^2.4.0\",\n        \"@keyv/bigmap\": \"^1.3.1\",\n        \"hookified\": \"^1.15.1\",\n        \"keyv\": \"^5.6.0\"\n      }\n    },\n    \"node_modules/@cacheable/memory/node_modules/@keyv/bigmap\": {\n      \"version\": \"1.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz\",\n      \"integrity\": \"sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"hashery\": \"^1.4.0\",\n        \"hookified\": \"^1.15.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\"\n      },\n      \"peerDependencies\": {\n        \"keyv\": \"^5.6.0\"\n      }\n    },\n    \"node_modules/@cacheable/memory/node_modules/keyv\": {\n      \"version\": \"5.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz\",\n      \"integrity\": \"sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@keyv/serialize\": \"^1.1.1\"\n      }\n    },\n    \"node_modules/@cacheable/node-cache\": {\n      \"version\": \"1.7.6\",\n      \"resolved\": \"https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz\",\n      \"integrity\": \"sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"cacheable\": \"^2.3.1\",\n        \"hookified\": \"^1.14.0\",\n        \"keyv\": \"^5.5.5\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@cacheable/node-cache/node_modules/keyv\": {\n      \"version\": \"5.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz\",\n      \"integrity\": \"sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@keyv/serialize\": \"^1.1.1\"\n      }\n    },\n    \"node_modules/@cacheable/utils\": {\n      \"version\": \"2.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.0.tgz\",\n      \"integrity\": \"sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"hashery\": \"^1.5.0\",\n        \"keyv\": \"^5.6.0\"\n      }\n    },\n    \"node_modules/@cacheable/utils/node_modules/keyv\": {\n      \"version\": \"5.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz\",\n      \"integrity\": \"sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@keyv/serialize\": \"^1.1.1\"\n      }\n    },\n    \"node_modules/@clack/core\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@clack/core/-/core-1.1.0.tgz\",\n      \"integrity\": \"sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"sisteransi\": \"^1.0.5\"\n      }\n    },\n    \"node_modules/@clack/prompts\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@clack/prompts/-/prompts-1.1.0.tgz\",\n      \"integrity\": \"sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@clack/core\": \"1.1.0\",\n        \"sisteransi\": \"^1.0.5\"\n      }\n    },\n    \"node_modules/@cloudflare/workers-types\": {\n      \"version\": \"4.20260120.0\",\n      \"resolved\": \"https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260120.0.tgz\",\n      \"integrity\": \"sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw==\",\n      \"license\": \"MIT OR Apache-2.0\",\n      \"optional\": true,\n      \"peer\": true\n    },\n    \"node_modules/@discordjs/voice\": {\n      \"version\": \"0.19.1\",\n      \"resolved\": \"https://registry.npmjs.org/@discordjs/voice/-/voice-0.19.1.tgz\",\n      \"integrity\": \"sha512-XYbFVyUBB7zhRvrjREfiWDwio24nEp/vFaVe6u9aBIC5UYuT7HvoMt8LgNfZ5hOyaCW0flFr72pkhUGz+gWw4Q==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@snazzah/davey\": \"^0.1.9\",\n        \"@types/ws\": \"^8.18.1\",\n        \"discord-api-types\": \"^0.38.41\",\n        \"prism-media\": \"^1.3.5\",\n        \"tslib\": \"^2.8.1\",\n        \"ws\": \"^8.19.0\"\n      },\n      \"engines\": {\n        \"node\": \">=22.12.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/discordjs/discord.js?sponsor\"\n      }\n    },\n    \"node_modules/@discordjs/voice/node_modules/opusscript\": {\n      \"version\": \"0.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz\",\n      \"integrity\": \"sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==\",\n      \"extraneous\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@discordjs/voice/node_modules/prism-media\": {\n      \"version\": \"1.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz\",\n      \"integrity\": \"sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"peerDependencies\": {\n        \"@discordjs/opus\": \">=0.8.0 <1.0.0\",\n        \"ffmpeg-static\": \"^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0\",\n        \"node-opus\": \"^0.3.3\",\n        \"opusscript\": \"^0.0.8\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@discordjs/opus\": {\n          \"optional\": true\n        },\n        \"ffmpeg-static\": {\n          \"optional\": true\n        },\n        \"node-opus\": {\n          \"optional\": true\n        },\n        \"opusscript\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@emnapi/core\": {\n      \"version\": \"1.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz\",\n      \"integrity\": \"sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"@emnapi/wasi-threads\": \"1.2.0\",\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/@emnapi/runtime\": {\n      \"version\": \"1.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz\",\n      \"integrity\": \"sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/@emnapi/wasi-threads\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz\",\n      \"integrity\": \"sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/@eslint-community/eslint-utils\": {\n      \"version\": \"4.9.1\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz\",\n      \"integrity\": \"sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"eslint-visitor-keys\": \"^3.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^12.22.0 || ^14.17.0 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^6.0.0 || ^7.0.0 || >=8.0.0\"\n      }\n    },\n    \"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys\": {\n      \"version\": \"3.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz\",\n      \"integrity\": \"sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^12.22.0 || ^14.17.0 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@eslint-community/regexpp\": {\n      \"version\": \"4.12.2\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz\",\n      \"integrity\": \"sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^12.0.0 || ^14.0.0 || >=16.0.0\"\n      }\n    },\n    \"node_modules/@eslint/config-array\": {\n      \"version\": \"0.21.2\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz\",\n      \"integrity\": \"sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/object-schema\": \"^2.1.7\",\n        \"debug\": \"^4.3.1\",\n        \"minimatch\": \"^3.1.5\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/config-helpers\": {\n      \"version\": \"0.4.2\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz\",\n      \"integrity\": \"sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/core\": \"^0.17.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/core\": {\n      \"version\": \"0.17.0\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz\",\n      \"integrity\": \"sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@types/json-schema\": \"^7.0.15\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/eslintrc\": {\n      \"version\": \"3.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz\",\n      \"integrity\": \"sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ajv\": \"^6.14.0\",\n        \"debug\": \"^4.3.2\",\n        \"espree\": \"^10.0.1\",\n        \"globals\": \"^14.0.0\",\n        \"ignore\": \"^5.2.0\",\n        \"import-fresh\": \"^3.2.1\",\n        \"js-yaml\": \"^4.1.1\",\n        \"minimatch\": \"^3.1.5\",\n        \"strip-json-comments\": \"^3.1.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@eslint/js\": {\n      \"version\": \"9.39.4\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz\",\n      \"integrity\": \"sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://eslint.org/donate\"\n      }\n    },\n    \"node_modules/@eslint/object-schema\": {\n      \"version\": \"2.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz\",\n      \"integrity\": \"sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/plugin-kit\": {\n      \"version\": \"0.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz\",\n      \"integrity\": \"sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/core\": \"^0.17.0\",\n        \"levn\": \"^0.4.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@google/genai\": {\n      \"version\": \"1.44.0\",\n      \"resolved\": \"https://registry.npmjs.org/@google/genai/-/genai-1.44.0.tgz\",\n      \"integrity\": \"sha512-kRt9ZtuXmz+tLlcNntN/VV4LRdpl6ZOu5B1KbfNgfR65db15O6sUQcwnwLka8sT/V6qysD93fWrgJHF2L7dA9A==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"google-auth-library\": \"^10.3.0\",\n        \"p-retry\": \"^4.6.2\",\n        \"protobufjs\": \"^7.5.4\",\n        \"ws\": \"^8.18.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      },\n      \"peerDependencies\": {\n        \"@modelcontextprotocol/sdk\": \"^1.25.2\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@modelcontextprotocol/sdk\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@grammyjs/runner\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz\",\n      \"integrity\": \"sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"abort-controller\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12.20.0 || >=14.13.1\"\n      },\n      \"peerDependencies\": {\n        \"grammy\": \"^1.13.1\"\n      }\n    },\n    \"node_modules/@grammyjs/transformer-throttler\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/@grammyjs/transformer-throttler/-/transformer-throttler-1.2.1.tgz\",\n      \"integrity\": \"sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"bottleneck\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^12.20.0 || >=14.13.1\"\n      },\n      \"peerDependencies\": {\n        \"grammy\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/@grammyjs/types\": {\n      \"version\": \"3.25.0\",\n      \"resolved\": \"https://registry.npmjs.org/@grammyjs/types/-/types-3.25.0.tgz\",\n      \"integrity\": \"sha512-iN9i5p+8ZOu9OMxWNcguojQfz4K/PDyMPOnL7PPCON+SoA/F8OKMH3uR7CVUkYfdNe0GCz8QOzAWrnqusQYFOg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@hapi/boom\": {\n      \"version\": \"9.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz\",\n      \"integrity\": \"sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@hapi/hoek\": \"9.x.x\"\n      }\n    },\n    \"node_modules/@hapi/hoek\": {\n      \"version\": \"9.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz\",\n      \"integrity\": \"sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@homebridge/ciao\": {\n      \"version\": \"1.3.5\",\n      \"resolved\": \"https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.5.tgz\",\n      \"integrity\": \"sha512-f7MAw7YuoEYgJEQ1VyRcLHGuVmCpmXi65GVR8CAtPWPqIZf/HFr4vHzVpOfQMpEQw9Pt5uh07guuLt5HE8ruog==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"debug\": \"^4.4.3\",\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"source-map-support\": \"^0.5.21\",\n        \"tslib\": \"^2.8.1\"\n      },\n      \"bin\": {\n        \"ciao-bcs\": \"lib/bonjour-conformance-testing.js\"\n      }\n    },\n    \"node_modules/@hono/node-server\": {\n      \"version\": \"1.19.11\",\n      \"resolved\": \"https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz\",\n      \"integrity\": \"sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18.14.1\"\n      },\n      \"peerDependencies\": {\n        \"hono\": \"^4\"\n      }\n    },\n    \"node_modules/@huggingface/jinja\": {\n      \"version\": \"0.5.6\",\n      \"resolved\": \"https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz\",\n      \"integrity\": \"sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@humanfs/core\": {\n      \"version\": \"0.19.1\",\n      \"resolved\": \"https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz\",\n      \"integrity\": \"sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.18.0\"\n      }\n    },\n    \"node_modules/@humanfs/node\": {\n      \"version\": \"0.16.7\",\n      \"resolved\": \"https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz\",\n      \"integrity\": \"sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@humanfs/core\": \"^0.19.1\",\n        \"@humanwhocodes/retry\": \"^0.4.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18.18.0\"\n      }\n    },\n    \"node_modules/@humanwhocodes/module-importer\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz\",\n      \"integrity\": \"sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=12.22\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/nzakas\"\n      }\n    },\n    \"node_modules/@humanwhocodes/retry\": {\n      \"version\": \"0.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz\",\n      \"integrity\": \"sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.18\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/nzakas\"\n      }\n    },\n    \"node_modules/@img/colour\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz\",\n      \"integrity\": \"sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/@img/sharp-darwin-arm64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz\",\n      \"integrity\": \"sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-darwin-arm64\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-darwin-x64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz\",\n      \"integrity\": \"sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-darwin-x64\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-darwin-arm64\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz\",\n      \"integrity\": \"sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-darwin-x64\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz\",\n      \"integrity\": \"sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-arm\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz\",\n      \"integrity\": \"sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-arm64\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz\",\n      \"integrity\": \"sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-ppc64\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz\",\n      \"integrity\": \"sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-riscv64\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz\",\n      \"integrity\": \"sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-s390x\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz\",\n      \"integrity\": \"sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linux-x64\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz\",\n      \"integrity\": \"sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linuxmusl-arm64\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz\",\n      \"integrity\": \"sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-libvips-linuxmusl-x64\": {\n      \"version\": \"1.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz\",\n      \"integrity\": \"sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-arm\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz\",\n      \"integrity\": \"sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-arm\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-arm64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz\",\n      \"integrity\": \"sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-arm64\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-ppc64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz\",\n      \"integrity\": \"sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-ppc64\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-riscv64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz\",\n      \"integrity\": \"sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-riscv64\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-s390x\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz\",\n      \"integrity\": \"sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-s390x\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-linux-x64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz\",\n      \"integrity\": \"sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linux-x64\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-linuxmusl-arm64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz\",\n      \"integrity\": \"sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linuxmusl-arm64\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-linuxmusl-x64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz\",\n      \"integrity\": \"sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"Apache-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-libvips-linuxmusl-x64\": \"1.2.4\"\n      }\n    },\n    \"node_modules/@img/sharp-wasm32\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz\",\n      \"integrity\": \"sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==\",\n      \"cpu\": [\n        \"wasm32\"\n      ],\n      \"license\": \"Apache-2.0 AND LGPL-3.0-or-later AND MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"dependencies\": {\n        \"@emnapi/runtime\": \"^1.7.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-win32-arm64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz\",\n      \"integrity\": \"sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"Apache-2.0 AND LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-win32-ia32\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz\",\n      \"integrity\": \"sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"license\": \"Apache-2.0 AND LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@img/sharp-win32-x64\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz\",\n      \"integrity\": \"sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"Apache-2.0 AND LGPL-3.0-or-later\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      }\n    },\n    \"node_modules/@isaacs/cliui\": {\n      \"version\": \"8.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz\",\n      \"integrity\": \"sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"string-width\": \"^5.1.2\",\n        \"string-width-cjs\": \"npm:string-width@^4.2.0\",\n        \"strip-ansi\": \"^7.0.1\",\n        \"strip-ansi-cjs\": \"npm:strip-ansi@^6.0.1\",\n        \"wrap-ansi\": \"^8.1.0\",\n        \"wrap-ansi-cjs\": \"npm:wrap-ansi@^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@isaacs/cliui/node_modules/ansi-styles\": {\n      \"version\": \"6.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz\",\n      \"integrity\": \"sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/@isaacs/cliui/node_modules/emoji-regex\": {\n      \"version\": \"9.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz\",\n      \"integrity\": \"sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@isaacs/cliui/node_modules/string-width\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz\",\n      \"integrity\": \"sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"eastasianwidth\": \"^0.2.0\",\n        \"emoji-regex\": \"^9.2.2\",\n        \"strip-ansi\": \"^7.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/@isaacs/cliui/node_modules/wrap-ansi\": {\n      \"version\": \"8.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz\",\n      \"integrity\": \"sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-styles\": \"^6.1.0\",\n        \"string-width\": \"^5.0.1\",\n        \"strip-ansi\": \"^7.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/@isaacs/fs-minipass\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz\",\n      \"integrity\": \"sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"minipass\": \"^7.0.4\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@jridgewell/resolve-uri\": {\n      \"version\": \"3.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz\",\n      \"integrity\": \"sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/@jridgewell/sourcemap-codec\": {\n      \"version\": \"1.5.5\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz\",\n      \"integrity\": \"sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@jridgewell/trace-mapping\": {\n      \"version\": \"0.3.31\",\n      \"resolved\": \"https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz\",\n      \"integrity\": \"sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/resolve-uri\": \"^3.1.0\",\n        \"@jridgewell/sourcemap-codec\": \"^1.4.14\"\n      }\n    },\n    \"node_modules/@keyv/serialize\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz\",\n      \"integrity\": \"sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@kwsites/file-exists\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz\",\n      \"integrity\": \"sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"debug\": \"^4.1.1\"\n      }\n    },\n    \"node_modules/@kwsites/promise-deferred\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz\",\n      \"integrity\": \"sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@larksuiteoapi/node-sdk\": {\n      \"version\": \"1.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@larksuiteoapi/node-sdk/-/node-sdk-1.59.0.tgz\",\n      \"integrity\": \"sha512-sBpkruTvZDOxnVtoTbepWKRX0j1Y1ZElQYu0x7+v088sI9pcpbVp6ZzCGn62dhrKPatzNyCJyzYCPXPYQWccrA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"axios\": \"~1.13.3\",\n        \"lodash.identity\": \"^3.0.0\",\n        \"lodash.merge\": \"^4.6.2\",\n        \"lodash.pickby\": \"^4.6.0\",\n        \"protobufjs\": \"^7.2.6\",\n        \"qs\": \"^6.14.2\",\n        \"ws\": \"^8.19.0\"\n      }\n    },\n    \"node_modules/@line/bot-sdk\": {\n      \"version\": \"10.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-10.6.0.tgz\",\n      \"integrity\": \"sha512-4hSpglL/G/cW2JCcohaYz/BS0uOSJNV9IEYdMm0EiPEvDLayoI2hGq2D86uYPQFD2gvgkyhmzdShpWLG3P5r3w==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \"^24.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"optionalDependencies\": {\n        \"axios\": \"^1.7.4\"\n      }\n    },\n    \"node_modules/@line/bot-sdk/node_modules/@types/node\": {\n      \"version\": \"24.12.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz\",\n      \"integrity\": \"sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"undici-types\": \"~7.16.0\"\n      }\n    },\n    \"node_modules/@line/bot-sdk/node_modules/undici-types\": {\n      \"version\": \"7.16.0\",\n      \"resolved\": \"https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz\",\n      \"integrity\": \"sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@lydell/node-pty\": {\n      \"version\": \"1.2.0-beta.3\",\n      \"resolved\": \"https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.2.0-beta.3.tgz\",\n      \"integrity\": \"sha512-ngGAItlRhmJXrhspxt8kX13n1dVFqzETOq0m/+gqSkO8NJBvNMwP7FZckMwps2UFySdr4yxCXNGu/bumg5at6A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"optionalDependencies\": {\n        \"@lydell/node-pty-darwin-arm64\": \"1.2.0-beta.3\",\n        \"@lydell/node-pty-darwin-x64\": \"1.2.0-beta.3\",\n        \"@lydell/node-pty-linux-arm64\": \"1.2.0-beta.3\",\n        \"@lydell/node-pty-linux-x64\": \"1.2.0-beta.3\",\n        \"@lydell/node-pty-win32-arm64\": \"1.2.0-beta.3\",\n        \"@lydell/node-pty-win32-x64\": \"1.2.0-beta.3\"\n      }\n    },\n    \"node_modules/@lydell/node-pty-darwin-arm64\": {\n      \"version\": \"1.2.0-beta.3\",\n      \"resolved\": \"https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.2.0-beta.3.tgz\",\n      \"integrity\": \"sha512-owcv+e1/OSu3bf9ZBdUQqJsQF888KyuSIiPYFNn0fLhgkhm9F3Pvha76Kj5mCPnodf7hh3suDe7upw7GPRXftQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/@lydell/node-pty-darwin-x64\": {\n      \"version\": \"1.2.0-beta.3\",\n      \"resolved\": \"https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.2.0-beta.3.tgz\",\n      \"integrity\": \"sha512-k38O+UviWrWdxtqZBBc/D8NJU11Rey8Y2YMwSWNxLv3eXZZdF5IVpbBkI/2RmLsV5nCcciqLPbukxeZnEfPlwA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/@lydell/node-pty-linux-arm64\": {\n      \"version\": \"1.2.0-beta.3\",\n      \"resolved\": \"https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.2.0-beta.3.tgz\",\n      \"integrity\": \"sha512-HUwRpGu3O+4sv9DAQFKnyW5LYhyYu2SDUa/bdFO/t4dIFCM4uDJEq47wfRM7+aYtJTi1b3lakN8SlWeuFQqJQQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/@lydell/node-pty-linux-x64\": {\n      \"version\": \"1.2.0-beta.3\",\n      \"resolved\": \"https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.2.0-beta.3.tgz\",\n      \"integrity\": \"sha512-+RRY0PoCUeQaCvPR7/UnkGbxulwbFtoTWJfe+o4T1RcNtngrgaI55I9nl8CD8uqhGrB3smKuyvPM5UtwGhASUw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/@lydell/node-pty-win32-arm64\": {\n      \"version\": \"1.2.0-beta.3\",\n      \"resolved\": \"https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.2.0-beta.3.tgz\",\n      \"integrity\": \"sha512-UEDd9ASp2M3iIYpIzfmfBlpyn4+K1G4CAjYcHWStptCkefoSVXWTiUBIa1KjBjZi3/xmsHIDpBEYTkGWuvLt2Q==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/@lydell/node-pty-win32-x64\": {\n      \"version\": \"1.2.0-beta.3\",\n      \"resolved\": \"https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.2.0-beta.3.tgz\",\n      \"integrity\": \"sha512-TpdqSFYx7/Rj+68tuP6F/lkRYrHCYAIJgaS1bx3SctTkb5QAQCFwOKHd4xlsivmEOMT2LdhkJggPxwX9PAO5pQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/@mariozechner/clipboard\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.2.tgz\",\n      \"integrity\": \"sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"optionalDependencies\": {\n        \"@mariozechner/clipboard-darwin-arm64\": \"0.3.2\",\n        \"@mariozechner/clipboard-darwin-universal\": \"0.3.2\",\n        \"@mariozechner/clipboard-darwin-x64\": \"0.3.2\",\n        \"@mariozechner/clipboard-linux-arm64-gnu\": \"0.3.2\",\n        \"@mariozechner/clipboard-linux-arm64-musl\": \"0.3.2\",\n        \"@mariozechner/clipboard-linux-riscv64-gnu\": \"0.3.2\",\n        \"@mariozechner/clipboard-linux-x64-gnu\": \"0.3.2\",\n        \"@mariozechner/clipboard-linux-x64-musl\": \"0.3.2\",\n        \"@mariozechner/clipboard-win32-arm64-msvc\": \"0.3.2\",\n        \"@mariozechner/clipboard-win32-x64-msvc\": \"0.3.2\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-darwin-arm64\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz\",\n      \"integrity\": \"sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-darwin-universal\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz\",\n      \"integrity\": \"sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-darwin-x64\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.2.tgz\",\n      \"integrity\": \"sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-linux-arm64-gnu\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.2.tgz\",\n      \"integrity\": \"sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-linux-arm64-musl\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.2.tgz\",\n      \"integrity\": \"sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-linux-riscv64-gnu\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.2.tgz\",\n      \"integrity\": \"sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-linux-x64-gnu\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.2.tgz\",\n      \"integrity\": \"sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-linux-x64-musl\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.2.tgz\",\n      \"integrity\": \"sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-win32-arm64-msvc\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.2.tgz\",\n      \"integrity\": \"sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/clipboard-win32-x64-msvc\": {\n      \"version\": \"0.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.2.tgz\",\n      \"integrity\": \"sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@mariozechner/jiti\": {\n      \"version\": \"2.6.5\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz\",\n      \"integrity\": \"sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"std-env\": \"^3.10.0\",\n        \"yoctocolors\": \"^2.1.2\"\n      },\n      \"bin\": {\n        \"jiti\": \"lib/jiti-cli.mjs\"\n      }\n    },\n    \"node_modules/@mariozechner/jiti/node_modules/std-env\": {\n      \"version\": \"3.10.0\",\n      \"resolved\": \"https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz\",\n      \"integrity\": \"sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@mariozechner/pi-agent-core\": {\n      \"version\": \"0.57.1\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.57.1.tgz\",\n      \"integrity\": \"sha512-WXsBbkNWOObFGHkhixaT8GXJpHDd3+fn8QntYF+4R8Sa9WB90ENXWidO6b7vcKX+JX0jjO5dIsQxmzosARJKlg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@mariozechner/pi-ai\": \"^0.57.1\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-ai\": {\n      \"version\": \"0.57.1\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.57.1.tgz\",\n      \"integrity\": \"sha512-Bd/J4a3YpdzJVyHLih0vDSdB0QPL4ti0XsAwtHOK/8eVhB0fHM1CpcgIrcBFJ23TMcKXMi0qamz18ERfp8tmgg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@anthropic-ai/sdk\": \"^0.73.0\",\n        \"@aws-sdk/client-bedrock-runtime\": \"^3.983.0\",\n        \"@google/genai\": \"^1.40.0\",\n        \"@mistralai/mistralai\": \"1.14.1\",\n        \"@sinclair/typebox\": \"^0.34.41\",\n        \"ajv\": \"^8.17.1\",\n        \"ajv-formats\": \"^3.0.1\",\n        \"chalk\": \"^5.6.2\",\n        \"openai\": \"6.26.0\",\n        \"partial-json\": \"^0.1.7\",\n        \"proxy-agent\": \"^6.5.0\",\n        \"undici\": \"^7.19.1\",\n        \"zod-to-json-schema\": \"^3.24.6\"\n      },\n      \"bin\": {\n        \"pi-ai\": \"dist/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-ai/node_modules/ajv\": {\n      \"version\": \"8.18.0\",\n      \"resolved\": \"https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz\",\n      \"integrity\": \"sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"fast-uri\": \"^3.0.1\",\n        \"json-schema-traverse\": \"^1.0.0\",\n        \"require-from-string\": \"^2.0.2\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/epoberezkin\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-ai/node_modules/chalk\": {\n      \"version\": \"5.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz\",\n      \"integrity\": \"sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-ai/node_modules/json-schema-traverse\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz\",\n      \"integrity\": \"sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@mariozechner/pi-coding-agent\": {\n      \"version\": \"0.57.1\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.57.1.tgz\",\n      \"integrity\": \"sha512-u5MQEduj68rwVIsRsqrWkJYiJCyPph/a6bMoJAQKo1sb+Pc17Y/ojwa+wGssnUMjEB38AQKofWTVe8NFEpSWNw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@mariozechner/jiti\": \"^2.6.2\",\n        \"@mariozechner/pi-agent-core\": \"^0.57.1\",\n        \"@mariozechner/pi-ai\": \"^0.57.1\",\n        \"@mariozechner/pi-tui\": \"^0.57.1\",\n        \"@silvia-odwyer/photon-node\": \"^0.3.4\",\n        \"chalk\": \"^5.5.0\",\n        \"cli-highlight\": \"^2.1.11\",\n        \"diff\": \"^8.0.2\",\n        \"extract-zip\": \"^2.0.1\",\n        \"file-type\": \"^21.1.1\",\n        \"glob\": \"^13.0.1\",\n        \"hosted-git-info\": \"^9.0.2\",\n        \"ignore\": \"^7.0.5\",\n        \"marked\": \"^15.0.12\",\n        \"minimatch\": \"^10.2.3\",\n        \"proper-lockfile\": \"^4.1.2\",\n        \"strip-ansi\": \"^7.1.0\",\n        \"undici\": \"^7.19.1\",\n        \"yaml\": \"^2.8.2\"\n      },\n      \"bin\": {\n        \"pi\": \"dist/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=20.6.0\"\n      },\n      \"optionalDependencies\": {\n        \"@mariozechner/clipboard\": \"^0.3.2\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-coding-agent/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz\",\n      \"integrity\": \"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-coding-agent/node_modules/brace-expansion\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz\",\n      \"integrity\": \"sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-coding-agent/node_modules/chalk\": {\n      \"version\": \"5.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz\",\n      \"integrity\": \"sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-coding-agent/node_modules/ignore\": {\n      \"version\": \"7.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz\",\n      \"integrity\": \"sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-coding-agent/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz\",\n      \"integrity\": \"sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-tui\": {\n      \"version\": \"0.57.1\",\n      \"resolved\": \"https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.57.1.tgz\",\n      \"integrity\": \"sha512-cjoRghLbeAHV0tTJeHgZXaryUi5zzBZofeZ7uJun1gztnckLLRjoVeaPTujNlc5BIfyKvFqhh1QWCZng/MXlpg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/mime-types\": \"^2.1.4\",\n        \"chalk\": \"^5.5.0\",\n        \"get-east-asian-width\": \"^1.3.0\",\n        \"marked\": \"^15.0.12\",\n        \"mime-types\": \"^3.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      },\n      \"optionalDependencies\": {\n        \"koffi\": \"^2.9.0\"\n      }\n    },\n    \"node_modules/@mariozechner/pi-tui/node_modules/chalk\": {\n      \"version\": \"5.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz\",\n      \"integrity\": \"sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/@memorilabs/axon\": {\n      \"version\": \"0.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@memorilabs/axon/-/axon-0.1.2.tgz\",\n      \"integrity\": \"sha512-75oxynwLv6peVlr37NU/k4EYZ8D7DmZiDcVDb2lYOOq678rUIJM4ALteVPbi4a3pQ0i763yjTGvaJ/wrI3wHVw==\",\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"@anthropic-ai/sdk\": \"*\",\n        \"@google/genai\": \"*\",\n        \"openai\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@anthropic-ai/sdk\": {\n          \"optional\": true\n        },\n        \"@google/genai\": {\n          \"optional\": true\n        },\n        \"openai\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@memorilabs/memori\": {\n      \"version\": \"0.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/@memorilabs/memori/-/memori-0.0.5.tgz\",\n      \"integrity\": \"sha512-ZVlW3sl1Syw9LTqVqMkge/A7T0KJ5Lt26KX3VcQklZWoodp/XpBeMHuoByjgn3E5EvakyWoO4zMeIM+zj9+IaA==\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@memorilabs/axon\": \"^0.1.2\"\n      },\n      \"bin\": {\n        \"memori\": \"dist/bin/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"@anthropic-ai/sdk\": \"*\",\n        \"openai\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@anthropic-ai/sdk\": {\n          \"optional\": true\n        },\n        \"openai\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@microsoft/tsdoc\": {\n      \"version\": \"0.16.0\",\n      \"resolved\": \"https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz\",\n      \"integrity\": \"sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@microsoft/tsdoc-config\": {\n      \"version\": \"0.18.1\",\n      \"resolved\": \"https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.18.1.tgz\",\n      \"integrity\": \"sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@microsoft/tsdoc\": \"0.16.0\",\n        \"ajv\": \"~8.18.0\",\n        \"jju\": \"~1.4.0\",\n        \"resolve\": \"~1.22.2\"\n      }\n    },\n    \"node_modules/@microsoft/tsdoc-config/node_modules/ajv\": {\n      \"version\": \"8.18.0\",\n      \"resolved\": \"https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz\",\n      \"integrity\": \"sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"fast-uri\": \"^3.0.1\",\n        \"json-schema-traverse\": \"^1.0.0\",\n        \"require-from-string\": \"^2.0.2\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/epoberezkin\"\n      }\n    },\n    \"node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz\",\n      \"integrity\": \"sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@mistralai/mistralai\": {\n      \"version\": \"1.14.1\",\n      \"resolved\": \"https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.14.1.tgz\",\n      \"integrity\": \"sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ws\": \"^8.18.0\",\n        \"zod\": \"^3.25.0 || ^4.0.0\",\n        \"zod-to-json-schema\": \"^3.24.1\"\n      }\n    },\n    \"node_modules/@mozilla/readability\": {\n      \"version\": \"0.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz\",\n      \"integrity\": \"sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.96.tgz\",\n      \"integrity\": \"sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"workspaces\": [\n        \"e2e/*\"\n      ],\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      },\n      \"optionalDependencies\": {\n        \"@napi-rs/canvas-android-arm64\": \"0.1.96\",\n        \"@napi-rs/canvas-darwin-arm64\": \"0.1.96\",\n        \"@napi-rs/canvas-darwin-x64\": \"0.1.96\",\n        \"@napi-rs/canvas-linux-arm-gnueabihf\": \"0.1.96\",\n        \"@napi-rs/canvas-linux-arm64-gnu\": \"0.1.96\",\n        \"@napi-rs/canvas-linux-arm64-musl\": \"0.1.96\",\n        \"@napi-rs/canvas-linux-riscv64-gnu\": \"0.1.96\",\n        \"@napi-rs/canvas-linux-x64-gnu\": \"0.1.96\",\n        \"@napi-rs/canvas-linux-x64-musl\": \"0.1.96\",\n        \"@napi-rs/canvas-win32-arm64-msvc\": \"0.1.96\",\n        \"@napi-rs/canvas-win32-x64-msvc\": \"0.1.96\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-android-arm64\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.96.tgz\",\n      \"integrity\": \"sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-darwin-arm64\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.96.tgz\",\n      \"integrity\": \"sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-darwin-x64\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.96.tgz\",\n      \"integrity\": \"sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-linux-arm-gnueabihf\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.96.tgz\",\n      \"integrity\": \"sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-linux-arm64-gnu\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.96.tgz\",\n      \"integrity\": \"sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-linux-arm64-musl\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.96.tgz\",\n      \"integrity\": \"sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-linux-riscv64-gnu\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.96.tgz\",\n      \"integrity\": \"sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-linux-x64-gnu\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.96.tgz\",\n      \"integrity\": \"sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-linux-x64-musl\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.96.tgz\",\n      \"integrity\": \"sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-win32-arm64-msvc\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.96.tgz\",\n      \"integrity\": \"sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/canvas-win32-x64-msvc\": {\n      \"version\": \"0.1.96\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.96.tgz\",\n      \"integrity\": \"sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@napi-rs/wasm-runtime\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz\",\n      \"integrity\": \"sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"@emnapi/core\": \"^1.7.1\",\n        \"@emnapi/runtime\": \"^1.7.1\",\n        \"@tybys/wasm-util\": \"^0.10.1\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Brooooooklyn\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/linux-arm64\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/linux-arm64/-/linux-arm64-3.16.2.tgz\",\n      \"integrity\": \"sha512-CxzgPsS84wL3W5sZRgxP3c9iJKEW+USrak1SmX6EAJxW/v9QGzehvT6W/aR1FyfidiIyQtOp3ga0Gg/9xfJPGw==\",\n      \"cpu\": [\n        \"arm64\",\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/linux-armv7l\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/linux-armv7l/-/linux-armv7l-3.16.2.tgz\",\n      \"integrity\": \"sha512-9G6W/MkQ/DLwGmpcj143NQ50QJg5gQZfzVf5RYx77VczBqhgwkgYHILekYrOs4xanOeqeJ8jnOnQQSp1YaJZUg==\",\n      \"cpu\": [\n        \"arm\",\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/linux-x64\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/linux-x64/-/linux-x64-3.16.2.tgz\",\n      \"integrity\": \"sha512-OXYf8rVfoDyvN+YrfKk8F9An9a5GOxVIM8OcR1U911tc0oRNf8yfJrQ8KrM75R26gwq0Y6YZwVTP0vRCInwWOw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/linux-x64-cuda\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda/-/linux-x64-cuda-3.16.2.tgz\",\n      \"integrity\": \"sha512-LTBQFqjin7tyrLNJz0XWTB5QAHDsZV71/qiiRRjXdBKSZHVVaPLfdgxypGu7ggPeBNsv+MckRXdlH5C7yMtE4A==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/linux-x64-cuda-ext\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda-ext/-/linux-x64-cuda-ext-3.16.2.tgz\",\n      \"integrity\": \"sha512-47d9myCJauZyzAlN7IK1eIt/4CcBMslF+yHy4q+yJotD/RV/S6qRpK2kGn+ybtdVjkPGNCoPkHKcyla9iIVjbw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/linux-x64-vulkan\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/linux-x64-vulkan/-/linux-x64-vulkan-3.16.2.tgz\",\n      \"integrity\": \"sha512-HDLAw4ZhwJuhKuF6n4x520yZXAQZahUOXtCGvPubjfpmIOElKrfDvCVlRsthAP0JwcwINzIQlVys3boMIXfBgw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/mac-arm64-metal\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/mac-arm64-metal/-/mac-arm64-metal-3.16.2.tgz\",\n      \"integrity\": \"sha512-nEZ74qB0lUohF88yR741YUrUqz/qD+FJFzUTHj0FwxAynSZCjvwtzEDtavRlh3qd3yLD/0ChNn00/RQ54ISImw==\",\n      \"cpu\": [\n        \"arm64\",\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/mac-x64\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/mac-x64/-/mac-x64-3.16.2.tgz\",\n      \"integrity\": \"sha512-BjA+DgeDt+kRxVMV6kChb9XVXm7U5b90jUif7Z/s6ZXtOOnV6exrTM2W09kbSqAiNhZmctcVY83h2dwNTZ/yIw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/win-arm64\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/win-arm64/-/win-arm64-3.16.2.tgz\",\n      \"integrity\": \"sha512-XHNFQzUjYODtkZjIn4NbQVrBtGB9RI9TpisiALryqfrIqagQmjBh6dmxZWlt5uduKAfT7M2/2vrABGR490FACA==\",\n      \"cpu\": [\n        \"arm64\",\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/win-x64\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/win-x64/-/win-x64-3.16.2.tgz\",\n      \"integrity\": \"sha512-etrivzbyLNVhZlUosFW8JSL0OSiuKQf9qcI3dNdehD907sHquQbBJrG7lXcdL6IecvXySp3oAwCkM87VJ0b3Fg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/win-x64-cuda\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda/-/win-x64-cuda-3.16.2.tgz\",\n      \"integrity\": \"sha512-jStDELHrU3rKQMOk5Hs5bWEazyjE2hzHwpNf6SblOpaGkajM/HJtxEZoL0mLHJx5qeXs4oOVkr7AzuLy0WPpNA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/win-x64-cuda-ext\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda-ext/-/win-x64-cuda-ext-3.16.2.tgz\",\n      \"integrity\": \"sha512-sdv4Kzn9bOQWNBRvw6B/zcn8dQRfZhjIHv5AfDBIOfRlSCgjebFpBeYUoU4wZPpjr3ISwcqO5MEWsw+AbUdV3Q==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@node-llama-cpp/win-x64-vulkan\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/@node-llama-cpp/win-x64-vulkan/-/win-x64-vulkan-3.16.2.tgz\",\n      \"integrity\": \"sha512-9xuHFCOhCQjZgQSFrk79EuSKn9nGWt/SAq/3wujQSQLtgp8jGdtZgwcmuDUoemInf10en2dcOmEt7t8dQdC3XA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      }\n    },\n    \"node_modules/@octokit/app\": {\n      \"version\": \"16.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/app/-/app-16.1.2.tgz\",\n      \"integrity\": \"sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/auth-app\": \"^8.1.2\",\n        \"@octokit/auth-unauthenticated\": \"^7.0.3\",\n        \"@octokit/core\": \"^7.0.6\",\n        \"@octokit/oauth-app\": \"^8.0.3\",\n        \"@octokit/plugin-paginate-rest\": \"^14.0.0\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"@octokit/webhooks\": \"^14.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/auth-app\": {\n      \"version\": \"8.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.2.0.tgz\",\n      \"integrity\": \"sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/auth-oauth-app\": \"^9.0.3\",\n        \"@octokit/auth-oauth-user\": \"^6.0.2\",\n        \"@octokit/request\": \"^10.0.6\",\n        \"@octokit/request-error\": \"^7.0.2\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"toad-cache\": \"^3.7.0\",\n        \"universal-github-app-jwt\": \"^2.2.0\",\n        \"universal-user-agent\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/auth-oauth-app\": {\n      \"version\": \"9.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.3.tgz\",\n      \"integrity\": \"sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/auth-oauth-device\": \"^8.0.3\",\n        \"@octokit/auth-oauth-user\": \"^6.0.2\",\n        \"@octokit/request\": \"^10.0.6\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"universal-user-agent\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/auth-oauth-device\": {\n      \"version\": \"8.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.3.tgz\",\n      \"integrity\": \"sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/oauth-methods\": \"^6.0.2\",\n        \"@octokit/request\": \"^10.0.6\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"universal-user-agent\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/auth-oauth-user\": {\n      \"version\": \"6.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.2.tgz\",\n      \"integrity\": \"sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/auth-oauth-device\": \"^8.0.3\",\n        \"@octokit/oauth-methods\": \"^6.0.2\",\n        \"@octokit/request\": \"^10.0.6\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"universal-user-agent\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/auth-token\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz\",\n      \"integrity\": \"sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/auth-unauthenticated\": {\n      \"version\": \"7.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.3.tgz\",\n      \"integrity\": \"sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/request-error\": \"^7.0.2\",\n        \"@octokit/types\": \"^16.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/core\": {\n      \"version\": \"7.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz\",\n      \"integrity\": \"sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/auth-token\": \"^6.0.0\",\n        \"@octokit/graphql\": \"^9.0.3\",\n        \"@octokit/request\": \"^10.0.6\",\n        \"@octokit/request-error\": \"^7.0.2\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"before-after-hook\": \"^4.0.0\",\n        \"universal-user-agent\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/endpoint\": {\n      \"version\": \"11.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz\",\n      \"integrity\": \"sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/types\": \"^16.0.0\",\n        \"universal-user-agent\": \"^7.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/graphql\": {\n      \"version\": \"9.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz\",\n      \"integrity\": \"sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/request\": \"^10.0.6\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"universal-user-agent\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/oauth-app\": {\n      \"version\": \"8.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.3.tgz\",\n      \"integrity\": \"sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/auth-oauth-app\": \"^9.0.2\",\n        \"@octokit/auth-oauth-user\": \"^6.0.1\",\n        \"@octokit/auth-unauthenticated\": \"^7.0.2\",\n        \"@octokit/core\": \"^7.0.5\",\n        \"@octokit/oauth-authorization-url\": \"^8.0.0\",\n        \"@octokit/oauth-methods\": \"^6.0.1\",\n        \"@types/aws-lambda\": \"^8.10.83\",\n        \"universal-user-agent\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/oauth-authorization-url\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz\",\n      \"integrity\": \"sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/oauth-methods\": {\n      \"version\": \"6.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.2.tgz\",\n      \"integrity\": \"sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/oauth-authorization-url\": \"^8.0.0\",\n        \"@octokit/request\": \"^10.0.6\",\n        \"@octokit/request-error\": \"^7.0.2\",\n        \"@octokit/types\": \"^16.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/openapi-types\": {\n      \"version\": \"27.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz\",\n      \"integrity\": \"sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@octokit/openapi-webhooks-types\": {\n      \"version\": \"12.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-12.1.0.tgz\",\n      \"integrity\": \"sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@octokit/plugin-paginate-graphql\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz\",\n      \"integrity\": \"sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 20\"\n      },\n      \"peerDependencies\": {\n        \"@octokit/core\": \">=6\"\n      }\n    },\n    \"node_modules/@octokit/plugin-paginate-rest\": {\n      \"version\": \"14.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz\",\n      \"integrity\": \"sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/types\": \"^16.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      },\n      \"peerDependencies\": {\n        \"@octokit/core\": \">=6\"\n      }\n    },\n    \"node_modules/@octokit/plugin-rest-endpoint-methods\": {\n      \"version\": \"17.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz\",\n      \"integrity\": \"sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/types\": \"^16.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      },\n      \"peerDependencies\": {\n        \"@octokit/core\": \">=6\"\n      }\n    },\n    \"node_modules/@octokit/plugin-retry\": {\n      \"version\": \"8.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.0.tgz\",\n      \"integrity\": \"sha512-O1FZgXeiGb2sowEr/hYTr6YunGdSAFWnr2fyW39Ah85H8O33ELASQxcvOFF5LE6Tjekcyu2ms4qAzJVhSaJxTw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/request-error\": \"^7.0.2\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"bottleneck\": \"^2.15.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      },\n      \"peerDependencies\": {\n        \"@octokit/core\": \">=7\"\n      }\n    },\n    \"node_modules/@octokit/plugin-throttling\": {\n      \"version\": \"11.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.3.tgz\",\n      \"integrity\": \"sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/types\": \"^16.0.0\",\n        \"bottleneck\": \"^2.15.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      },\n      \"peerDependencies\": {\n        \"@octokit/core\": \"^7.0.0\"\n      }\n    },\n    \"node_modules/@octokit/request\": {\n      \"version\": \"10.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz\",\n      \"integrity\": \"sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/endpoint\": \"^11.0.3\",\n        \"@octokit/request-error\": \"^7.0.2\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"fast-content-type-parse\": \"^3.0.0\",\n        \"json-with-bigint\": \"^3.5.3\",\n        \"universal-user-agent\": \"^7.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/request-error\": {\n      \"version\": \"7.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz\",\n      \"integrity\": \"sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/types\": \"^16.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/types\": {\n      \"version\": \"16.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz\",\n      \"integrity\": \"sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/openapi-types\": \"^27.0.0\"\n      }\n    },\n    \"node_modules/@octokit/webhooks\": {\n      \"version\": \"14.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.2.0.tgz\",\n      \"integrity\": \"sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/openapi-webhooks-types\": \"12.1.0\",\n        \"@octokit/request-error\": \"^7.0.0\",\n        \"@octokit/webhooks-methods\": \"^6.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@octokit/webhooks-methods\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz\",\n      \"integrity\": \"sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/@oxc-project/runtime\": {\n      \"version\": \"0.115.0\",\n      \"resolved\": \"https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz\",\n      \"integrity\": \"sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@oxc-project/types\": {\n      \"version\": \"0.115.0\",\n      \"resolved\": \"https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz\",\n      \"integrity\": \"sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/Boshen\"\n      }\n    },\n    \"node_modules/@pinojs/redact\": {\n      \"version\": \"0.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz\",\n      \"integrity\": \"sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@pkgjs/parseargs\": {\n      \"version\": \"0.11.0\",\n      \"resolved\": \"https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz\",\n      \"integrity\": \"sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14\"\n      }\n    },\n    \"node_modules/@protobufjs/aspromise\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz\",\n      \"integrity\": \"sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@protobufjs/base64\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz\",\n      \"integrity\": \"sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@protobufjs/codegen\": {\n      \"version\": \"2.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz\",\n      \"integrity\": \"sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@protobufjs/eventemitter\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz\",\n      \"integrity\": \"sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@protobufjs/fetch\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz\",\n      \"integrity\": \"sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@protobufjs/aspromise\": \"^1.1.1\",\n        \"@protobufjs/inquire\": \"^1.1.0\"\n      }\n    },\n    \"node_modules/@protobufjs/float\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz\",\n      \"integrity\": \"sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@protobufjs/inquire\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz\",\n      \"integrity\": \"sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@protobufjs/path\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz\",\n      \"integrity\": \"sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@protobufjs/pool\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz\",\n      \"integrity\": \"sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@protobufjs/utf8\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz\",\n      \"integrity\": \"sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/@reflink/reflink\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink/-/reflink-0.1.19.tgz\",\n      \"integrity\": \"sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"optionalDependencies\": {\n        \"@reflink/reflink-darwin-arm64\": \"0.1.19\",\n        \"@reflink/reflink-darwin-x64\": \"0.1.19\",\n        \"@reflink/reflink-linux-arm64-gnu\": \"0.1.19\",\n        \"@reflink/reflink-linux-arm64-musl\": \"0.1.19\",\n        \"@reflink/reflink-linux-x64-gnu\": \"0.1.19\",\n        \"@reflink/reflink-linux-x64-musl\": \"0.1.19\",\n        \"@reflink/reflink-win32-arm64-msvc\": \"0.1.19\",\n        \"@reflink/reflink-win32-x64-msvc\": \"0.1.19\"\n      }\n    },\n    \"node_modules/@reflink/reflink-darwin-arm64\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink-darwin-arm64/-/reflink-darwin-arm64-0.1.19.tgz\",\n      \"integrity\": \"sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@reflink/reflink-darwin-x64\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink-darwin-x64/-/reflink-darwin-x64-0.1.19.tgz\",\n      \"integrity\": \"sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@reflink/reflink-linux-arm64-gnu\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink-linux-arm64-gnu/-/reflink-linux-arm64-gnu-0.1.19.tgz\",\n      \"integrity\": \"sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@reflink/reflink-linux-arm64-musl\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink-linux-arm64-musl/-/reflink-linux-arm64-musl-0.1.19.tgz\",\n      \"integrity\": \"sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@reflink/reflink-linux-x64-gnu\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink-linux-x64-gnu/-/reflink-linux-x64-gnu-0.1.19.tgz\",\n      \"integrity\": \"sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@reflink/reflink-linux-x64-musl\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink-linux-x64-musl/-/reflink-linux-x64-musl-0.1.19.tgz\",\n      \"integrity\": \"sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@reflink/reflink-win32-arm64-msvc\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink-win32-arm64-msvc/-/reflink-win32-arm64-msvc-0.1.19.tgz\",\n      \"integrity\": \"sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@reflink/reflink-win32-x64-msvc\": {\n      \"version\": \"0.1.19\",\n      \"resolved\": \"https://registry.npmjs.org/@reflink/reflink-win32-x64-msvc/-/reflink-win32-x64-msvc-0.1.19.tgz\",\n      \"integrity\": \"sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@rolldown/binding-android-arm64\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-darwin-arm64\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-darwin-x64\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-freebsd-x64\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-linux-arm-gnueabihf\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-linux-arm64-gnu\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-linux-arm64-musl\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-linux-ppc64-gnu\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-linux-s390x-gnu\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-linux-x64-gnu\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-linux-x64-musl\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-openharmony-arm64\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openharmony\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-wasm32-wasi\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==\",\n      \"cpu\": [\n        \"wasm32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"@napi-rs/wasm-runtime\": \"^1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-win32-arm64-msvc\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/binding-win32-x64-msvc\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      }\n    },\n    \"node_modules/@rolldown/pluginutils\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@silvia-odwyer/photon-node\": {\n      \"version\": \"0.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz\",\n      \"integrity\": \"sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true\n    },\n    \"node_modules/@sinclair/typebox\": {\n      \"version\": \"0.34.48\",\n      \"resolved\": \"https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz\",\n      \"integrity\": \"sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@slack/bolt\": {\n      \"version\": \"4.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/@slack/bolt/-/bolt-4.6.0.tgz\",\n      \"integrity\": \"sha512-xPgfUs2+OXSugz54Ky07pA890+Qydk22SYToi8uGpXeHSt1JWwFJkRyd/9Vlg5I1AdfdpGXExDpwnbuN9Q/2dQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@slack/logger\": \"^4.0.0\",\n        \"@slack/oauth\": \"^3.0.4\",\n        \"@slack/socket-mode\": \"^2.0.5\",\n        \"@slack/types\": \"^2.18.0\",\n        \"@slack/web-api\": \"^7.12.0\",\n        \"axios\": \"^1.12.0\",\n        \"express\": \"^5.0.0\",\n        \"path-to-regexp\": \"^8.1.0\",\n        \"raw-body\": \"^3\",\n        \"tsscmp\": \"^1.0.6\"\n      },\n      \"engines\": {\n        \"node\": \">=18\",\n        \"npm\": \">=8.6.0\"\n      },\n      \"peerDependencies\": {\n        \"@types/express\": \"^5.0.0\"\n      }\n    },\n    \"node_modules/@slack/logger\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz\",\n      \"integrity\": \"sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \">=18.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\",\n        \"npm\": \">= 8.6.0\"\n      }\n    },\n    \"node_modules/@slack/oauth\": {\n      \"version\": \"3.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/@slack/oauth/-/oauth-3.0.4.tgz\",\n      \"integrity\": \"sha512-+8H0g7mbrHndEUbYCP7uYyBCbwqmm3E6Mo3nfsDvZZW74zKk1ochfH/fWSvGInYNCVvaBUbg3RZBbTp0j8yJCg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@slack/logger\": \"^4\",\n        \"@slack/web-api\": \"^7.10.0\",\n        \"@types/jsonwebtoken\": \"^9\",\n        \"@types/node\": \">=18\",\n        \"jsonwebtoken\": \"^9\"\n      },\n      \"engines\": {\n        \"node\": \">=18\",\n        \"npm\": \">=8.6.0\"\n      }\n    },\n    \"node_modules/@slack/socket-mode\": {\n      \"version\": \"2.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/@slack/socket-mode/-/socket-mode-2.0.5.tgz\",\n      \"integrity\": \"sha512-VaapvmrAifeFLAFaDPfGhEwwunTKsI6bQhYzxRXw7BSujZUae5sANO76WqlVsLXuhVtCVrBWPiS2snAQR2RHJQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@slack/logger\": \"^4\",\n        \"@slack/web-api\": \"^7.10.0\",\n        \"@types/node\": \">=18\",\n        \"@types/ws\": \"^8\",\n        \"eventemitter3\": \"^5\",\n        \"ws\": \"^8\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\",\n        \"npm\": \">= 8.6.0\"\n      }\n    },\n    \"node_modules/@slack/types\": {\n      \"version\": \"2.20.0\",\n      \"resolved\": \"https://registry.npmjs.org/@slack/types/-/types-2.20.0.tgz\",\n      \"integrity\": \"sha512-PVF6P6nxzDMrzPC8fSCsnwaI+kF8YfEpxf3MqXmdyjyWTYsZQURpkK7WWUWvP5QpH55pB7zyYL9Qem/xSgc5VA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 12.13.0\",\n        \"npm\": \">= 6.12.0\"\n      }\n    },\n    \"node_modules/@slack/web-api\": {\n      \"version\": \"7.14.1\",\n      \"resolved\": \"https://registry.npmjs.org/@slack/web-api/-/web-api-7.14.1.tgz\",\n      \"integrity\": \"sha512-RoygyteJeFswxDPJjUMESn9dldWVMD2xUcHHd9DenVavSfVC6FeVnSdDerOO7m8LLvw4Q132nQM4hX8JiF7dng==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@slack/logger\": \"^4.0.0\",\n        \"@slack/types\": \"^2.20.0\",\n        \"@types/node\": \">=18.0.0\",\n        \"@types/retry\": \"0.12.0\",\n        \"axios\": \"^1.13.5\",\n        \"eventemitter3\": \"^5.0.1\",\n        \"form-data\": \"^4.0.4\",\n        \"is-electron\": \"2.2.2\",\n        \"is-stream\": \"^2\",\n        \"p-queue\": \"^6\",\n        \"p-retry\": \"^4\",\n        \"retry\": \"^0.13.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\",\n        \"npm\": \">= 8.6.0\"\n      }\n    },\n    \"node_modules/@smithy/abort-controller\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz\",\n      \"integrity\": \"sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/config-resolver\": {\n      \"version\": \"4.4.11\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz\",\n      \"integrity\": \"sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/node-config-provider\": \"^4.3.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-config-provider\": \"^4.2.2\",\n        \"@smithy/util-endpoints\": \"^3.3.3\",\n        \"@smithy/util-middleware\": \"^4.2.12\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/core\": {\n      \"version\": \"3.23.10\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/core/-/core-3.23.10.tgz\",\n      \"integrity\": \"sha512-pn0HaJpxmdeCLdbAm79SUjX8IPiej9ANHNHec4K4u5Bkf5BqYCbAgK3c8NTCVf44DnlWJK7W1mimlgBPUQ3IlA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/protocol-http\": \"^5.3.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/url-parser\": \"^4.2.12\",\n        \"@smithy/util-base64\": \"^4.3.2\",\n        \"@smithy/util-body-length-browser\": \"^4.2.2\",\n        \"@smithy/util-middleware\": \"^4.2.12\",\n        \"@smithy/util-stream\": \"^4.5.18\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"@smithy/uuid\": \"^1.1.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/credential-provider-imds\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz\",\n      \"integrity\": \"sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/node-config-provider\": \"^4.3.12\",\n        \"@smithy/property-provider\": \"^4.2.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/url-parser\": \"^4.2.12\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/eventstream-codec\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz\",\n      \"integrity\": \"sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@aws-crypto/crc32\": \"5.2.0\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-hex-encoding\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/eventstream-serde-browser\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz\",\n      \"integrity\": \"sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/eventstream-serde-universal\": \"^4.2.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/eventstream-serde-config-resolver\": {\n      \"version\": \"4.3.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz\",\n      \"integrity\": \"sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/eventstream-serde-node\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz\",\n      \"integrity\": \"sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/eventstream-serde-universal\": \"^4.2.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/eventstream-serde-universal\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz\",\n      \"integrity\": \"sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/eventstream-codec\": \"^4.2.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/fetch-http-handler\": {\n      \"version\": \"5.3.14\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.14.tgz\",\n      \"integrity\": \"sha512-Aswg1yMsujkikRVv+JIDw2ybTgx0cnTnv7pMee46OX6lTMwk/QpH1lbx3vN3feMwyNrFcSUbYBtbgwHXXn3CIA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/protocol-http\": \"^5.3.12\",\n        \"@smithy/querystring-builder\": \"^4.2.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-base64\": \"^4.3.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/hash-node\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz\",\n      \"integrity\": \"sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-buffer-from\": \"^4.2.2\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/invalid-dependency\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz\",\n      \"integrity\": \"sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/is-array-buffer\": {\n      \"version\": \"4.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz\",\n      \"integrity\": \"sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/middleware-content-length\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz\",\n      \"integrity\": \"sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/protocol-http\": \"^5.3.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/middleware-endpoint\": {\n      \"version\": \"4.4.24\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.24.tgz\",\n      \"integrity\": \"sha512-k7SZG+7IbS4fVAI47p+QixmcjqliCoZ7T5ZtAJMHyViiv7AhMC9aXtgxvNQ8TQmbUe7kotsvW2XeEEqnTmdOXg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/core\": \"^3.23.10\",\n        \"@smithy/middleware-serde\": \"^4.2.13\",\n        \"@smithy/node-config-provider\": \"^4.3.12\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.7\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/url-parser\": \"^4.2.12\",\n        \"@smithy/util-middleware\": \"^4.2.12\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/middleware-retry\": {\n      \"version\": \"4.4.41\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.41.tgz\",\n      \"integrity\": \"sha512-qjeS0KGftfz2CL4/IziPmQurzemKRPh6sekt3IFbj1519nkj+JM+RcdjVrC1AQFFZhmW3zz7KqwOgN+qJZeVlQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/node-config-provider\": \"^4.3.12\",\n        \"@smithy/protocol-http\": \"^5.3.12\",\n        \"@smithy/service-error-classification\": \"^4.2.12\",\n        \"@smithy/smithy-client\": \"^4.12.4\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-middleware\": \"^4.2.12\",\n        \"@smithy/util-retry\": \"^4.2.12\",\n        \"@smithy/uuid\": \"^1.1.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/middleware-serde\": {\n      \"version\": \"4.2.13\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.13.tgz\",\n      \"integrity\": \"sha512-appEschlOmriCVGLYTTjKdbnXIZ55XT9TsV+aGuj5Jiw988gmEZwJwPkYqlZdwajMKgfxt5epjFTGriyYf4Kiw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/core\": \"^3.23.10\",\n        \"@smithy/protocol-http\": \"^5.3.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/middleware-stack\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz\",\n      \"integrity\": \"sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/node-config-provider\": {\n      \"version\": \"4.3.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz\",\n      \"integrity\": \"sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/property-provider\": \"^4.2.12\",\n        \"@smithy/shared-ini-file-loader\": \"^4.4.7\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/node-http-handler\": {\n      \"version\": \"4.4.15\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.15.tgz\",\n      \"integrity\": \"sha512-2z3Z7Qfts2Eui5Oy+MLJjwKx1LT0Hm/b6W0XJXkUIFHP1W9D4BhdvxWW2W5xPP92CoXO+B4C/zSH67uIxMkWoA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/abort-controller\": \"^4.2.12\",\n        \"@smithy/protocol-http\": \"^5.3.12\",\n        \"@smithy/querystring-builder\": \"^4.2.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/property-provider\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz\",\n      \"integrity\": \"sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/protocol-http\": {\n      \"version\": \"5.3.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz\",\n      \"integrity\": \"sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/querystring-builder\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz\",\n      \"integrity\": \"sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-uri-escape\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/querystring-parser\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz\",\n      \"integrity\": \"sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/service-error-classification\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz\",\n      \"integrity\": \"sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/shared-ini-file-loader\": {\n      \"version\": \"4.4.7\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz\",\n      \"integrity\": \"sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/signature-v4\": {\n      \"version\": \"5.3.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz\",\n      \"integrity\": \"sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/is-array-buffer\": \"^4.2.2\",\n        \"@smithy/protocol-http\": \"^5.3.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-hex-encoding\": \"^4.2.2\",\n        \"@smithy/util-middleware\": \"^4.2.12\",\n        \"@smithy/util-uri-escape\": \"^4.2.2\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/smithy-client\": {\n      \"version\": \"4.12.4\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.4.tgz\",\n      \"integrity\": \"sha512-kbFGh3QrUj7Z9zYHCip+dGVyRGiFo6JK0A+9InOwmU4ZCkJs3HKhjLL/ABe5I8kp9uScqrftcWrDh7YxlWmmZA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/core\": \"^3.23.10\",\n        \"@smithy/middleware-endpoint\": \"^4.4.24\",\n        \"@smithy/middleware-stack\": \"^4.2.12\",\n        \"@smithy/protocol-http\": \"^5.3.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-stream\": \"^4.5.18\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/types\": {\n      \"version\": \"4.13.1\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz\",\n      \"integrity\": \"sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/url-parser\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz\",\n      \"integrity\": \"sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/querystring-parser\": \"^4.2.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-base64\": {\n      \"version\": \"4.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz\",\n      \"integrity\": \"sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/util-buffer-from\": \"^4.2.2\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-body-length-browser\": {\n      \"version\": \"4.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz\",\n      \"integrity\": \"sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-body-length-node\": {\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz\",\n      \"integrity\": \"sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-buffer-from\": {\n      \"version\": \"4.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz\",\n      \"integrity\": \"sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/is-array-buffer\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-config-provider\": {\n      \"version\": \"4.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz\",\n      \"integrity\": \"sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-defaults-mode-browser\": {\n      \"version\": \"4.3.40\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.40.tgz\",\n      \"integrity\": \"sha512-TB++dVe/aHkhCw8+fVUiGEEyz70Drftze6uk5VGBDJAjEj2mqNFftkeY7Jyit3uui346NkZxzLMGM0yzD/S8og==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/property-provider\": \"^4.2.12\",\n        \"@smithy/smithy-client\": \"^4.12.4\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-defaults-mode-node\": {\n      \"version\": \"4.2.43\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.43.tgz\",\n      \"integrity\": \"sha512-cHmr8Q1BJstJC8ahvYrcyqjSIwrgLbpphOYmfMvF+EVsKUU52b3DDLb0SyiAzR16o7FR1r2IVUFfWWu7ADh1iw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/config-resolver\": \"^4.4.11\",\n        \"@smithy/credential-provider-imds\": \"^4.2.12\",\n        \"@smithy/node-config-provider\": \"^4.3.12\",\n        \"@smithy/property-provider\": \"^4.2.12\",\n        \"@smithy/smithy-client\": \"^4.12.4\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-endpoints\": {\n      \"version\": \"3.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz\",\n      \"integrity\": \"sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/node-config-provider\": \"^4.3.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-hex-encoding\": {\n      \"version\": \"4.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz\",\n      \"integrity\": \"sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-middleware\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz\",\n      \"integrity\": \"sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-retry\": {\n      \"version\": \"4.2.12\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz\",\n      \"integrity\": \"sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/service-error-classification\": \"^4.2.12\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-stream\": {\n      \"version\": \"4.5.18\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.18.tgz\",\n      \"integrity\": \"sha512-o0hxsNp2rC7Kz93RNER/mv5G60kntYPPjV9e9Zoa3Mm455bCGHlFW6TywziCQRlLzvrQj/mmWJimAvJWF/wfjg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/fetch-http-handler\": \"^5.3.14\",\n        \"@smithy/node-http-handler\": \"^4.4.15\",\n        \"@smithy/types\": \"^4.13.1\",\n        \"@smithy/util-base64\": \"^4.3.2\",\n        \"@smithy/util-buffer-from\": \"^4.2.2\",\n        \"@smithy/util-hex-encoding\": \"^4.2.2\",\n        \"@smithy/util-utf8\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-uri-escape\": {\n      \"version\": \"4.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz\",\n      \"integrity\": \"sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/util-utf8\": {\n      \"version\": \"4.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz\",\n      \"integrity\": \"sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@smithy/util-buffer-from\": \"^4.2.2\",\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@smithy/uuid\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz\",\n      \"integrity\": \"sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.6.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/@snazzah/davey\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey/-/davey-0.1.10.tgz\",\n      \"integrity\": \"sha512-J5f7vV5/tnj0xGnqufFRd6qiWn3FcR3iXjpjpEmO2Ok+Io0AASkMaZ3I39TsL45as0Qo5bq9wWuamFQ77PjJ+g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/Snazzah\"\n      },\n      \"optionalDependencies\": {\n        \"@snazzah/davey-android-arm-eabi\": \"0.1.10\",\n        \"@snazzah/davey-android-arm64\": \"0.1.10\",\n        \"@snazzah/davey-darwin-arm64\": \"0.1.10\",\n        \"@snazzah/davey-darwin-x64\": \"0.1.10\",\n        \"@snazzah/davey-freebsd-x64\": \"0.1.10\",\n        \"@snazzah/davey-linux-arm-gnueabihf\": \"0.1.10\",\n        \"@snazzah/davey-linux-arm64-gnu\": \"0.1.10\",\n        \"@snazzah/davey-linux-arm64-musl\": \"0.1.10\",\n        \"@snazzah/davey-linux-x64-gnu\": \"0.1.10\",\n        \"@snazzah/davey-linux-x64-musl\": \"0.1.10\",\n        \"@snazzah/davey-wasm32-wasi\": \"0.1.10\",\n        \"@snazzah/davey-win32-arm64-msvc\": \"0.1.10\",\n        \"@snazzah/davey-win32-ia32-msvc\": \"0.1.10\",\n        \"@snazzah/davey-win32-x64-msvc\": \"0.1.10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-android-arm-eabi\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-android-arm-eabi/-/davey-android-arm-eabi-0.1.10.tgz\",\n      \"integrity\": \"sha512-7bwHxSNEI2wVXOT6xnmpnO9SHb2xwAnf9oEdL45dlfVHTgU1Okg5rwGwRvZ2aLVFFbTyecfC8EVZyhpyTkjLSw==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-android-arm64\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-android-arm64/-/davey-android-arm64-0.1.10.tgz\",\n      \"integrity\": \"sha512-68WUf2LQwQTP9MgPcCqTWwJztJSIk0keGfF2Y/b+MihSDh29fYJl7C0rbz69aUrVCvCC2lYkB/46P8X1kBz7yg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-darwin-arm64\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-darwin-arm64/-/davey-darwin-arm64-0.1.10.tgz\",\n      \"integrity\": \"sha512-nYC+DWCGUC1jUGEenCNQE/jJpL/02m0ebY/NvTCQbul5ktI/ShVzgA3kzssEhZvhf6jbH048Rs39wDhp/b24Jg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-darwin-x64\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-darwin-x64/-/davey-darwin-x64-0.1.10.tgz\",\n      \"integrity\": \"sha512-0q5Rrcs+O9sSSnPX+A3R3djEQs2nTAtMe5N3lApO6lZas/QNMl6wkEWCvTbDc2cfAYBMSk2jgc1awlRXi4LX3Q==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-freebsd-x64\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-freebsd-x64/-/davey-freebsd-x64-0.1.10.tgz\",\n      \"integrity\": \"sha512-/Gq5YDD6Oz8iBqVJLswUnetCv9JCRo1quYX5ujzpAG8zPCNItZo4g4h5p9C+h4Yoay2quWBYhoaVqQKT96bm8g==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-linux-arm-gnueabihf\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-linux-arm-gnueabihf/-/davey-linux-arm-gnueabihf-0.1.10.tgz\",\n      \"integrity\": \"sha512-0Z7Vrt0WIbgxws9CeHB9qlueYJlvltI44rUuZmysdi70UcHGxlr7nE3MnzYCr9nRWRegohn8EQPWHMKMDJH2GA==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-linux-arm64-gnu\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-linux-arm64-gnu/-/davey-linux-arm64-gnu-0.1.10.tgz\",\n      \"integrity\": \"sha512-xhZQycn4QB+qXhqm/QmZ+kb9MHMXcbjjoPfvcIL4WMQXFG/zUWHW8EiBk7ZTEGMOpeab3F9D1+MlgumglYByUQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-linux-arm64-musl\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-linux-arm64-musl/-/davey-linux-arm64-musl-0.1.10.tgz\",\n      \"integrity\": \"sha512-pudzQCP9rZItwW4qHHvciMwtNd9kWH4l73g6Id1LRpe6sc8jiFBV7W+YXITj2PZbI0by6XPfkRP6Dk5IkGOuAw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-linux-x64-gnu\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-linux-x64-gnu/-/davey-linux-x64-gnu-0.1.10.tgz\",\n      \"integrity\": \"sha512-DC8qRmk+xJEFNqjxKB46cETKeDQqgUqE5p39KXS2k6Vl/XTi8pw8pXOxrPfYte5neoqlWAVQzbxuLnwpyRJVEQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-linux-x64-musl\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-linux-x64-musl/-/davey-linux-x64-musl-0.1.10.tgz\",\n      \"integrity\": \"sha512-wPR5/2QmsF7sR0WUaCwbk4XI3TLcxK9PVK8mhgcAYyuRpbhcVgNGWXs8ulcyMSXve5pFRJAFAuMTGCEb014peg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-wasm32-wasi\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-wasm32-wasi/-/davey-wasm32-wasi-0.1.10.tgz\",\n      \"integrity\": \"sha512-SfQavU+eKTDbRmPeLRodrVSfsWq25PYTmH1nIZW3B27L6IkijzjXZZuxiU1ZG1gdI5fB7mwXrOTtx34t+vAG7Q==\",\n      \"cpu\": [\n        \"wasm32\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"dependencies\": {\n        \"@napi-rs/wasm-runtime\": \"^1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/@snazzah/davey-win32-arm64-msvc\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-win32-arm64-msvc/-/davey-win32-arm64-msvc-0.1.10.tgz\",\n      \"integrity\": \"sha512-Raafk53smYs67wZCY9bQXHXzbaiRMS5QCdjTdin3D9fF5A06T/0Zv1z7/YnaN+O3GSL/Ou3RvynF7SziToYiFQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-win32-ia32-msvc\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-win32-ia32-msvc/-/davey-win32-ia32-msvc-0.1.10.tgz\",\n      \"integrity\": \"sha512-pAs43l/DiZ+icqBwxIwNePzuYxFM1ZblVuf7t6vwwSLxvova7vnREnU7qDVjbc5/YTUHOsqYy3S6TpZMzDo2lw==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@snazzah/davey-win32-x64-msvc\": {\n      \"version\": \"0.1.10\",\n      \"resolved\": \"https://registry.npmjs.org/@snazzah/davey-win32-x64-msvc/-/davey-win32-x64-msvc-0.1.10.tgz\",\n      \"integrity\": \"sha512-kr6148VVBoUT4CtD+5hYshTFRny7R/xQZxXFhFc0fYjtmdMVM8Px9M91olg1JFNxuNzdfMfTufR58Q3wfBocug==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/@standard-schema/spec\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz\",\n      \"integrity\": \"sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@tinyhttp/content-disposition\": {\n      \"version\": \"2.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.4.tgz\",\n      \"integrity\": \"sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12.17.0\"\n      },\n      \"funding\": {\n        \"type\": \"individual\",\n        \"url\": \"https://github.com/tinyhttp/tinyhttp?sponsor=1\"\n      }\n    },\n    \"node_modules/@tokenizer/inflate\": {\n      \"version\": \"0.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz\",\n      \"integrity\": \"sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"debug\": \"^4.4.3\",\n        \"token-types\": \"^6.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Borewit\"\n      }\n    },\n    \"node_modules/@tokenizer/token\": {\n      \"version\": \"0.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz\",\n      \"integrity\": \"sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@tootallnate/quickjs-emscripten\": {\n      \"version\": \"0.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz\",\n      \"integrity\": \"sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@tybys/wasm-util\": {\n      \"version\": \"0.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz\",\n      \"integrity\": \"sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/@types/aws-lambda\": {\n      \"version\": \"8.10.161\",\n      \"resolved\": \"https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.161.tgz\",\n      \"integrity\": \"sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@types/body-parser\": {\n      \"version\": \"1.19.6\",\n      \"resolved\": \"https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz\",\n      \"integrity\": \"sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/connect\": \"*\",\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/bun\": {\n      \"version\": \"1.3.9\",\n      \"resolved\": \"https://registry.npmjs.org/@types/bun/-/bun-1.3.9.tgz\",\n      \"integrity\": \"sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"dependencies\": {\n        \"bun-types\": \"1.3.9\"\n      }\n    },\n    \"node_modules/@types/chai\": {\n      \"version\": \"5.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz\",\n      \"integrity\": \"sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/deep-eql\": \"*\",\n        \"assertion-error\": \"^2.0.1\"\n      }\n    },\n    \"node_modules/@types/connect\": {\n      \"version\": \"3.4.38\",\n      \"resolved\": \"https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz\",\n      \"integrity\": \"sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/deep-eql\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz\",\n      \"integrity\": \"sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/estree\": {\n      \"version\": \"1.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz\",\n      \"integrity\": \"sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/express\": {\n      \"version\": \"5.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz\",\n      \"integrity\": \"sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/body-parser\": \"*\",\n        \"@types/express-serve-static-core\": \"^5.0.0\",\n        \"@types/serve-static\": \"^2\"\n      }\n    },\n    \"node_modules/@types/express-serve-static-core\": {\n      \"version\": \"5.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz\",\n      \"integrity\": \"sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \"*\",\n        \"@types/qs\": \"*\",\n        \"@types/range-parser\": \"*\",\n        \"@types/send\": \"*\"\n      }\n    },\n    \"node_modules/@types/http-errors\": {\n      \"version\": \"2.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz\",\n      \"integrity\": \"sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@types/json-schema\": {\n      \"version\": \"7.0.15\",\n      \"resolved\": \"https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz\",\n      \"integrity\": \"sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/jsonwebtoken\": {\n      \"version\": \"9.0.10\",\n      \"resolved\": \"https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz\",\n      \"integrity\": \"sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/ms\": \"*\",\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/long\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz\",\n      \"integrity\": \"sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@types/mime-types\": {\n      \"version\": \"2.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz\",\n      \"integrity\": \"sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@types/ms\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz\",\n      \"integrity\": \"sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@types/node\": {\n      \"version\": \"20.19.37\",\n      \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz\",\n      \"integrity\": \"sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"undici-types\": \"~6.21.0\"\n      }\n    },\n    \"node_modules/@types/qs\": {\n      \"version\": \"6.15.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz\",\n      \"integrity\": \"sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@types/range-parser\": {\n      \"version\": \"1.2.7\",\n      \"resolved\": \"https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz\",\n      \"integrity\": \"sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@types/retry\": {\n      \"version\": \"0.12.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz\",\n      \"integrity\": \"sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/@types/send\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz\",\n      \"integrity\": \"sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/serve-static\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz\",\n      \"integrity\": \"sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/http-errors\": \"*\",\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/ws\": {\n      \"version\": \"8.18.1\",\n      \"resolved\": \"https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz\",\n      \"integrity\": \"sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@types/yauzl\": {\n      \"version\": \"2.10.3\",\n      \"resolved\": \"https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz\",\n      \"integrity\": \"sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz\",\n      \"integrity\": \"sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/regexpp\": \"^4.12.2\",\n        \"@typescript-eslint/scope-manager\": \"8.57.0\",\n        \"@typescript-eslint/type-utils\": \"8.57.0\",\n        \"@typescript-eslint/utils\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\",\n        \"ignore\": \"^7.0.5\",\n        \"natural-compare\": \"^1.4.0\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"@typescript-eslint/parser\": \"^8.57.0\",\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz\",\n      \"integrity\": \"sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/tsconfig-utils\": \"^8.57.0\",\n        \"@typescript-eslint/types\": \"^8.57.0\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz\",\n      \"integrity\": \"sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz\",\n      \"integrity\": \"sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz\",\n      \"integrity\": \"sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz\",\n      \"integrity\": \"sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/project-service\": \"8.57.0\",\n        \"@typescript-eslint/tsconfig-utils\": \"8.57.0\",\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\",\n        \"debug\": \"^4.4.3\",\n        \"minimatch\": \"^10.2.2\",\n        \"semver\": \"^7.7.3\",\n        \"tinyglobby\": \"^0.2.15\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz\",\n      \"integrity\": \"sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.9.1\",\n        \"@typescript-eslint/scope-manager\": \"8.57.0\",\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.57.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz\",\n      \"integrity\": \"sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"eslint-visitor-keys\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz\",\n      \"integrity\": \"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz\",\n      \"integrity\": \"sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz\",\n      \"integrity\": \"sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore\": {\n      \"version\": \"7.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz\",\n      \"integrity\": \"sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz\",\n      \"integrity\": \"sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz\",\n      \"integrity\": \"sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/scope-manager\": \"8.57.0\",\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz\",\n      \"integrity\": \"sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/tsconfig-utils\": \"^8.57.0\",\n        \"@typescript-eslint/types\": \"^8.57.0\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz\",\n      \"integrity\": \"sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz\",\n      \"integrity\": \"sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz\",\n      \"integrity\": \"sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz\",\n      \"integrity\": \"sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/project-service\": \"8.57.0\",\n        \"@typescript-eslint/tsconfig-utils\": \"8.57.0\",\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\",\n        \"debug\": \"^4.4.3\",\n        \"minimatch\": \"^10.2.2\",\n        \"semver\": \"^7.7.3\",\n        \"tinyglobby\": \"^0.2.15\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz\",\n      \"integrity\": \"sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"eslint-visitor-keys\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz\",\n      \"integrity\": \"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/brace-expansion\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz\",\n      \"integrity\": \"sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz\",\n      \"integrity\": \"sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz\",\n      \"integrity\": \"sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/@typescript-eslint/project-service\": {\n      \"version\": \"8.56.1\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz\",\n      \"integrity\": \"sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/tsconfig-utils\": \"^8.56.1\",\n        \"@typescript-eslint/types\": \"^8.56.1\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/scope-manager\": {\n      \"version\": \"8.56.1\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz\",\n      \"integrity\": \"sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/tsconfig-utils\": {\n      \"version\": \"8.56.1\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz\",\n      \"integrity\": \"sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz\",\n      \"integrity\": \"sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.57.0\",\n        \"@typescript-eslint/utils\": \"8.57.0\",\n        \"debug\": \"^4.4.3\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz\",\n      \"integrity\": \"sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/tsconfig-utils\": \"^8.57.0\",\n        \"@typescript-eslint/types\": \"^8.57.0\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz\",\n      \"integrity\": \"sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz\",\n      \"integrity\": \"sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz\",\n      \"integrity\": \"sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz\",\n      \"integrity\": \"sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/project-service\": \"8.57.0\",\n        \"@typescript-eslint/tsconfig-utils\": \"8.57.0\",\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\",\n        \"debug\": \"^4.4.3\",\n        \"minimatch\": \"^10.2.2\",\n        \"semver\": \"^7.7.3\",\n        \"tinyglobby\": \"^0.2.15\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz\",\n      \"integrity\": \"sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.9.1\",\n        \"@typescript-eslint/scope-manager\": \"8.57.0\",\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.57.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz\",\n      \"integrity\": \"sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"eslint-visitor-keys\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz\",\n      \"integrity\": \"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz\",\n      \"integrity\": \"sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz\",\n      \"integrity\": \"sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz\",\n      \"integrity\": \"sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/@typescript-eslint/types\": {\n      \"version\": \"8.56.1\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz\",\n      \"integrity\": \"sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree\": {\n      \"version\": \"8.56.1\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz\",\n      \"integrity\": \"sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/project-service\": \"8.56.1\",\n        \"@typescript-eslint/tsconfig-utils\": \"8.56.1\",\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.1\",\n        \"debug\": \"^4.4.3\",\n        \"minimatch\": \"^10.2.2\",\n        \"semver\": \"^7.7.3\",\n        \"tinyglobby\": \"^0.2.15\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz\",\n      \"integrity\": \"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz\",\n      \"integrity\": \"sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz\",\n      \"integrity\": \"sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/@typescript-eslint/utils\": {\n      \"version\": \"8.56.1\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz\",\n      \"integrity\": \"sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.9.1\",\n        \"@typescript-eslint/scope-manager\": \"8.56.1\",\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/visitor-keys\": {\n      \"version\": \"8.56.1\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz\",\n      \"integrity\": \"sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"eslint-visitor-keys\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz\",\n      \"integrity\": \"sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@vitest/coverage-v8\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.0.tgz\",\n      \"integrity\": \"sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@bcoe/v8-coverage\": \"^1.0.2\",\n        \"@vitest/utils\": \"4.1.0\",\n        \"ast-v8-to-istanbul\": \"^1.0.0\",\n        \"istanbul-lib-coverage\": \"^3.2.2\",\n        \"istanbul-lib-report\": \"^3.0.1\",\n        \"istanbul-reports\": \"^3.2.0\",\n        \"magicast\": \"^0.5.2\",\n        \"obug\": \"^2.1.1\",\n        \"std-env\": \"^4.0.0-rc.1\",\n        \"tinyrainbow\": \"^3.0.3\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      },\n      \"peerDependencies\": {\n        \"@vitest/browser\": \"4.1.0\",\n        \"vitest\": \"4.1.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@vitest/browser\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@vitest/expect\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz\",\n      \"integrity\": \"sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@standard-schema/spec\": \"^1.1.0\",\n        \"@types/chai\": \"^5.2.2\",\n        \"@vitest/spy\": \"4.1.0\",\n        \"@vitest/utils\": \"4.1.0\",\n        \"chai\": \"^6.2.2\",\n        \"tinyrainbow\": \"^3.0.3\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/mocker\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz\",\n      \"integrity\": \"sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/spy\": \"4.1.0\",\n        \"estree-walker\": \"^3.0.3\",\n        \"magic-string\": \"^0.30.21\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      },\n      \"peerDependencies\": {\n        \"msw\": \"^2.4.9\",\n        \"vite\": \"^6.0.0 || ^7.0.0 || ^8.0.0-0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"msw\": {\n          \"optional\": true\n        },\n        \"vite\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@vitest/pretty-format\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz\",\n      \"integrity\": \"sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tinyrainbow\": \"^3.0.3\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/runner\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz\",\n      \"integrity\": \"sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/utils\": \"4.1.0\",\n        \"pathe\": \"^2.0.3\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/snapshot\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz\",\n      \"integrity\": \"sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/pretty-format\": \"4.1.0\",\n        \"@vitest/utils\": \"4.1.0\",\n        \"magic-string\": \"^0.30.21\",\n        \"pathe\": \"^2.0.3\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/spy\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz\",\n      \"integrity\": \"sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/utils\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz\",\n      \"integrity\": \"sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/pretty-format\": \"4.1.0\",\n        \"convert-source-map\": \"^2.0.0\",\n        \"tinyrainbow\": \"^3.0.3\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@whiskeysockets/baileys\": {\n      \"version\": \"7.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/@whiskeysockets/baileys/-/baileys-7.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-YFm5gKXfDP9byCXCW3OPHKXLzrAKzolzgVUlRosHHgwbnf2YOO3XknkMm6J7+F0ns8OA0uuSBhgkRHTDtqkacw==\",\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@cacheable/node-cache\": \"^1.4.0\",\n        \"@hapi/boom\": \"^9.1.3\",\n        \"async-mutex\": \"^0.5.0\",\n        \"libsignal\": \"git+https://github.com/whiskeysockets/libsignal-node.git\",\n        \"lru-cache\": \"^11.1.0\",\n        \"music-metadata\": \"^11.7.0\",\n        \"p-queue\": \"^9.0.0\",\n        \"pino\": \"^9.6\",\n        \"protobufjs\": \"^7.2.4\",\n        \"ws\": \"^8.13.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      },\n      \"peerDependencies\": {\n        \"audio-decode\": \"^2.1.3\",\n        \"jimp\": \"^1.6.0\",\n        \"link-preview-js\": \"^3.0.0\",\n        \"sharp\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"audio-decode\": {\n          \"optional\": true\n        },\n        \"jimp\": {\n          \"optional\": true\n        },\n        \"link-preview-js\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@whiskeysockets/baileys/node_modules/p-queue\": {\n      \"version\": \"9.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz\",\n      \"integrity\": \"sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"eventemitter3\": \"^5.0.1\",\n        \"p-timeout\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/@whiskeysockets/baileys/node_modules/p-timeout\": {\n      \"version\": \"7.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz\",\n      \"integrity\": \"sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/abort-controller\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz\",\n      \"integrity\": \"sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"event-target-shim\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=6.5\"\n      }\n    },\n    \"node_modules/accepts\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz\",\n      \"integrity\": \"sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"mime-types\": \"^3.0.0\",\n        \"negotiator\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/acorn\": {\n      \"version\": \"8.16.0\",\n      \"resolved\": \"https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz\",\n      \"integrity\": \"sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"acorn\": \"bin/acorn\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/acorn-jsx\": {\n      \"version\": \"5.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz\",\n      \"integrity\": \"sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"acorn\": \"^6.0.0 || ^7.0.0 || ^8.0.0\"\n      }\n    },\n    \"node_modules/agent-base\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/agent-base/-/agent-base-8.0.0.tgz\",\n      \"integrity\": \"sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/ajv\": {\n      \"version\": \"6.14.0\",\n      \"resolved\": \"https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz\",\n      \"integrity\": \"sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^3.1.1\",\n        \"fast-json-stable-stringify\": \"^2.0.0\",\n        \"json-schema-traverse\": \"^0.4.1\",\n        \"uri-js\": \"^4.2.2\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/epoberezkin\"\n      }\n    },\n    \"node_modules/ajv-formats\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz\",\n      \"integrity\": \"sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ajv\": \"^8.0.0\"\n      },\n      \"peerDependencies\": {\n        \"ajv\": \"^8.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"ajv\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/ajv-formats/node_modules/ajv\": {\n      \"version\": \"8.18.0\",\n      \"resolved\": \"https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz\",\n      \"integrity\": \"sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"fast-uri\": \"^3.0.1\",\n        \"json-schema-traverse\": \"^1.0.0\",\n        \"require-from-string\": \"^2.0.2\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/epoberezkin\"\n      }\n    },\n    \"node_modules/ajv-formats/node_modules/json-schema-traverse\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz\",\n      \"integrity\": \"sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/ansi-escapes\": {\n      \"version\": \"6.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz\",\n      \"integrity\": \"sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14.16\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/ansi-regex\": {\n      \"version\": \"6.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz\",\n      \"integrity\": \"sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-regex?sponsor=1\"\n      }\n    },\n    \"node_modules/ansi-styles\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz\",\n      \"integrity\": \"sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"color-convert\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/any-promise\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz\",\n      \"integrity\": \"sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/argparse\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz\",\n      \"integrity\": \"sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==\",\n      \"license\": \"Python-2.0\"\n    },\n    \"node_modules/assertion-error\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz\",\n      \"integrity\": \"sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/ast-types\": {\n      \"version\": \"0.13.4\",\n      \"resolved\": \"https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz\",\n      \"integrity\": \"sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/ast-v8-to-istanbul\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz\",\n      \"integrity\": \"sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/trace-mapping\": \"^0.3.31\",\n        \"estree-walker\": \"^3.0.3\",\n        \"js-tokens\": \"^10.0.0\"\n      }\n    },\n    \"node_modules/async-mutex\": {\n      \"version\": \"0.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz\",\n      \"integrity\": \"sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tslib\": \"^2.4.0\"\n      }\n    },\n    \"node_modules/async-retry\": {\n      \"version\": \"1.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz\",\n      \"integrity\": \"sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"retry\": \"0.13.1\"\n      }\n    },\n    \"node_modules/asynckit\": {\n      \"version\": \"0.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz\",\n      \"integrity\": \"sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/atomic-sleep\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz\",\n      \"integrity\": \"sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8.0.0\"\n      }\n    },\n    \"node_modules/axios\": {\n      \"version\": \"1.13.6\",\n      \"resolved\": \"https://registry.npmjs.org/axios/-/axios-1.13.6.tgz\",\n      \"integrity\": \"sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"follow-redirects\": \"^1.15.11\",\n        \"form-data\": \"^4.0.5\",\n        \"proxy-from-env\": \"^1.1.0\"\n      }\n    },\n    \"node_modules/balanced-match\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz\",\n      \"integrity\": \"sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/base64-js\": {\n      \"version\": \"1.5.1\",\n      \"resolved\": \"https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz\",\n      \"integrity\": \"sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/feross\"\n        },\n        {\n          \"type\": \"patreon\",\n          \"url\": \"https://www.patreon.com/feross\"\n        },\n        {\n          \"type\": \"consulting\",\n          \"url\": \"https://feross.org/support\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/basic-ftp\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz\",\n      \"integrity\": \"sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=10.0.0\"\n      }\n    },\n    \"node_modules/before-after-hook\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz\",\n      \"integrity\": \"sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true\n    },\n    \"node_modules/bignumber.js\": {\n      \"version\": \"9.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz\",\n      \"integrity\": \"sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/body-parser\": {\n      \"version\": \"2.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz\",\n      \"integrity\": \"sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"bytes\": \"^3.1.2\",\n        \"content-type\": \"^1.0.5\",\n        \"debug\": \"^4.4.3\",\n        \"http-errors\": \"^2.0.0\",\n        \"iconv-lite\": \"^0.7.0\",\n        \"on-finished\": \"^2.4.1\",\n        \"qs\": \"^6.14.1\",\n        \"raw-body\": \"^3.0.1\",\n        \"type-is\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/boolbase\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz\",\n      \"integrity\": \"sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/bottleneck\": {\n      \"version\": \"2.19.5\",\n      \"resolved\": \"https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz\",\n      \"integrity\": \"sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/bowser\": {\n      \"version\": \"2.14.1\",\n      \"resolved\": \"https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz\",\n      \"integrity\": \"sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/brace-expansion\": {\n      \"version\": \"1.1.12\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz\",\n      \"integrity\": \"sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^1.0.0\",\n        \"concat-map\": \"0.0.1\"\n      }\n    },\n    \"node_modules/buffer-crc32\": {\n      \"version\": \"0.2.13\",\n      \"resolved\": \"https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz\",\n      \"integrity\": \"sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/buffer-equal-constant-time\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz\",\n      \"integrity\": \"sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/buffer-from\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz\",\n      \"integrity\": \"sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/bun-types\": {\n      \"version\": \"1.3.9\",\n      \"resolved\": \"https://registry.npmjs.org/bun-types/-/bun-types-1.3.9.tgz\",\n      \"integrity\": \"sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/node\": \"*\"\n      }\n    },\n    \"node_modules/bytes\": {\n      \"version\": \"3.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz\",\n      \"integrity\": \"sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/cacheable\": {\n      \"version\": \"2.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/cacheable/-/cacheable-2.3.3.tgz\",\n      \"integrity\": \"sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@cacheable/memory\": \"^2.0.8\",\n        \"@cacheable/utils\": \"^2.4.0\",\n        \"hookified\": \"^1.15.0\",\n        \"keyv\": \"^5.6.0\",\n        \"qified\": \"^0.6.0\"\n      }\n    },\n    \"node_modules/cacheable/node_modules/keyv\": {\n      \"version\": \"5.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz\",\n      \"integrity\": \"sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@keyv/serialize\": \"^1.1.1\"\n      }\n    },\n    \"node_modules/call-bind-apply-helpers\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz\",\n      \"integrity\": \"sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"function-bind\": \"^1.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/call-bound\": {\n      \"version\": \"1.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz\",\n      \"integrity\": \"sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"call-bind-apply-helpers\": \"^1.0.2\",\n        \"get-intrinsic\": \"^1.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/callsites\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz\",\n      \"integrity\": \"sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/chai\": {\n      \"version\": \"6.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/chai/-/chai-6.2.2.tgz\",\n      \"integrity\": \"sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/chalk\": {\n      \"version\": \"4.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz\",\n      \"integrity\": \"sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^4.1.0\",\n        \"supports-color\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/chmodrp\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/chmodrp/-/chmodrp-1.0.2.tgz\",\n      \"integrity\": \"sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/chokidar\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz\",\n      \"integrity\": \"sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"readdirp\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20.19.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://paulmillr.com/funding/\"\n      }\n    },\n    \"node_modules/chownr\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz\",\n      \"integrity\": \"sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/ci-info\": {\n      \"version\": \"4.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz\",\n      \"integrity\": \"sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/sibiraj-s\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cli-cursor\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz\",\n      \"integrity\": \"sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"restore-cursor\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/cli-highlight\": {\n      \"version\": \"2.1.11\",\n      \"resolved\": \"https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz\",\n      \"integrity\": \"sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"chalk\": \"^4.0.0\",\n        \"highlight.js\": \"^10.7.1\",\n        \"mz\": \"^2.4.0\",\n        \"parse5\": \"^5.1.1\",\n        \"parse5-htmlparser2-tree-adapter\": \"^6.0.0\",\n        \"yargs\": \"^16.0.0\"\n      },\n      \"bin\": {\n        \"highlight\": \"bin/highlight\"\n      },\n      \"engines\": {\n        \"node\": \">=8.0.0\",\n        \"npm\": \">=5.0.0\"\n      }\n    },\n    \"node_modules/cli-highlight/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cli-highlight/node_modules/cliui\": {\n      \"version\": \"7.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz\",\n      \"integrity\": \"sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"string-width\": \"^4.2.0\",\n        \"strip-ansi\": \"^6.0.0\",\n        \"wrap-ansi\": \"^7.0.0\"\n      }\n    },\n    \"node_modules/cli-highlight/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz\",\n      \"integrity\": \"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cli-highlight/node_modules/string-width\": {\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cli-highlight/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cli-highlight/node_modules/yargs\": {\n      \"version\": \"16.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz\",\n      \"integrity\": \"sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"cliui\": \"^7.0.2\",\n        \"escalade\": \"^3.1.1\",\n        \"get-caller-file\": \"^2.0.5\",\n        \"require-directory\": \"^2.1.1\",\n        \"string-width\": \"^4.2.0\",\n        \"y18n\": \"^5.0.5\",\n        \"yargs-parser\": \"^20.2.2\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/cli-highlight/node_modules/yargs-parser\": {\n      \"version\": \"20.2.9\",\n      \"resolved\": \"https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz\",\n      \"integrity\": \"sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/cli-spinners\": {\n      \"version\": \"2.9.2\",\n      \"resolved\": \"https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz\",\n      \"integrity\": \"sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/cliui\": {\n      \"version\": \"8.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz\",\n      \"integrity\": \"sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"string-width\": \"^4.2.0\",\n        \"strip-ansi\": \"^6.0.1\",\n        \"wrap-ansi\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/cliui/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cliui/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz\",\n      \"integrity\": \"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cliui/node_modules/string-width\": {\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cliui/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cmake-js\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz\",\n      \"integrity\": \"sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"debug\": \"^4.4.3\",\n        \"fs-extra\": \"^11.3.3\",\n        \"node-api-headers\": \"^1.8.0\",\n        \"rc\": \"1.2.8\",\n        \"semver\": \"^7.7.3\",\n        \"tar\": \"^7.5.6\",\n        \"url-join\": \"^4.0.1\",\n        \"which\": \"^6.0.0\",\n        \"yargs\": \"^17.7.2\"\n      },\n      \"bin\": {\n        \"cmake-js\": \"bin/cmake-js\"\n      },\n      \"engines\": {\n        \"node\": \"^20.17.0 || >=22.9.0\"\n      }\n    },\n    \"node_modules/cmake-js/node_modules/isexe\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz\",\n      \"integrity\": \"sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/cmake-js/node_modules/which\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/which/-/which-6.0.1.tgz\",\n      \"integrity\": \"sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"isexe\": \"^4.0.0\"\n      },\n      \"bin\": {\n        \"node-which\": \"bin/which.js\"\n      },\n      \"engines\": {\n        \"node\": \"^20.17.0 || >=22.9.0\"\n      }\n    },\n    \"node_modules/color-convert\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz\",\n      \"integrity\": \"sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"color-name\": \"~1.1.4\"\n      },\n      \"engines\": {\n        \"node\": \">=7.0.0\"\n      }\n    },\n    \"node_modules/color-name\": {\n      \"version\": \"1.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz\",\n      \"integrity\": \"sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/combined-stream\": {\n      \"version\": \"1.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz\",\n      \"integrity\": \"sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"delayed-stream\": \"~1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/commander\": {\n      \"version\": \"10.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/commander/-/commander-10.0.1.tgz\",\n      \"integrity\": \"sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14\"\n      }\n    },\n    \"node_modules/concat-map\": {\n      \"version\": \"0.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz\",\n      \"integrity\": \"sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/content-disposition\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz\",\n      \"integrity\": \"sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/content-type\": {\n      \"version\": \"1.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz\",\n      \"integrity\": \"sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/convert-source-map\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz\",\n      \"integrity\": \"sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/cookie\": {\n      \"version\": \"0.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz\",\n      \"integrity\": \"sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/cookie-signature\": {\n      \"version\": \"1.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz\",\n      \"integrity\": \"sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=6.6.0\"\n      }\n    },\n    \"node_modules/core-util-is\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz\",\n      \"integrity\": \"sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/croner\": {\n      \"version\": \"10.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/croner/-/croner-10.0.1.tgz\",\n      \"integrity\": \"sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==\",\n      \"funding\": [\n        {\n          \"type\": \"other\",\n          \"url\": \"https://paypal.me/hexagonpp\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/hexagon\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18.0\"\n      }\n    },\n    \"node_modules/cross-spawn\": {\n      \"version\": \"7.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz\",\n      \"integrity\": \"sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"path-key\": \"^3.1.0\",\n        \"shebang-command\": \"^2.0.0\",\n        \"which\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/css-select\": {\n      \"version\": \"5.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz\",\n      \"integrity\": \"sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"boolbase\": \"^1.0.0\",\n        \"css-what\": \"^6.1.0\",\n        \"domhandler\": \"^5.0.2\",\n        \"domutils\": \"^3.0.1\",\n        \"nth-check\": \"^2.0.1\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/fb55\"\n      }\n    },\n    \"node_modules/css-what\": {\n      \"version\": \"6.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz\",\n      \"integrity\": \"sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/fb55\"\n      }\n    },\n    \"node_modules/cssom\": {\n      \"version\": \"0.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz\",\n      \"integrity\": \"sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/curve25519-js\": {\n      \"version\": \"0.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz\",\n      \"integrity\": \"sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/data-uri-to-buffer\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz\",\n      \"integrity\": \"sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 12\"\n      }\n    },\n    \"node_modules/debug\": {\n      \"version\": \"4.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/debug/-/debug-4.4.3.tgz\",\n      \"integrity\": \"sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ms\": \"^2.1.3\"\n      },\n      \"engines\": {\n        \"node\": \">=6.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"supports-color\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/deep-extend\": {\n      \"version\": \"0.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz\",\n      \"integrity\": \"sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=4.0.0\"\n      }\n    },\n    \"node_modules/deep-is\": {\n      \"version\": \"0.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz\",\n      \"integrity\": \"sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/degenerator\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz\",\n      \"integrity\": \"sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ast-types\": \"^0.13.4\",\n        \"escodegen\": \"^2.1.0\",\n        \"esprima\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/delayed-stream\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz\",\n      \"integrity\": \"sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/depd\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/depd/-/depd-2.0.0.tgz\",\n      \"integrity\": \"sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/detect-libc\": {\n      \"version\": \"2.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz\",\n      \"integrity\": \"sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==\",\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/diff\": {\n      \"version\": \"8.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/diff/-/diff-8.0.3.tgz\",\n      \"integrity\": \"sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.3.1\"\n      }\n    },\n    \"node_modules/discord-api-types\": {\n      \"version\": \"0.38.42\",\n      \"resolved\": \"https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz\",\n      \"integrity\": \"sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"workspaces\": [\n        \"scripts/actions/documentation\"\n      ]\n    },\n    \"node_modules/dom-serializer\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz\",\n      \"integrity\": \"sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"domelementtype\": \"^2.3.0\",\n        \"domhandler\": \"^5.0.2\",\n        \"entities\": \"^4.2.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/cheeriojs/dom-serializer?sponsor=1\"\n      }\n    },\n    \"node_modules/domelementtype\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz\",\n      \"integrity\": \"sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/fb55\"\n        }\n      ],\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/domhandler\": {\n      \"version\": \"5.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz\",\n      \"integrity\": \"sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"domelementtype\": \"^2.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/domhandler?sponsor=1\"\n      }\n    },\n    \"node_modules/domutils\": {\n      \"version\": \"3.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz\",\n      \"integrity\": \"sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"dom-serializer\": \"^2.0.0\",\n        \"domelementtype\": \"^2.3.0\",\n        \"domhandler\": \"^5.0.3\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/domutils?sponsor=1\"\n      }\n    },\n    \"node_modules/dotenv\": {\n      \"version\": \"17.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz\",\n      \"integrity\": \"sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://dotenvx.com\"\n      }\n    },\n    \"node_modules/dunder-proto\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz\",\n      \"integrity\": \"sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"call-bind-apply-helpers\": \"^1.0.1\",\n        \"es-errors\": \"^1.3.0\",\n        \"gopd\": \"^1.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/eastasianwidth\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz\",\n      \"integrity\": \"sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/ecdsa-sig-formatter\": {\n      \"version\": \"1.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz\",\n      \"integrity\": \"sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"safe-buffer\": \"^5.0.1\"\n      }\n    },\n    \"node_modules/ee-first\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz\",\n      \"integrity\": \"sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/emoji-regex\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz\",\n      \"integrity\": \"sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/encodeurl\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz\",\n      \"integrity\": \"sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/end-of-stream\": {\n      \"version\": \"1.4.5\",\n      \"resolved\": \"https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz\",\n      \"integrity\": \"sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"once\": \"^1.4.0\"\n      }\n    },\n    \"node_modules/entities\": {\n      \"version\": \"4.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/entities/-/entities-4.5.0.tgz\",\n      \"integrity\": \"sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/entities?sponsor=1\"\n      }\n    },\n    \"node_modules/env-var\": {\n      \"version\": \"7.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz\",\n      \"integrity\": \"sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/es-define-property\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz\",\n      \"integrity\": \"sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-errors\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz\",\n      \"integrity\": \"sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-module-lexer\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz\",\n      \"integrity\": \"sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/es-object-atoms\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz\",\n      \"integrity\": \"sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/es-set-tostringtag\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz\",\n      \"integrity\": \"sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"get-intrinsic\": \"^1.2.6\",\n        \"has-tostringtag\": \"^1.0.2\",\n        \"hasown\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/escalade\": {\n      \"version\": \"3.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz\",\n      \"integrity\": \"sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/escape-html\": {\n      \"version\": \"1.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz\",\n      \"integrity\": \"sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/escape-string-regexp\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz\",\n      \"integrity\": \"sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/escodegen\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz\",\n      \"integrity\": \"sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"esprima\": \"^4.0.1\",\n        \"estraverse\": \"^5.2.0\",\n        \"esutils\": \"^2.0.2\"\n      },\n      \"bin\": {\n        \"escodegen\": \"bin/escodegen.js\",\n        \"esgenerate\": \"bin/esgenerate.js\"\n      },\n      \"engines\": {\n        \"node\": \">=6.0\"\n      },\n      \"optionalDependencies\": {\n        \"source-map\": \"~0.6.1\"\n      }\n    },\n    \"node_modules/eslint\": {\n      \"version\": \"9.39.4\",\n      \"resolved\": \"https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz\",\n      \"integrity\": \"sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.8.0\",\n        \"@eslint-community/regexpp\": \"^4.12.1\",\n        \"@eslint/config-array\": \"^0.21.2\",\n        \"@eslint/config-helpers\": \"^0.4.2\",\n        \"@eslint/core\": \"^0.17.0\",\n        \"@eslint/eslintrc\": \"^3.3.5\",\n        \"@eslint/js\": \"9.39.4\",\n        \"@eslint/plugin-kit\": \"^0.4.1\",\n        \"@humanfs/node\": \"^0.16.6\",\n        \"@humanwhocodes/module-importer\": \"^1.0.1\",\n        \"@humanwhocodes/retry\": \"^0.4.2\",\n        \"@types/estree\": \"^1.0.6\",\n        \"ajv\": \"^6.14.0\",\n        \"chalk\": \"^4.0.0\",\n        \"cross-spawn\": \"^7.0.6\",\n        \"debug\": \"^4.3.2\",\n        \"escape-string-regexp\": \"^4.0.0\",\n        \"eslint-scope\": \"^8.4.0\",\n        \"eslint-visitor-keys\": \"^4.2.1\",\n        \"espree\": \"^10.4.0\",\n        \"esquery\": \"^1.5.0\",\n        \"esutils\": \"^2.0.2\",\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"file-entry-cache\": \"^8.0.0\",\n        \"find-up\": \"^5.0.0\",\n        \"glob-parent\": \"^6.0.2\",\n        \"ignore\": \"^5.2.0\",\n        \"imurmurhash\": \"^0.1.4\",\n        \"is-glob\": \"^4.0.0\",\n        \"json-stable-stringify-without-jsonify\": \"^1.0.1\",\n        \"lodash.merge\": \"^4.6.2\",\n        \"minimatch\": \"^3.1.5\",\n        \"natural-compare\": \"^1.4.0\",\n        \"optionator\": \"^0.9.3\"\n      },\n      \"bin\": {\n        \"eslint\": \"bin/eslint.js\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://eslint.org/donate\"\n      },\n      \"peerDependencies\": {\n        \"jiti\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"jiti\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/eslint-config-prettier\": {\n      \"version\": \"10.1.8\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz\",\n      \"integrity\": \"sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"eslint-config-prettier\": \"bin/cli.js\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint-config-prettier\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \">=7.0.0\"\n      }\n    },\n    \"node_modules/eslint-plugin-tsdoc\": {\n      \"version\": \"0.5.2\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.5.2.tgz\",\n      \"integrity\": \"sha512-BlvqjWZdBJDIPO/YU3zcPCF23CvjYT3gyu63yo6b609NNV3D1b6zceAREy2xnweuBoDpZcLNuPyAUq9cvx6bbQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@microsoft/tsdoc\": \"0.16.0\",\n        \"@microsoft/tsdoc-config\": \"0.18.1\",\n        \"@typescript-eslint/utils\": \"~8.56.0\"\n      }\n    },\n    \"node_modules/eslint-scope\": {\n      \"version\": \"8.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz\",\n      \"integrity\": \"sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"esrecurse\": \"^4.3.0\",\n        \"estraverse\": \"^5.2.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/eslint-visitor-keys\": {\n      \"version\": \"4.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz\",\n      \"integrity\": \"sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/espree\": {\n      \"version\": \"10.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/espree/-/espree-10.4.0.tgz\",\n      \"integrity\": \"sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"acorn\": \"^8.15.0\",\n        \"acorn-jsx\": \"^5.3.2\",\n        \"eslint-visitor-keys\": \"^4.2.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/esprima\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz\",\n      \"integrity\": \"sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"bin\": {\n        \"esparse\": \"bin/esparse.js\",\n        \"esvalidate\": \"bin/esvalidate.js\"\n      },\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/esquery\": {\n      \"version\": \"1.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz\",\n      \"integrity\": \"sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"estraverse\": \"^5.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/esrecurse\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz\",\n      \"integrity\": \"sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"estraverse\": \"^5.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/estraverse\": {\n      \"version\": \"5.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz\",\n      \"integrity\": \"sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==\",\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/estree-walker\": {\n      \"version\": \"3.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz\",\n      \"integrity\": \"sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/estree\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/esutils\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz\",\n      \"integrity\": \"sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==\",\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/etag\": {\n      \"version\": \"1.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/etag/-/etag-1.8.1.tgz\",\n      \"integrity\": \"sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/event-target-shim\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz\",\n      \"integrity\": \"sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/eventemitter3\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz\",\n      \"integrity\": \"sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/expect-type\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz\",\n      \"integrity\": \"sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      }\n    },\n    \"node_modules/express\": {\n      \"version\": \"5.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/express/-/express-5.2.1.tgz\",\n      \"integrity\": \"sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"accepts\": \"^2.0.0\",\n        \"body-parser\": \"^2.2.1\",\n        \"content-disposition\": \"^1.0.0\",\n        \"content-type\": \"^1.0.5\",\n        \"cookie\": \"^0.7.1\",\n        \"cookie-signature\": \"^1.2.1\",\n        \"debug\": \"^4.4.0\",\n        \"depd\": \"^2.0.0\",\n        \"encodeurl\": \"^2.0.0\",\n        \"escape-html\": \"^1.0.3\",\n        \"etag\": \"^1.8.1\",\n        \"finalhandler\": \"^2.1.0\",\n        \"fresh\": \"^2.0.0\",\n        \"http-errors\": \"^2.0.0\",\n        \"merge-descriptors\": \"^2.0.0\",\n        \"mime-types\": \"^3.0.0\",\n        \"on-finished\": \"^2.4.1\",\n        \"once\": \"^1.4.0\",\n        \"parseurl\": \"^1.3.3\",\n        \"proxy-addr\": \"^2.0.7\",\n        \"qs\": \"^6.14.0\",\n        \"range-parser\": \"^1.2.1\",\n        \"router\": \"^2.2.0\",\n        \"send\": \"^1.1.0\",\n        \"serve-static\": \"^2.2.0\",\n        \"statuses\": \"^2.0.1\",\n        \"type-is\": \"^2.0.1\",\n        \"vary\": \"^1.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/extend\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/extend/-/extend-3.0.2.tgz\",\n      \"integrity\": \"sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/extract-zip\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz\",\n      \"integrity\": \"sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"debug\": \"^4.1.1\",\n        \"get-stream\": \"^5.1.0\",\n        \"yauzl\": \"^2.10.0\"\n      },\n      \"bin\": {\n        \"extract-zip\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">= 10.17.0\"\n      },\n      \"optionalDependencies\": {\n        \"@types/yauzl\": \"^2.9.1\"\n      }\n    },\n    \"node_modules/fast-content-type-parse\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz\",\n      \"integrity\": \"sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/fastify\"\n        },\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/fastify\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/fast-deep-equal\": {\n      \"version\": \"3.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz\",\n      \"integrity\": \"sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-json-stable-stringify\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz\",\n      \"integrity\": \"sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-levenshtein\": {\n      \"version\": \"2.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz\",\n      \"integrity\": \"sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-uri\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz\",\n      \"integrity\": \"sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/fastify\"\n        },\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/fastify\"\n        }\n      ],\n      \"license\": \"BSD-3-Clause\"\n    },\n    \"node_modules/fast-xml-builder\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz\",\n      \"integrity\": \"sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/NaturalIntelligence\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"path-expression-matcher\": \"^1.1.3\"\n      }\n    },\n    \"node_modules/fast-xml-parser\": {\n      \"version\": \"5.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz\",\n      \"integrity\": \"sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/NaturalIntelligence\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"fast-xml-builder\": \"^1.0.0\",\n        \"strnum\": \"^2.1.2\"\n      },\n      \"bin\": {\n        \"fxparser\": \"src/cli/cli.js\"\n      }\n    },\n    \"node_modules/fd-slicer\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz\",\n      \"integrity\": \"sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"pend\": \"~1.2.0\"\n      }\n    },\n    \"node_modules/fdir\": {\n      \"version\": \"6.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz\",\n      \"integrity\": \"sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      },\n      \"peerDependencies\": {\n        \"picomatch\": \"^3 || ^4\"\n      },\n      \"peerDependenciesMeta\": {\n        \"picomatch\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/fetch-blob\": {\n      \"version\": \"3.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz\",\n      \"integrity\": \"sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/jimmywarting\"\n        },\n        {\n          \"type\": \"paypal\",\n          \"url\": \"https://paypal.me/jimmywarting\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"node-domexception\": \"^1.0.0\",\n        \"web-streams-polyfill\": \"^3.0.3\"\n      },\n      \"engines\": {\n        \"node\": \"^12.20 || >= 14.13\"\n      }\n    },\n    \"node_modules/file-entry-cache\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz\",\n      \"integrity\": \"sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"flat-cache\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16.0.0\"\n      }\n    },\n    \"node_modules/file-type\": {\n      \"version\": \"21.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/file-type/-/file-type-21.3.1.tgz\",\n      \"integrity\": \"sha512-SrzXX46I/zsRDjTb82eucsGg0ODq2NpGDp4HcsFKApPy8P8vACjpJRDoGGMfEzhFC0ry61ajd7f72J3603anBA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@tokenizer/inflate\": \"^0.4.1\",\n        \"strtok3\": \"^10.3.4\",\n        \"token-types\": \"^6.1.1\",\n        \"uint8array-extras\": \"^1.4.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sindresorhus/file-type?sponsor=1\"\n      }\n    },\n    \"node_modules/filename-reserved-regex\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz\",\n      \"integrity\": \"sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^12.20.0 || ^14.13.1 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/filenamify\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz\",\n      \"integrity\": \"sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"filename-reserved-regex\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/finalhandler\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz\",\n      \"integrity\": \"sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"debug\": \"^4.4.0\",\n        \"encodeurl\": \"^2.0.0\",\n        \"escape-html\": \"^1.0.3\",\n        \"on-finished\": \"^2.4.1\",\n        \"parseurl\": \"^1.3.3\",\n        \"statuses\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 18.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/find-up\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz\",\n      \"integrity\": \"sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"locate-path\": \"^6.0.0\",\n        \"path-exists\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/flat-cache\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz\",\n      \"integrity\": \"sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"flatted\": \"^3.2.9\",\n        \"keyv\": \"^4.5.4\"\n      },\n      \"engines\": {\n        \"node\": \">=16\"\n      }\n    },\n    \"node_modules/flatted\": {\n      \"version\": \"3.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz\",\n      \"integrity\": \"sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/follow-redirects\": {\n      \"version\": \"1.15.11\",\n      \"resolved\": \"https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz\",\n      \"integrity\": \"sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==\",\n      \"funding\": [\n        {\n          \"type\": \"individual\",\n          \"url\": \"https://github.com/sponsors/RubenVerborgh\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=4.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"debug\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/foreground-child\": {\n      \"version\": \"3.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz\",\n      \"integrity\": \"sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"cross-spawn\": \"^7.0.6\",\n        \"signal-exit\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/foreground-child/node_modules/signal-exit\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz\",\n      \"integrity\": \"sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/form-data\": {\n      \"version\": \"4.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz\",\n      \"integrity\": \"sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"asynckit\": \"^0.4.0\",\n        \"combined-stream\": \"^1.0.8\",\n        \"es-set-tostringtag\": \"^2.1.0\",\n        \"hasown\": \"^2.0.2\",\n        \"mime-types\": \"^2.1.12\"\n      },\n      \"engines\": {\n        \"node\": \">= 6\"\n      }\n    },\n    \"node_modules/form-data/node_modules/mime-db\": {\n      \"version\": \"1.52.0\",\n      \"resolved\": \"https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz\",\n      \"integrity\": \"sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/form-data/node_modules/mime-types\": {\n      \"version\": \"2.1.35\",\n      \"resolved\": \"https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz\",\n      \"integrity\": \"sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"mime-db\": \"1.52.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/formdata-polyfill\": {\n      \"version\": \"4.0.10\",\n      \"resolved\": \"https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz\",\n      \"integrity\": \"sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"fetch-blob\": \"^3.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">=12.20.0\"\n      }\n    },\n    \"node_modules/forwarded\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz\",\n      \"integrity\": \"sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/fresh\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz\",\n      \"integrity\": \"sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/fs-extra\": {\n      \"version\": \"11.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz\",\n      \"integrity\": \"sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"graceful-fs\": \"^4.2.0\",\n        \"jsonfile\": \"^6.0.1\",\n        \"universalify\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=14.14\"\n      }\n    },\n    \"node_modules/fsevents\": {\n      \"version\": \"2.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz\",\n      \"integrity\": \"sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==\",\n      \"dev\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^8.16.0 || ^10.6.0 || >=11.0.0\"\n      }\n    },\n    \"node_modules/function-bind\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz\",\n      \"integrity\": \"sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==\",\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/gaxios\": {\n      \"version\": \"7.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz\",\n      \"integrity\": \"sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"extend\": \"^3.0.2\",\n        \"https-proxy-agent\": \"^7.0.1\",\n        \"node-fetch\": \"^3.3.2\",\n        \"rimraf\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/gaxios/node_modules/agent-base\": {\n      \"version\": \"7.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz\",\n      \"integrity\": \"sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/gaxios/node_modules/https-proxy-agent\": {\n      \"version\": \"7.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz\",\n      \"integrity\": \"sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"agent-base\": \"^7.1.2\",\n        \"debug\": \"4\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/gcp-metadata\": {\n      \"version\": \"8.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz\",\n      \"integrity\": \"sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"gaxios\": \"^7.0.0\",\n        \"google-logging-utils\": \"^1.0.0\",\n        \"json-bigint\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/get-caller-file\": {\n      \"version\": \"2.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz\",\n      \"integrity\": \"sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"6.* || 8.* || >= 10.*\"\n      }\n    },\n    \"node_modules/get-east-asian-width\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz\",\n      \"integrity\": \"sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/get-intrinsic\": {\n      \"version\": \"1.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz\",\n      \"integrity\": \"sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"call-bind-apply-helpers\": \"^1.0.2\",\n        \"es-define-property\": \"^1.0.1\",\n        \"es-errors\": \"^1.3.0\",\n        \"es-object-atoms\": \"^1.1.1\",\n        \"function-bind\": \"^1.1.2\",\n        \"get-proto\": \"^1.0.1\",\n        \"gopd\": \"^1.2.0\",\n        \"has-symbols\": \"^1.1.0\",\n        \"hasown\": \"^2.0.2\",\n        \"math-intrinsics\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/get-proto\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz\",\n      \"integrity\": \"sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"dunder-proto\": \"^1.0.1\",\n        \"es-object-atoms\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/get-stream\": {\n      \"version\": \"5.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz\",\n      \"integrity\": \"sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"pump\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/get-uri\": {\n      \"version\": \"6.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz\",\n      \"integrity\": \"sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"basic-ftp\": \"^5.0.2\",\n        \"data-uri-to-buffer\": \"^6.0.2\",\n        \"debug\": \"^4.3.4\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/get-uri/node_modules/data-uri-to-buffer\": {\n      \"version\": \"6.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz\",\n      \"integrity\": \"sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/glob\": {\n      \"version\": \"13.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/glob/-/glob-13.0.6.tgz\",\n      \"integrity\": \"sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"minimatch\": \"^10.2.2\",\n        \"minipass\": \"^7.1.3\",\n        \"path-scurry\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/glob-parent\": {\n      \"version\": \"6.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz\",\n      \"integrity\": \"sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"is-glob\": \"^4.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">=10.13.0\"\n      }\n    },\n    \"node_modules/glob/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz\",\n      \"integrity\": \"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/glob/node_modules/brace-expansion\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz\",\n      \"integrity\": \"sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/glob/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz\",\n      \"integrity\": \"sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/globals\": {\n      \"version\": \"14.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/globals/-/globals-14.0.0.tgz\",\n      \"integrity\": \"sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/google-auth-library\": {\n      \"version\": \"10.6.1\",\n      \"resolved\": \"https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz\",\n      \"integrity\": \"sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"base64-js\": \"^1.3.0\",\n        \"ecdsa-sig-formatter\": \"^1.0.11\",\n        \"gaxios\": \"7.1.3\",\n        \"gcp-metadata\": \"8.1.2\",\n        \"google-logging-utils\": \"1.1.3\",\n        \"jws\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/google-logging-utils\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz\",\n      \"integrity\": \"sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14\"\n      }\n    },\n    \"node_modules/gopd\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz\",\n      \"integrity\": \"sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/graceful-fs\": {\n      \"version\": \"4.2.11\",\n      \"resolved\": \"https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz\",\n      \"integrity\": \"sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/grammy\": {\n      \"version\": \"1.41.1\",\n      \"resolved\": \"https://registry.npmjs.org/grammy/-/grammy-1.41.1.tgz\",\n      \"integrity\": \"sha512-wcHAQ1e7svL3fJMpDchcQVcWUmywhuepOOjHUHmMmWAwUJEIyK5ea5sbSjZd+Gy1aMpZeP8VYJa+4tP+j1YptQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@grammyjs/types\": \"3.25.0\",\n        \"abort-controller\": \"^3.0.0\",\n        \"debug\": \"^4.4.3\",\n        \"node-fetch\": \"^2.7.0\"\n      },\n      \"engines\": {\n        \"node\": \"^12.20.0 || >=14.13.1\"\n      }\n    },\n    \"node_modules/grammy/node_modules/node-fetch\": {\n      \"version\": \"2.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz\",\n      \"integrity\": \"sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"whatwg-url\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"4.x || >=6.0.0\"\n      },\n      \"peerDependencies\": {\n        \"encoding\": \"^0.1.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"encoding\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/has-flag\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz\",\n      \"integrity\": \"sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/has-symbols\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz\",\n      \"integrity\": \"sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/has-tostringtag\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz\",\n      \"integrity\": \"sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"has-symbols\": \"^1.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/hashery\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/hashery/-/hashery-1.5.0.tgz\",\n      \"integrity\": \"sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"hookified\": \"^1.14.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/hasown\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz\",\n      \"integrity\": \"sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"function-bind\": \"^1.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/highlight.js\": {\n      \"version\": \"10.7.3\",\n      \"resolved\": \"https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz\",\n      \"integrity\": \"sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/hono\": {\n      \"version\": \"4.12.7\",\n      \"resolved\": \"https://registry.npmjs.org/hono/-/hono-4.12.7.tgz\",\n      \"integrity\": \"sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=16.9.0\"\n      }\n    },\n    \"node_modules/hookified\": {\n      \"version\": \"1.15.1\",\n      \"resolved\": \"https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz\",\n      \"integrity\": \"sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/hosted-git-info\": {\n      \"version\": \"9.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz\",\n      \"integrity\": \"sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"lru-cache\": \"^11.1.0\"\n      },\n      \"engines\": {\n        \"node\": \"^20.17.0 || >=22.9.0\"\n      }\n    },\n    \"node_modules/html-escaper\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz\",\n      \"integrity\": \"sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/htmlparser2\": {\n      \"version\": \"10.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz\",\n      \"integrity\": \"sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==\",\n      \"funding\": [\n        \"https://github.com/fb55/htmlparser2?sponsor=1\",\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/fb55\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"domelementtype\": \"^2.3.0\",\n        \"domhandler\": \"^5.0.3\",\n        \"domutils\": \"^3.2.2\",\n        \"entities\": \"^7.0.1\"\n      }\n    },\n    \"node_modules/htmlparser2/node_modules/entities\": {\n      \"version\": \"7.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/entities/-/entities-7.0.1.tgz\",\n      \"integrity\": \"sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/entities?sponsor=1\"\n      }\n    },\n    \"node_modules/http-errors\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz\",\n      \"integrity\": \"sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"depd\": \"~2.0.0\",\n        \"inherits\": \"~2.0.4\",\n        \"setprototypeof\": \"~1.2.0\",\n        \"statuses\": \"~2.0.2\",\n        \"toidentifier\": \"~1.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/http-proxy-agent\": {\n      \"version\": \"7.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz\",\n      \"integrity\": \"sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"agent-base\": \"^7.1.0\",\n        \"debug\": \"^4.3.4\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/http-proxy-agent/node_modules/agent-base\": {\n      \"version\": \"7.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz\",\n      \"integrity\": \"sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/https-proxy-agent\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-8.0.0.tgz\",\n      \"integrity\": \"sha512-YYeW+iCnAS3xhvj2dvVoWgsbca3RfQy/IlaNHHOtDmU0jMqPI9euIq3Y9BJETdxk16h9NHHCKqp/KB9nIMStCQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"agent-base\": \"8.0.0\",\n        \"debug\": \"^4.3.4\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/iconv-lite\": {\n      \"version\": \"0.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz\",\n      \"integrity\": \"sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"safer-buffer\": \">= 2.1.2 < 3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/ieee754\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz\",\n      \"integrity\": \"sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/feross\"\n        },\n        {\n          \"type\": \"patreon\",\n          \"url\": \"https://www.patreon.com/feross\"\n        },\n        {\n          \"type\": \"consulting\",\n          \"url\": \"https://feross.org/support\"\n        }\n      ],\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/ignore\": {\n      \"version\": \"5.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz\",\n      \"integrity\": \"sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/immediate\": {\n      \"version\": \"3.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz\",\n      \"integrity\": \"sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/import-fresh\": {\n      \"version\": \"3.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz\",\n      \"integrity\": \"sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"parent-module\": \"^1.0.0\",\n        \"resolve-from\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/imurmurhash\": {\n      \"version\": \"0.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz\",\n      \"integrity\": \"sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.8.19\"\n      }\n    },\n    \"node_modules/inherits\": {\n      \"version\": \"2.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz\",\n      \"integrity\": \"sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/ini\": {\n      \"version\": \"1.3.8\",\n      \"resolved\": \"https://registry.npmjs.org/ini/-/ini-1.3.8.tgz\",\n      \"integrity\": \"sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/ip-address\": {\n      \"version\": \"10.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz\",\n      \"integrity\": \"sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 12\"\n      }\n    },\n    \"node_modules/ipaddr.js\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz\",\n      \"integrity\": \"sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10\"\n      }\n    },\n    \"node_modules/ipull\": {\n      \"version\": \"3.9.5\",\n      \"resolved\": \"https://registry.npmjs.org/ipull/-/ipull-3.9.5.tgz\",\n      \"integrity\": \"sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@tinyhttp/content-disposition\": \"^2.2.0\",\n        \"async-retry\": \"^1.3.3\",\n        \"chalk\": \"^5.3.0\",\n        \"ci-info\": \"^4.0.0\",\n        \"cli-spinners\": \"^2.9.2\",\n        \"commander\": \"^10.0.0\",\n        \"eventemitter3\": \"^5.0.1\",\n        \"filenamify\": \"^6.0.0\",\n        \"fs-extra\": \"^11.1.1\",\n        \"is-unicode-supported\": \"^2.0.0\",\n        \"lifecycle-utils\": \"^2.0.1\",\n        \"lodash.debounce\": \"^4.0.8\",\n        \"lowdb\": \"^7.0.1\",\n        \"pretty-bytes\": \"^6.1.0\",\n        \"pretty-ms\": \"^8.0.0\",\n        \"sleep-promise\": \"^9.1.0\",\n        \"slice-ansi\": \"^7.1.0\",\n        \"stdout-update\": \"^4.0.1\",\n        \"strip-ansi\": \"^7.1.0\"\n      },\n      \"bin\": {\n        \"ipull\": \"dist/cli/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/ido-pluto/ipull?sponsor=1\"\n      },\n      \"optionalDependencies\": {\n        \"@reflink/reflink\": \"^0.1.16\"\n      }\n    },\n    \"node_modules/ipull/node_modules/ansi-styles\": {\n      \"version\": \"6.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz\",\n      \"integrity\": \"sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/ipull/node_modules/chalk\": {\n      \"version\": \"5.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz\",\n      \"integrity\": \"sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/ipull/node_modules/lifecycle-utils\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz\",\n      \"integrity\": \"sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/ipull/node_modules/parse-ms\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz\",\n      \"integrity\": \"sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/ipull/node_modules/pretty-ms\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz\",\n      \"integrity\": \"sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"parse-ms\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=14.16\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/ipull/node_modules/slice-ansi\": {\n      \"version\": \"7.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz\",\n      \"integrity\": \"sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-styles\": \"^6.2.1\",\n        \"is-fullwidth-code-point\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/slice-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/is-core-module\": {\n      \"version\": \"2.16.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz\",\n      \"integrity\": \"sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"hasown\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-electron\": {\n      \"version\": \"2.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz\",\n      \"integrity\": \"sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/is-extglob\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz\",\n      \"integrity\": \"sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is-fullwidth-code-point\": {\n      \"version\": \"5.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz\",\n      \"integrity\": \"sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"get-east-asian-width\": \"^1.3.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/is-glob\": {\n      \"version\": \"4.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz\",\n      \"integrity\": \"sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-extglob\": \"^2.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is-interactive\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz\",\n      \"integrity\": \"sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/is-promise\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz\",\n      \"integrity\": \"sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/is-stream\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz\",\n      \"integrity\": \"sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/is-unicode-supported\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz\",\n      \"integrity\": \"sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/isarray\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz\",\n      \"integrity\": \"sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/isexe\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz\",\n      \"integrity\": \"sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==\",\n      \"license\": \"ISC\"\n    },\n    \"node_modules/istanbul-lib-coverage\": {\n      \"version\": \"3.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz\",\n      \"integrity\": \"sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/istanbul-lib-report\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz\",\n      \"integrity\": \"sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"istanbul-lib-coverage\": \"^3.0.0\",\n        \"make-dir\": \"^4.0.0\",\n        \"supports-color\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/istanbul-reports\": {\n      \"version\": \"3.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz\",\n      \"integrity\": \"sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"html-escaper\": \"^2.0.0\",\n        \"istanbul-lib-report\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/jackspeak\": {\n      \"version\": \"3.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz\",\n      \"integrity\": \"sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@isaacs/cliui\": \"^8.0.2\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      },\n      \"optionalDependencies\": {\n        \"@pkgjs/parseargs\": \"^0.11.0\"\n      }\n    },\n    \"node_modules/jiti\": {\n      \"version\": \"2.6.1\",\n      \"resolved\": \"https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz\",\n      \"integrity\": \"sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"bin\": {\n        \"jiti\": \"lib/jiti-cli.mjs\"\n      }\n    },\n    \"node_modules/jju\": {\n      \"version\": \"1.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/jju/-/jju-1.4.0.tgz\",\n      \"integrity\": \"sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/js-tokens\": {\n      \"version\": \"10.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz\",\n      \"integrity\": \"sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/js-yaml\": {\n      \"version\": \"4.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz\",\n      \"integrity\": \"sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"argparse\": \"^2.0.1\"\n      },\n      \"bin\": {\n        \"js-yaml\": \"bin/js-yaml.js\"\n      }\n    },\n    \"node_modules/json-bigint\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz\",\n      \"integrity\": \"sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"bignumber.js\": \"^9.0.0\"\n      }\n    },\n    \"node_modules/json-buffer\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz\",\n      \"integrity\": \"sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/json-schema-to-ts\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz\",\n      \"integrity\": \"sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@babel/runtime\": \"^7.18.3\",\n        \"ts-algebra\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16\"\n      }\n    },\n    \"node_modules/json-schema-traverse\": {\n      \"version\": \"0.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz\",\n      \"integrity\": \"sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/json-stable-stringify-without-jsonify\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz\",\n      \"integrity\": \"sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/json-with-bigint\": {\n      \"version\": \"3.5.7\",\n      \"resolved\": \"https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz\",\n      \"integrity\": \"sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/json5\": {\n      \"version\": \"2.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/json5/-/json5-2.2.3.tgz\",\n      \"integrity\": \"sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"bin\": {\n        \"json5\": \"lib/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/jsonfile\": {\n      \"version\": \"6.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz\",\n      \"integrity\": \"sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"universalify\": \"^2.0.0\"\n      },\n      \"optionalDependencies\": {\n        \"graceful-fs\": \"^4.1.6\"\n      }\n    },\n    \"node_modules/jsonwebtoken\": {\n      \"version\": \"9.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz\",\n      \"integrity\": \"sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"jws\": \"^4.0.1\",\n        \"lodash.includes\": \"^4.3.0\",\n        \"lodash.isboolean\": \"^3.0.3\",\n        \"lodash.isinteger\": \"^4.0.4\",\n        \"lodash.isnumber\": \"^3.0.3\",\n        \"lodash.isplainobject\": \"^4.0.6\",\n        \"lodash.isstring\": \"^4.0.1\",\n        \"lodash.once\": \"^4.0.0\",\n        \"ms\": \"^2.1.1\",\n        \"semver\": \"^7.5.4\"\n      },\n      \"engines\": {\n        \"node\": \">=12\",\n        \"npm\": \">=6\"\n      }\n    },\n    \"node_modules/jszip\": {\n      \"version\": \"3.10.1\",\n      \"resolved\": \"https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz\",\n      \"integrity\": \"sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==\",\n      \"license\": \"(MIT OR GPL-3.0-or-later)\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"lie\": \"~3.3.0\",\n        \"pako\": \"~1.0.2\",\n        \"readable-stream\": \"~2.3.6\",\n        \"setimmediate\": \"^1.0.5\"\n      }\n    },\n    \"node_modules/jwa\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz\",\n      \"integrity\": \"sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"buffer-equal-constant-time\": \"^1.0.1\",\n        \"ecdsa-sig-formatter\": \"1.0.11\",\n        \"safe-buffer\": \"^5.0.1\"\n      }\n    },\n    \"node_modules/jws\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/jws/-/jws-4.0.1.tgz\",\n      \"integrity\": \"sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"jwa\": \"^2.0.1\",\n        \"safe-buffer\": \"^5.0.1\"\n      }\n    },\n    \"node_modules/keyv\": {\n      \"version\": \"4.5.4\",\n      \"resolved\": \"https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz\",\n      \"integrity\": \"sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"json-buffer\": \"3.0.1\"\n      }\n    },\n    \"node_modules/koffi\": {\n      \"version\": \"2.15.2\",\n      \"resolved\": \"https://registry.npmjs.org/koffi/-/koffi-2.15.2.tgz\",\n      \"integrity\": \"sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==\",\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://liberapay.com/Koromix\"\n      }\n    },\n    \"node_modules/levn\": {\n      \"version\": \"0.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/levn/-/levn-0.4.1.tgz\",\n      \"integrity\": \"sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prelude-ls\": \"^1.2.1\",\n        \"type-check\": \"~0.4.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/libsignal\": {\n      \"name\": \"@whiskeysockets/libsignal-node\",\n      \"version\": \"2.0.1\",\n      \"resolved\": \"git+ssh://git@github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67\",\n      \"license\": \"GPL-3.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"curve25519-js\": \"^0.0.4\",\n        \"protobufjs\": \"6.8.8\"\n      }\n    },\n    \"node_modules/libsignal/node_modules/@types/node\": {\n      \"version\": \"10.17.60\",\n      \"resolved\": \"https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz\",\n      \"integrity\": \"sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/libsignal/node_modules/long\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/long/-/long-4.0.0.tgz\",\n      \"integrity\": \"sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true\n    },\n    \"node_modules/libsignal/node_modules/protobufjs\": {\n      \"version\": \"6.8.8\",\n      \"resolved\": \"https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz\",\n      \"integrity\": \"sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==\",\n      \"hasInstallScript\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@protobufjs/aspromise\": \"^1.1.2\",\n        \"@protobufjs/base64\": \"^1.1.2\",\n        \"@protobufjs/codegen\": \"^2.0.4\",\n        \"@protobufjs/eventemitter\": \"^1.1.0\",\n        \"@protobufjs/fetch\": \"^1.1.0\",\n        \"@protobufjs/float\": \"^1.0.2\",\n        \"@protobufjs/inquire\": \"^1.1.0\",\n        \"@protobufjs/path\": \"^1.1.2\",\n        \"@protobufjs/pool\": \"^1.1.0\",\n        \"@protobufjs/utf8\": \"^1.1.0\",\n        \"@types/long\": \"^4.0.0\",\n        \"@types/node\": \"^10.1.0\",\n        \"long\": \"^4.0.0\"\n      },\n      \"bin\": {\n        \"pbjs\": \"bin/pbjs\",\n        \"pbts\": \"bin/pbts\"\n      }\n    },\n    \"node_modules/lie\": {\n      \"version\": \"3.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/lie/-/lie-3.3.0.tgz\",\n      \"integrity\": \"sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"immediate\": \"~3.0.5\"\n      }\n    },\n    \"node_modules/lifecycle-utils\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-3.1.1.tgz\",\n      \"integrity\": \"sha512-gNd3OvhFNjHykJE3uGntz7UuPzWlK9phrIdXxU9Adis0+ExkwnZibfxCJWiWWZ+a6VbKiZrb+9D9hCQWd4vjTg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lightningcss\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz\",\n      \"integrity\": \"sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==\",\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"dependencies\": {\n        \"detect-libc\": \"^2.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      },\n      \"optionalDependencies\": {\n        \"lightningcss-android-arm64\": \"1.32.0\",\n        \"lightningcss-darwin-arm64\": \"1.32.0\",\n        \"lightningcss-darwin-x64\": \"1.32.0\",\n        \"lightningcss-freebsd-x64\": \"1.32.0\",\n        \"lightningcss-linux-arm-gnueabihf\": \"1.32.0\",\n        \"lightningcss-linux-arm64-gnu\": \"1.32.0\",\n        \"lightningcss-linux-arm64-musl\": \"1.32.0\",\n        \"lightningcss-linux-x64-gnu\": \"1.32.0\",\n        \"lightningcss-linux-x64-musl\": \"1.32.0\",\n        \"lightningcss-win32-arm64-msvc\": \"1.32.0\",\n        \"lightningcss-win32-x64-msvc\": \"1.32.0\"\n      }\n    },\n    \"node_modules/lightningcss-android-arm64\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz\",\n      \"integrity\": \"sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-darwin-arm64\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz\",\n      \"integrity\": \"sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-darwin-x64\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz\",\n      \"integrity\": \"sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-freebsd-x64\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz\",\n      \"integrity\": \"sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-linux-arm-gnueabihf\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz\",\n      \"integrity\": \"sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-linux-arm64-gnu\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz\",\n      \"integrity\": \"sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-linux-arm64-musl\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz\",\n      \"integrity\": \"sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-linux-x64-gnu\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz\",\n      \"integrity\": \"sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-linux-x64-musl\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz\",\n      \"integrity\": \"sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-win32-arm64-msvc\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz\",\n      \"integrity\": \"sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/lightningcss-win32-x64-msvc\": {\n      \"version\": \"1.32.0\",\n      \"resolved\": \"https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz\",\n      \"integrity\": \"sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MPL-2.0\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">= 12.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/parcel\"\n      }\n    },\n    \"node_modules/linkedom\": {\n      \"version\": \"0.18.12\",\n      \"resolved\": \"https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz\",\n      \"integrity\": \"sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"css-select\": \"^5.1.0\",\n        \"cssom\": \"^0.5.0\",\n        \"html-escaper\": \"^3.0.3\",\n        \"htmlparser2\": \"^10.0.0\",\n        \"uhyphen\": \"^0.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16\"\n      },\n      \"peerDependencies\": {\n        \"canvas\": \">= 2\"\n      },\n      \"peerDependenciesMeta\": {\n        \"canvas\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/linkedom/node_modules/html-escaper\": {\n      \"version\": \"3.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz\",\n      \"integrity\": \"sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/linkify-it\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz\",\n      \"integrity\": \"sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"uc.micro\": \"^2.0.0\"\n      }\n    },\n    \"node_modules/locate-path\": {\n      \"version\": \"6.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz\",\n      \"integrity\": \"sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"p-locate\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/lodash.debounce\": {\n      \"version\": \"4.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz\",\n      \"integrity\": \"sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.identity\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.identity/-/lodash.identity-3.0.0.tgz\",\n      \"integrity\": \"sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.includes\": {\n      \"version\": \"4.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz\",\n      \"integrity\": \"sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.isboolean\": {\n      \"version\": \"3.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz\",\n      \"integrity\": \"sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.isinteger\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz\",\n      \"integrity\": \"sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.isnumber\": {\n      \"version\": \"3.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz\",\n      \"integrity\": \"sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.isplainobject\": {\n      \"version\": \"4.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz\",\n      \"integrity\": \"sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.isstring\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz\",\n      \"integrity\": \"sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.merge\": {\n      \"version\": \"4.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz\",\n      \"integrity\": \"sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/lodash.once\": {\n      \"version\": \"4.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz\",\n      \"integrity\": \"sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/lodash.pickby\": {\n      \"version\": \"4.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz\",\n      \"integrity\": \"sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/log-symbols\": {\n      \"version\": \"7.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz\",\n      \"integrity\": \"sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"is-unicode-supported\": \"^2.0.0\",\n        \"yoctocolors\": \"^2.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/long\": {\n      \"version\": \"5.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/long/-/long-5.3.2.tgz\",\n      \"integrity\": \"sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true\n    },\n    \"node_modules/lowdb\": {\n      \"version\": \"7.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz\",\n      \"integrity\": \"sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"steno\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/typicode\"\n      }\n    },\n    \"node_modules/lru-cache\": {\n      \"version\": \"11.2.6\",\n      \"resolved\": \"https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz\",\n      \"integrity\": \"sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"20 || >=22\"\n      }\n    },\n    \"node_modules/magic-string\": {\n      \"version\": \"0.30.21\",\n      \"resolved\": \"https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz\",\n      \"integrity\": \"sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/sourcemap-codec\": \"^1.5.5\"\n      }\n    },\n    \"node_modules/magicast\": {\n      \"version\": \"0.5.2\",\n      \"resolved\": \"https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz\",\n      \"integrity\": \"sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/parser\": \"^7.29.0\",\n        \"@babel/types\": \"^7.29.0\",\n        \"source-map-js\": \"^1.2.1\"\n      }\n    },\n    \"node_modules/make-dir\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz\",\n      \"integrity\": \"sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"semver\": \"^7.5.3\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/markdown-it\": {\n      \"version\": \"14.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz\",\n      \"integrity\": \"sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"argparse\": \"^2.0.1\",\n        \"entities\": \"^4.4.0\",\n        \"linkify-it\": \"^5.0.0\",\n        \"mdurl\": \"^2.0.0\",\n        \"punycode.js\": \"^2.3.1\",\n        \"uc.micro\": \"^2.1.0\"\n      },\n      \"bin\": {\n        \"markdown-it\": \"bin/markdown-it.mjs\"\n      }\n    },\n    \"node_modules/marked\": {\n      \"version\": \"15.0.12\",\n      \"resolved\": \"https://registry.npmjs.org/marked/-/marked-15.0.12.tgz\",\n      \"integrity\": \"sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"bin\": {\n        \"marked\": \"bin/marked.js\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\"\n      }\n    },\n    \"node_modules/math-intrinsics\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz\",\n      \"integrity\": \"sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/mdurl\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz\",\n      \"integrity\": \"sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/media-typer\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz\",\n      \"integrity\": \"sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/merge-descriptors\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz\",\n      \"integrity\": \"sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/mime-db\": {\n      \"version\": \"1.54.0\",\n      \"resolved\": \"https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz\",\n      \"integrity\": \"sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/mime-types\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz\",\n      \"integrity\": \"sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"mime-db\": \"^1.54.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/mimic-function\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz\",\n      \"integrity\": \"sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/minimatch\": {\n      \"version\": \"3.1.5\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz\",\n      \"integrity\": \"sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^1.1.7\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/minimist\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz\",\n      \"integrity\": \"sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/minipass\": {\n      \"version\": \"7.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz\",\n      \"integrity\": \"sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=16 || 14 >=14.17\"\n      }\n    },\n    \"node_modules/minizlib\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz\",\n      \"integrity\": \"sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"minipass\": \"^7.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\"\n      }\n    },\n    \"node_modules/ms\": {\n      \"version\": \"2.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/ms/-/ms-2.1.3.tgz\",\n      \"integrity\": \"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/music-metadata\": {\n      \"version\": \"11.12.2\",\n      \"resolved\": \"https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.2.tgz\",\n      \"integrity\": \"sha512-LsJHPigbRdAD49lcLAPtg5bjsIkc79mEbi9yV2NsWb9cA5jYG+4qLG2Lt3cTXHhCSEh5rFbed5AY8RIT0SXJ3A==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/Borewit\"\n        },\n        {\n          \"type\": \"buymeacoffee\",\n          \"url\": \"https://buymeacoffee.com/borewit\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@borewit/text-codec\": \"^0.2.2\",\n        \"@tokenizer/token\": \"^0.3.0\",\n        \"content-type\": \"^1.0.5\",\n        \"debug\": \"^4.4.3\",\n        \"file-type\": \"^21.3.1\",\n        \"media-typer\": \"^1.1.0\",\n        \"strtok3\": \"^10.3.4\",\n        \"token-types\": \"^6.1.2\",\n        \"uint8array-extras\": \"^1.5.0\",\n        \"win-guid\": \"^0.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/mz\": {\n      \"version\": \"2.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/mz/-/mz-2.7.0.tgz\",\n      \"integrity\": \"sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"any-promise\": \"^1.0.0\",\n        \"object-assign\": \"^4.0.1\",\n        \"thenify-all\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/nanoid\": {\n      \"version\": \"5.1.6\",\n      \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz\",\n      \"integrity\": \"sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"bin\": {\n        \"nanoid\": \"bin/nanoid.js\"\n      },\n      \"engines\": {\n        \"node\": \"^18 || >=20\"\n      }\n    },\n    \"node_modules/natural-compare\": {\n      \"version\": \"1.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz\",\n      \"integrity\": \"sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/negotiator\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz\",\n      \"integrity\": \"sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/netmask\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz\",\n      \"integrity\": \"sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.4.0\"\n      }\n    },\n    \"node_modules/node-addon-api\": {\n      \"version\": \"8.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz\",\n      \"integrity\": \"sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^18 || ^20 || >= 21\"\n      }\n    },\n    \"node_modules/node-api-headers\": {\n      \"version\": \"1.8.0\",\n      \"resolved\": \"https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.8.0.tgz\",\n      \"integrity\": \"sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/node-domexception\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz\",\n      \"integrity\": \"sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==\",\n      \"deprecated\": \"Use your platform's native DOMException instead\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/jimmywarting\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://paypal.me/jimmywarting\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=10.5.0\"\n      }\n    },\n    \"node_modules/node-edge-tts\": {\n      \"version\": \"1.2.10\",\n      \"resolved\": \"https://registry.npmjs.org/node-edge-tts/-/node-edge-tts-1.2.10.tgz\",\n      \"integrity\": \"sha512-bV2i4XU54D45+US0Zm1HcJRkifuB3W438dWyuJEHLQdKxnuqlI1kim2MOvR6Q3XUQZvfF9PoDyR1Rt7aeXhPdQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"https-proxy-agent\": \"^7.0.1\",\n        \"ws\": \"^8.13.0\",\n        \"yargs\": \"^17.7.2\"\n      },\n      \"bin\": {\n        \"node-edge-tts\": \"bin.js\"\n      }\n    },\n    \"node_modules/node-edge-tts/node_modules/agent-base\": {\n      \"version\": \"7.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz\",\n      \"integrity\": \"sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/node-edge-tts/node_modules/https-proxy-agent\": {\n      \"version\": \"7.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz\",\n      \"integrity\": \"sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"agent-base\": \"^7.1.2\",\n        \"debug\": \"4\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/node-fetch\": {\n      \"version\": \"3.3.2\",\n      \"resolved\": \"https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz\",\n      \"integrity\": \"sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"data-uri-to-buffer\": \"^4.0.0\",\n        \"fetch-blob\": \"^3.1.4\",\n        \"formdata-polyfill\": \"^4.0.10\"\n      },\n      \"engines\": {\n        \"node\": \"^12.20.0 || ^14.13.1 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/node-fetch\"\n      }\n    },\n    \"node_modules/node-llama-cpp\": {\n      \"version\": \"3.16.2\",\n      \"resolved\": \"https://registry.npmjs.org/node-llama-cpp/-/node-llama-cpp-3.16.2.tgz\",\n      \"integrity\": \"sha512-ovhuTaXSWfcoyfI8ljWxO2Rg63mNxqQQAbDGkXRhlgsL7UjPqm2Nsy1bTNa0ZaQRg3vezG4agnCJTImrICY/0A==\",\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@huggingface/jinja\": \"^0.5.5\",\n        \"async-retry\": \"^1.3.3\",\n        \"bytes\": \"^3.1.2\",\n        \"chalk\": \"^5.6.2\",\n        \"chmodrp\": \"^1.0.2\",\n        \"cmake-js\": \"^8.0.0\",\n        \"cross-spawn\": \"^7.0.6\",\n        \"env-var\": \"^7.5.0\",\n        \"filenamify\": \"^6.0.0\",\n        \"fs-extra\": \"^11.3.0\",\n        \"ignore\": \"^7.0.4\",\n        \"ipull\": \"^3.9.5\",\n        \"is-unicode-supported\": \"^2.1.0\",\n        \"lifecycle-utils\": \"^3.1.1\",\n        \"log-symbols\": \"^7.0.1\",\n        \"nanoid\": \"^5.1.6\",\n        \"node-addon-api\": \"^8.5.0\",\n        \"octokit\": \"^5.0.5\",\n        \"ora\": \"^9.3.0\",\n        \"pretty-ms\": \"^9.3.0\",\n        \"proper-lockfile\": \"^4.1.2\",\n        \"semver\": \"^7.7.1\",\n        \"simple-git\": \"^3.31.1\",\n        \"slice-ansi\": \"^8.0.0\",\n        \"stdout-update\": \"^4.0.1\",\n        \"strip-ansi\": \"^7.1.2\",\n        \"validate-npm-package-name\": \"^7.0.2\",\n        \"which\": \"^6.0.1\",\n        \"yargs\": \"^17.7.2\"\n      },\n      \"bin\": {\n        \"nlc\": \"dist/cli/cli.js\",\n        \"node-llama-cpp\": \"dist/cli/cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=20.0.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/giladgd\"\n      },\n      \"optionalDependencies\": {\n        \"@node-llama-cpp/linux-arm64\": \"3.16.2\",\n        \"@node-llama-cpp/linux-armv7l\": \"3.16.2\",\n        \"@node-llama-cpp/linux-x64\": \"3.16.2\",\n        \"@node-llama-cpp/linux-x64-cuda\": \"3.16.2\",\n        \"@node-llama-cpp/linux-x64-cuda-ext\": \"3.16.2\",\n        \"@node-llama-cpp/linux-x64-vulkan\": \"3.16.2\",\n        \"@node-llama-cpp/mac-arm64-metal\": \"3.16.2\",\n        \"@node-llama-cpp/mac-x64\": \"3.16.2\",\n        \"@node-llama-cpp/win-arm64\": \"3.16.2\",\n        \"@node-llama-cpp/win-x64\": \"3.16.2\",\n        \"@node-llama-cpp/win-x64-cuda\": \"3.16.2\",\n        \"@node-llama-cpp/win-x64-cuda-ext\": \"3.16.2\",\n        \"@node-llama-cpp/win-x64-vulkan\": \"3.16.2\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=5.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"typescript\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/node-llama-cpp/node_modules/chalk\": {\n      \"version\": \"5.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz\",\n      \"integrity\": \"sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/node-llama-cpp/node_modules/ignore\": {\n      \"version\": \"7.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz\",\n      \"integrity\": \"sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/node-llama-cpp/node_modules/isexe\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz\",\n      \"integrity\": \"sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/node-llama-cpp/node_modules/which\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/which/-/which-6.0.1.tgz\",\n      \"integrity\": \"sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"isexe\": \"^4.0.0\"\n      },\n      \"bin\": {\n        \"node-which\": \"bin/which.js\"\n      },\n      \"engines\": {\n        \"node\": \"^20.17.0 || >=22.9.0\"\n      }\n    },\n    \"node_modules/node-readable-to-web-readable-stream\": {\n      \"version\": \"0.4.2\",\n      \"resolved\": \"https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz\",\n      \"integrity\": \"sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==\",\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"peer\": true\n    },\n    \"node_modules/nth-check\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz\",\n      \"integrity\": \"sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"boolbase\": \"^1.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fb55/nth-check?sponsor=1\"\n      }\n    },\n    \"node_modules/object-assign\": {\n      \"version\": \"4.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz\",\n      \"integrity\": \"sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/object-inspect\": {\n      \"version\": \"1.13.4\",\n      \"resolved\": \"https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz\",\n      \"integrity\": \"sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/obug\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/obug/-/obug-2.1.1.tgz\",\n      \"integrity\": \"sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==\",\n      \"dev\": true,\n      \"funding\": [\n        \"https://github.com/sponsors/sxzz\",\n        \"https://opencollective.com/debug\"\n      ],\n      \"license\": \"MIT\"\n    },\n    \"node_modules/octokit\": {\n      \"version\": \"5.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/octokit/-/octokit-5.0.5.tgz\",\n      \"integrity\": \"sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@octokit/app\": \"^16.1.2\",\n        \"@octokit/core\": \"^7.0.6\",\n        \"@octokit/oauth-app\": \"^8.0.3\",\n        \"@octokit/plugin-paginate-graphql\": \"^6.0.0\",\n        \"@octokit/plugin-paginate-rest\": \"^14.0.0\",\n        \"@octokit/plugin-rest-endpoint-methods\": \"^17.0.0\",\n        \"@octokit/plugin-retry\": \"^8.0.3\",\n        \"@octokit/plugin-throttling\": \"^11.0.3\",\n        \"@octokit/request-error\": \"^7.0.2\",\n        \"@octokit/types\": \"^16.0.0\",\n        \"@octokit/webhooks\": \"^14.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 20\"\n      }\n    },\n    \"node_modules/on-exit-leak-free\": {\n      \"version\": \"2.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz\",\n      \"integrity\": \"sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/on-finished\": {\n      \"version\": \"2.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz\",\n      \"integrity\": \"sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ee-first\": \"1.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/once\": {\n      \"version\": \"1.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/once/-/once-1.4.0.tgz\",\n      \"integrity\": \"sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"wrappy\": \"1\"\n      }\n    },\n    \"node_modules/onetime\": {\n      \"version\": \"7.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz\",\n      \"integrity\": \"sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"mimic-function\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/openai\": {\n      \"version\": \"6.26.0\",\n      \"resolved\": \"https://registry.npmjs.org/openai/-/openai-6.26.0.tgz\",\n      \"integrity\": \"sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"bin\": {\n        \"openai\": \"bin/cli\"\n      },\n      \"peerDependencies\": {\n        \"ws\": \"^8.18.0\",\n        \"zod\": \"^3.25 || ^4.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"ws\": {\n          \"optional\": true\n        },\n        \"zod\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/openclaw\": {\n      \"version\": \"2026.3.11\",\n      \"resolved\": \"https://registry.npmjs.org/openclaw/-/openclaw-2026.3.11.tgz\",\n      \"integrity\": \"sha512-bxwiBmHPakwfpY5tqC9lrV5TCu5PKf0c1bHNc3nhrb+pqKcPEWV4zOjDVFLQUHr98ihgWA+3pacy4b3LQ8wduQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@agentclientprotocol/sdk\": \"0.16.1\",\n        \"@aws-sdk/client-bedrock\": \"^3.1007.0\",\n        \"@buape/carbon\": \"0.0.0-beta-20260216184201\",\n        \"@clack/prompts\": \"^1.1.0\",\n        \"@discordjs/voice\": \"^0.19.1\",\n        \"@grammyjs/runner\": \"^2.0.3\",\n        \"@grammyjs/transformer-throttler\": \"^1.2.1\",\n        \"@homebridge/ciao\": \"^1.3.5\",\n        \"@larksuiteoapi/node-sdk\": \"^1.59.0\",\n        \"@line/bot-sdk\": \"^10.6.0\",\n        \"@lydell/node-pty\": \"1.2.0-beta.3\",\n        \"@mariozechner/pi-agent-core\": \"0.57.1\",\n        \"@mariozechner/pi-ai\": \"0.57.1\",\n        \"@mariozechner/pi-coding-agent\": \"0.57.1\",\n        \"@mariozechner/pi-tui\": \"0.57.1\",\n        \"@mozilla/readability\": \"^0.6.0\",\n        \"@sinclair/typebox\": \"0.34.48\",\n        \"@slack/bolt\": \"^4.6.0\",\n        \"@slack/web-api\": \"^7.14.1\",\n        \"@whiskeysockets/baileys\": \"7.0.0-rc.9\",\n        \"ajv\": \"^8.18.0\",\n        \"chalk\": \"^5.6.2\",\n        \"chokidar\": \"^5.0.0\",\n        \"cli-highlight\": \"^2.1.11\",\n        \"commander\": \"^14.0.3\",\n        \"croner\": \"^10.0.1\",\n        \"discord-api-types\": \"^0.38.42\",\n        \"dotenv\": \"^17.3.1\",\n        \"express\": \"^5.2.1\",\n        \"file-type\": \"^21.3.1\",\n        \"grammy\": \"^1.41.1\",\n        \"hono\": \"4.12.7\",\n        \"https-proxy-agent\": \"^8.0.0\",\n        \"ipaddr.js\": \"^2.3.0\",\n        \"jiti\": \"^2.6.1\",\n        \"json5\": \"^2.2.3\",\n        \"jszip\": \"^3.10.1\",\n        \"linkedom\": \"^0.18.12\",\n        \"long\": \"^5.3.2\",\n        \"markdown-it\": \"^14.1.1\",\n        \"node-edge-tts\": \"^1.2.10\",\n        \"opusscript\": \"^0.1.1\",\n        \"osc-progress\": \"^0.3.0\",\n        \"pdfjs-dist\": \"^5.5.207\",\n        \"playwright-core\": \"1.58.2\",\n        \"qrcode-terminal\": \"^0.12.0\",\n        \"sharp\": \"^0.34.5\",\n        \"sqlite-vec\": \"0.1.7-alpha.2\",\n        \"tar\": \"7.5.11\",\n        \"tslog\": \"^4.10.2\",\n        \"undici\": \"^7.22.0\",\n        \"ws\": \"^8.19.0\",\n        \"yaml\": \"^2.8.2\",\n        \"zod\": \"^4.3.6\"\n      },\n      \"bin\": {\n        \"openclaw\": \"openclaw.mjs\"\n      },\n      \"engines\": {\n        \"node\": \">=22.12.0\"\n      },\n      \"peerDependencies\": {\n        \"@napi-rs/canvas\": \"^0.1.89\",\n        \"node-llama-cpp\": \"3.16.2\"\n      }\n    },\n    \"node_modules/openclaw/node_modules/ajv\": {\n      \"version\": \"8.18.0\",\n      \"resolved\": \"https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz\",\n      \"integrity\": \"sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"fast-uri\": \"^3.0.1\",\n        \"json-schema-traverse\": \"^1.0.0\",\n        \"require-from-string\": \"^2.0.2\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/epoberezkin\"\n      }\n    },\n    \"node_modules/openclaw/node_modules/chalk\": {\n      \"version\": \"5.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz\",\n      \"integrity\": \"sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/openclaw/node_modules/commander\": {\n      \"version\": \"14.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/commander/-/commander-14.0.3.tgz\",\n      \"integrity\": \"sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/openclaw/node_modules/json-schema-traverse\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz\",\n      \"integrity\": \"sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/optionator\": {\n      \"version\": \"0.9.4\",\n      \"resolved\": \"https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz\",\n      \"integrity\": \"sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"deep-is\": \"^0.1.3\",\n        \"fast-levenshtein\": \"^2.0.6\",\n        \"levn\": \"^0.4.1\",\n        \"prelude-ls\": \"^1.2.1\",\n        \"type-check\": \"^0.4.0\",\n        \"word-wrap\": \"^1.2.5\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/opusscript\": {\n      \"version\": \"0.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/opusscript/-/opusscript-0.1.1.tgz\",\n      \"integrity\": \"sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/ora\": {\n      \"version\": \"9.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/ora/-/ora-9.3.0.tgz\",\n      \"integrity\": \"sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"chalk\": \"^5.6.2\",\n        \"cli-cursor\": \"^5.0.0\",\n        \"cli-spinners\": \"^3.2.0\",\n        \"is-interactive\": \"^2.0.0\",\n        \"is-unicode-supported\": \"^2.1.0\",\n        \"log-symbols\": \"^7.0.1\",\n        \"stdin-discarder\": \"^0.3.1\",\n        \"string-width\": \"^8.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/ora/node_modules/chalk\": {\n      \"version\": \"5.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz\",\n      \"integrity\": \"sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/ora/node_modules/cli-spinners\": {\n      \"version\": \"3.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz\",\n      \"integrity\": \"sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18.20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/osc-progress\": {\n      \"version\": \"0.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/osc-progress/-/osc-progress-0.3.0.tgz\",\n      \"integrity\": \"sha512-4/8JfsetakdeEa4vAYV45FW20aY+B/+K8NEXp5Eiar3wR8726whgHrbSg5Ar/ZY1FLJ/AGtUqV7W2IVF+Gvp9A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/p-finally\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz\",\n      \"integrity\": \"sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/p-limit\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz\",\n      \"integrity\": \"sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"yocto-queue\": \"^0.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/p-locate\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz\",\n      \"integrity\": \"sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"p-limit\": \"^3.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/p-queue\": {\n      \"version\": \"6.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz\",\n      \"integrity\": \"sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"eventemitter3\": \"^4.0.4\",\n        \"p-timeout\": \"^3.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/p-queue/node_modules/eventemitter3\": {\n      \"version\": \"4.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz\",\n      \"integrity\": \"sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/p-retry\": {\n      \"version\": \"4.6.2\",\n      \"resolved\": \"https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz\",\n      \"integrity\": \"sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@types/retry\": \"0.12.0\",\n        \"retry\": \"^0.13.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/p-timeout\": {\n      \"version\": \"3.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz\",\n      \"integrity\": \"sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"p-finally\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/pac-proxy-agent\": {\n      \"version\": \"7.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz\",\n      \"integrity\": \"sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@tootallnate/quickjs-emscripten\": \"^0.23.0\",\n        \"agent-base\": \"^7.1.2\",\n        \"debug\": \"^4.3.4\",\n        \"get-uri\": \"^6.0.1\",\n        \"http-proxy-agent\": \"^7.0.0\",\n        \"https-proxy-agent\": \"^7.0.6\",\n        \"pac-resolver\": \"^7.0.1\",\n        \"socks-proxy-agent\": \"^8.0.5\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/pac-proxy-agent/node_modules/agent-base\": {\n      \"version\": \"7.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz\",\n      \"integrity\": \"sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/pac-proxy-agent/node_modules/https-proxy-agent\": {\n      \"version\": \"7.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz\",\n      \"integrity\": \"sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"agent-base\": \"^7.1.2\",\n        \"debug\": \"4\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/pac-resolver\": {\n      \"version\": \"7.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz\",\n      \"integrity\": \"sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"degenerator\": \"^5.0.0\",\n        \"netmask\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/package-json-from-dist\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz\",\n      \"integrity\": \"sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true\n    },\n    \"node_modules/pako\": {\n      \"version\": \"1.0.11\",\n      \"resolved\": \"https://registry.npmjs.org/pako/-/pako-1.0.11.tgz\",\n      \"integrity\": \"sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==\",\n      \"license\": \"(MIT AND Zlib)\",\n      \"peer\": true\n    },\n    \"node_modules/parent-module\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz\",\n      \"integrity\": \"sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"callsites\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/parse-ms\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz\",\n      \"integrity\": \"sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/parse5\": {\n      \"version\": \"5.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz\",\n      \"integrity\": \"sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/parse5-htmlparser2-tree-adapter\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz\",\n      \"integrity\": \"sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"parse5\": \"^6.0.1\"\n      }\n    },\n    \"node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz\",\n      \"integrity\": \"sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/parseurl\": {\n      \"version\": \"1.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz\",\n      \"integrity\": \"sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/partial-json\": {\n      \"version\": \"0.1.7\",\n      \"resolved\": \"https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz\",\n      \"integrity\": \"sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/path-exists\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz\",\n      \"integrity\": \"sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/path-expression-matcher\": {\n      \"version\": \"1.1.3\",\n      \"resolved\": \"https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz\",\n      \"integrity\": \"sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/NaturalIntelligence\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/path-key\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz\",\n      \"integrity\": \"sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/path-parse\": {\n      \"version\": \"1.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz\",\n      \"integrity\": \"sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/path-scurry\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz\",\n      \"integrity\": \"sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"lru-cache\": \"^11.0.0\",\n        \"minipass\": \"^7.1.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/path-to-regexp\": {\n      \"version\": \"8.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz\",\n      \"integrity\": \"sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/pathe\": {\n      \"version\": \"2.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz\",\n      \"integrity\": \"sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/pdfjs-dist\": {\n      \"version\": \"5.5.207\",\n      \"resolved\": \"https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz\",\n      \"integrity\": \"sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.19.0 || >=22.13.0 || >=24\"\n      },\n      \"optionalDependencies\": {\n        \"@napi-rs/canvas\": \"^0.1.95\",\n        \"node-readable-to-web-readable-stream\": \"^0.4.2\"\n      }\n    },\n    \"node_modules/pend\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/pend/-/pend-1.2.0.tgz\",\n      \"integrity\": \"sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/picocolors\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz\",\n      \"integrity\": \"sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/picomatch\": {\n      \"version\": \"4.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz\",\n      \"integrity\": \"sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/jonschlinkert\"\n      }\n    },\n    \"node_modules/pino\": {\n      \"version\": \"9.14.0\",\n      \"resolved\": \"https://registry.npmjs.org/pino/-/pino-9.14.0.tgz\",\n      \"integrity\": \"sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@pinojs/redact\": \"^0.4.0\",\n        \"atomic-sleep\": \"^1.0.0\",\n        \"on-exit-leak-free\": \"^2.1.0\",\n        \"pino-abstract-transport\": \"^2.0.0\",\n        \"pino-std-serializers\": \"^7.0.0\",\n        \"process-warning\": \"^5.0.0\",\n        \"quick-format-unescaped\": \"^4.0.3\",\n        \"real-require\": \"^0.2.0\",\n        \"safe-stable-stringify\": \"^2.3.1\",\n        \"sonic-boom\": \"^4.0.1\",\n        \"thread-stream\": \"^3.0.0\"\n      },\n      \"bin\": {\n        \"pino\": \"bin.js\"\n      }\n    },\n    \"node_modules/pino-abstract-transport\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz\",\n      \"integrity\": \"sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"split2\": \"^4.0.0\"\n      }\n    },\n    \"node_modules/pino-std-serializers\": {\n      \"version\": \"7.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz\",\n      \"integrity\": \"sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/playwright-core\": {\n      \"version\": \"1.58.2\",\n      \"resolved\": \"https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz\",\n      \"integrity\": \"sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==\",\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"bin\": {\n        \"playwright-core\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/postcss\": {\n      \"version\": \"8.5.8\",\n      \"resolved\": \"https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz\",\n      \"integrity\": \"sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==\",\n      \"dev\": true,\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/postcss/\"\n        },\n        {\n          \"type\": \"tidelift\",\n          \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"nanoid\": \"^3.3.11\",\n        \"picocolors\": \"^1.1.1\",\n        \"source-map-js\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || >=14\"\n      }\n    },\n    \"node_modules/postcss/node_modules/nanoid\": {\n      \"version\": \"3.3.11\",\n      \"resolved\": \"https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz\",\n      \"integrity\": \"sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==\",\n      \"dev\": true,\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"nanoid\": \"bin/nanoid.cjs\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\"\n      }\n    },\n    \"node_modules/prelude-ls\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz\",\n      \"integrity\": \"sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/prettier\": {\n      \"version\": \"3.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz\",\n      \"integrity\": \"sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"prettier\": \"bin/prettier.cjs\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/prettier/prettier?sponsor=1\"\n      }\n    },\n    \"node_modules/pretty-bytes\": {\n      \"version\": \"6.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz\",\n      \"integrity\": \"sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^14.13.1 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/pretty-ms\": {\n      \"version\": \"9.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz\",\n      \"integrity\": \"sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"parse-ms\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/process-nextick-args\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz\",\n      \"integrity\": \"sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/process-warning\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz\",\n      \"integrity\": \"sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/fastify\"\n        },\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/fastify\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/proper-lockfile\": {\n      \"version\": \"4.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz\",\n      \"integrity\": \"sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"graceful-fs\": \"^4.2.4\",\n        \"retry\": \"^0.12.0\",\n        \"signal-exit\": \"^3.0.2\"\n      }\n    },\n    \"node_modules/proper-lockfile/node_modules/retry\": {\n      \"version\": \"0.12.0\",\n      \"resolved\": \"https://registry.npmjs.org/retry/-/retry-0.12.0.tgz\",\n      \"integrity\": \"sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/protobufjs\": {\n      \"version\": \"7.5.4\",\n      \"resolved\": \"https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz\",\n      \"integrity\": \"sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==\",\n      \"hasInstallScript\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@protobufjs/aspromise\": \"^1.1.2\",\n        \"@protobufjs/base64\": \"^1.1.2\",\n        \"@protobufjs/codegen\": \"^2.0.4\",\n        \"@protobufjs/eventemitter\": \"^1.1.0\",\n        \"@protobufjs/fetch\": \"^1.1.0\",\n        \"@protobufjs/float\": \"^1.0.2\",\n        \"@protobufjs/inquire\": \"^1.1.0\",\n        \"@protobufjs/path\": \"^1.1.2\",\n        \"@protobufjs/pool\": \"^1.1.0\",\n        \"@protobufjs/utf8\": \"^1.1.0\",\n        \"@types/node\": \">=13.7.0\",\n        \"long\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      }\n    },\n    \"node_modules/proxy-addr\": {\n      \"version\": \"2.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz\",\n      \"integrity\": \"sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"forwarded\": \"0.2.0\",\n        \"ipaddr.js\": \"1.9.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.10\"\n      }\n    },\n    \"node_modules/proxy-addr/node_modules/ipaddr.js\": {\n      \"version\": \"1.9.1\",\n      \"resolved\": \"https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz\",\n      \"integrity\": \"sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.10\"\n      }\n    },\n    \"node_modules/proxy-agent\": {\n      \"version\": \"6.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz\",\n      \"integrity\": \"sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"agent-base\": \"^7.1.2\",\n        \"debug\": \"^4.3.4\",\n        \"http-proxy-agent\": \"^7.0.1\",\n        \"https-proxy-agent\": \"^7.0.6\",\n        \"lru-cache\": \"^7.14.1\",\n        \"pac-proxy-agent\": \"^7.1.0\",\n        \"proxy-from-env\": \"^1.1.0\",\n        \"socks-proxy-agent\": \"^8.0.5\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/proxy-agent/node_modules/agent-base\": {\n      \"version\": \"7.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz\",\n      \"integrity\": \"sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/proxy-agent/node_modules/https-proxy-agent\": {\n      \"version\": \"7.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz\",\n      \"integrity\": \"sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"agent-base\": \"^7.1.2\",\n        \"debug\": \"4\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/proxy-agent/node_modules/lru-cache\": {\n      \"version\": \"7.18.3\",\n      \"resolved\": \"https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz\",\n      \"integrity\": \"sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/proxy-from-env\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz\",\n      \"integrity\": \"sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/pump\": {\n      \"version\": \"3.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/pump/-/pump-3.0.4.tgz\",\n      \"integrity\": \"sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"end-of-stream\": \"^1.1.0\",\n        \"once\": \"^1.3.1\"\n      }\n    },\n    \"node_modules/punycode\": {\n      \"version\": \"2.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz\",\n      \"integrity\": \"sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/punycode.js\": {\n      \"version\": \"2.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz\",\n      \"integrity\": \"sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/qified\": {\n      \"version\": \"0.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/qified/-/qified-0.6.0.tgz\",\n      \"integrity\": \"sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"hookified\": \"^1.14.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      }\n    },\n    \"node_modules/qrcode-terminal\": {\n      \"version\": \"0.12.0\",\n      \"resolved\": \"https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz\",\n      \"integrity\": \"sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==\",\n      \"peer\": true,\n      \"bin\": {\n        \"qrcode-terminal\": \"bin/qrcode-terminal.js\"\n      }\n    },\n    \"node_modules/qs\": {\n      \"version\": \"6.15.0\",\n      \"resolved\": \"https://registry.npmjs.org/qs/-/qs-6.15.0.tgz\",\n      \"integrity\": \"sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"side-channel\": \"^1.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/quick-format-unescaped\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz\",\n      \"integrity\": \"sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/range-parser\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz\",\n      \"integrity\": \"sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/raw-body\": {\n      \"version\": \"3.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz\",\n      \"integrity\": \"sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"bytes\": \"~3.1.2\",\n        \"http-errors\": \"~2.0.1\",\n        \"iconv-lite\": \"~0.7.0\",\n        \"unpipe\": \"~1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.10\"\n      }\n    },\n    \"node_modules/rc\": {\n      \"version\": \"1.2.8\",\n      \"resolved\": \"https://registry.npmjs.org/rc/-/rc-1.2.8.tgz\",\n      \"integrity\": \"sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==\",\n      \"license\": \"(BSD-2-Clause OR MIT OR Apache-2.0)\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"deep-extend\": \"^0.6.0\",\n        \"ini\": \"~1.3.0\",\n        \"minimist\": \"^1.2.0\",\n        \"strip-json-comments\": \"~2.0.1\"\n      },\n      \"bin\": {\n        \"rc\": \"cli.js\"\n      }\n    },\n    \"node_modules/rc/node_modules/strip-json-comments\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz\",\n      \"integrity\": \"sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/readable-stream\": {\n      \"version\": \"2.3.8\",\n      \"resolved\": \"https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz\",\n      \"integrity\": \"sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"core-util-is\": \"~1.0.0\",\n        \"inherits\": \"~2.0.3\",\n        \"isarray\": \"~1.0.0\",\n        \"process-nextick-args\": \"~2.0.0\",\n        \"safe-buffer\": \"~5.1.1\",\n        \"string_decoder\": \"~1.1.1\",\n        \"util-deprecate\": \"~1.0.1\"\n      }\n    },\n    \"node_modules/readable-stream/node_modules/safe-buffer\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz\",\n      \"integrity\": \"sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/readdirp\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz\",\n      \"integrity\": \"sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 20.19.0\"\n      },\n      \"funding\": {\n        \"type\": \"individual\",\n        \"url\": \"https://paulmillr.com/funding/\"\n      }\n    },\n    \"node_modules/real-require\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz\",\n      \"integrity\": \"sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 12.13.0\"\n      }\n    },\n    \"node_modules/require-directory\": {\n      \"version\": \"2.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz\",\n      \"integrity\": \"sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/require-from-string\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz\",\n      \"integrity\": \"sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/resolve\": {\n      \"version\": \"1.22.11\",\n      \"resolved\": \"https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz\",\n      \"integrity\": \"sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-core-module\": \"^2.16.1\",\n        \"path-parse\": \"^1.0.7\",\n        \"supports-preserve-symlinks-flag\": \"^1.0.0\"\n      },\n      \"bin\": {\n        \"resolve\": \"bin/resolve\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/resolve-from\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz\",\n      \"integrity\": \"sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/restore-cursor\": {\n      \"version\": \"5.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz\",\n      \"integrity\": \"sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"onetime\": \"^7.0.0\",\n        \"signal-exit\": \"^4.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/restore-cursor/node_modules/signal-exit\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz\",\n      \"integrity\": \"sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/retry\": {\n      \"version\": \"0.13.1\",\n      \"resolved\": \"https://registry.npmjs.org/retry/-/retry-0.13.1.tgz\",\n      \"integrity\": \"sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/rimraf\": {\n      \"version\": \"5.0.10\",\n      \"resolved\": \"https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz\",\n      \"integrity\": \"sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"glob\": \"^10.3.7\"\n      },\n      \"bin\": {\n        \"rimraf\": \"dist/esm/bin.mjs\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/rimraf/node_modules/brace-expansion\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz\",\n      \"integrity\": \"sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"balanced-match\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/rimraf/node_modules/glob\": {\n      \"version\": \"10.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/glob/-/glob-10.5.0.tgz\",\n      \"integrity\": \"sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==\",\n      \"deprecated\": \"Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"foreground-child\": \"^3.1.0\",\n        \"jackspeak\": \"^3.1.2\",\n        \"minimatch\": \"^9.0.4\",\n        \"minipass\": \"^7.1.2\",\n        \"package-json-from-dist\": \"^1.0.0\",\n        \"path-scurry\": \"^1.11.1\"\n      },\n      \"bin\": {\n        \"glob\": \"dist/esm/bin.mjs\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/rimraf/node_modules/lru-cache\": {\n      \"version\": \"10.4.3\",\n      \"resolved\": \"https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz\",\n      \"integrity\": \"sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/rimraf/node_modules/minimatch\": {\n      \"version\": \"9.0.9\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz\",\n      \"integrity\": \"sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"brace-expansion\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=16 || 14 >=14.17\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/rimraf/node_modules/path-scurry\": {\n      \"version\": \"1.11.1\",\n      \"resolved\": \"https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz\",\n      \"integrity\": \"sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"lru-cache\": \"^10.2.0\",\n        \"minipass\": \"^5.0.0 || ^6.0.2 || ^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16 || 14 >=14.18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/rolldown\": {\n      \"version\": \"1.0.0-rc.9\",\n      \"resolved\": \"https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz\",\n      \"integrity\": \"sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@oxc-project/types\": \"=0.115.0\",\n        \"@rolldown/pluginutils\": \"1.0.0-rc.9\"\n      },\n      \"bin\": {\n        \"rolldown\": \"bin/cli.mjs\"\n      },\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      },\n      \"optionalDependencies\": {\n        \"@rolldown/binding-android-arm64\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-darwin-arm64\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-darwin-x64\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-freebsd-x64\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-linux-arm-gnueabihf\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-linux-arm64-gnu\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-linux-arm64-musl\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-linux-ppc64-gnu\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-linux-s390x-gnu\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-linux-x64-gnu\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-linux-x64-musl\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-openharmony-arm64\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-wasm32-wasi\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-win32-arm64-msvc\": \"1.0.0-rc.9\",\n        \"@rolldown/binding-win32-x64-msvc\": \"1.0.0-rc.9\"\n      }\n    },\n    \"node_modules/router\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/router/-/router-2.2.0.tgz\",\n      \"integrity\": \"sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"debug\": \"^4.4.0\",\n        \"depd\": \"^2.0.0\",\n        \"is-promise\": \"^4.0.0\",\n        \"parseurl\": \"^1.3.3\",\n        \"path-to-regexp\": \"^8.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\"\n      }\n    },\n    \"node_modules/safe-buffer\": {\n      \"version\": \"5.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz\",\n      \"integrity\": \"sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/feross\"\n        },\n        {\n          \"type\": \"patreon\",\n          \"url\": \"https://www.patreon.com/feross\"\n        },\n        {\n          \"type\": \"consulting\",\n          \"url\": \"https://feross.org/support\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/safe-stable-stringify\": {\n      \"version\": \"2.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz\",\n      \"integrity\": \"sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/safer-buffer\": {\n      \"version\": \"2.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz\",\n      \"integrity\": \"sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/semver\": {\n      \"version\": \"7.7.4\",\n      \"resolved\": \"https://registry.npmjs.org/semver/-/semver-7.7.4.tgz\",\n      \"integrity\": \"sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==\",\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"semver\": \"bin/semver.js\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/send\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/send/-/send-1.2.1.tgz\",\n      \"integrity\": \"sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"debug\": \"^4.4.3\",\n        \"encodeurl\": \"^2.0.0\",\n        \"escape-html\": \"^1.0.3\",\n        \"etag\": \"^1.8.1\",\n        \"fresh\": \"^2.0.0\",\n        \"http-errors\": \"^2.0.1\",\n        \"mime-types\": \"^3.0.2\",\n        \"ms\": \"^2.1.3\",\n        \"on-finished\": \"^2.4.1\",\n        \"range-parser\": \"^1.2.1\",\n        \"statuses\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/serve-static\": {\n      \"version\": \"2.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz\",\n      \"integrity\": \"sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"encodeurl\": \"^2.0.0\",\n        \"escape-html\": \"^1.0.3\",\n        \"parseurl\": \"^1.3.3\",\n        \"send\": \"^1.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 18\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/express\"\n      }\n    },\n    \"node_modules/setimmediate\": {\n      \"version\": \"1.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz\",\n      \"integrity\": \"sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/setprototypeof\": {\n      \"version\": \"1.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz\",\n      \"integrity\": \"sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/sharp\": {\n      \"version\": \"0.34.5\",\n      \"resolved\": \"https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz\",\n      \"integrity\": \"sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==\",\n      \"hasInstallScript\": true,\n      \"license\": \"Apache-2.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@img/colour\": \"^1.0.0\",\n        \"detect-libc\": \"^2.1.2\",\n        \"semver\": \"^7.7.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.17.0 || ^20.3.0 || >=21.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/libvips\"\n      },\n      \"optionalDependencies\": {\n        \"@img/sharp-darwin-arm64\": \"0.34.5\",\n        \"@img/sharp-darwin-x64\": \"0.34.5\",\n        \"@img/sharp-libvips-darwin-arm64\": \"1.2.4\",\n        \"@img/sharp-libvips-darwin-x64\": \"1.2.4\",\n        \"@img/sharp-libvips-linux-arm\": \"1.2.4\",\n        \"@img/sharp-libvips-linux-arm64\": \"1.2.4\",\n        \"@img/sharp-libvips-linux-ppc64\": \"1.2.4\",\n        \"@img/sharp-libvips-linux-riscv64\": \"1.2.4\",\n        \"@img/sharp-libvips-linux-s390x\": \"1.2.4\",\n        \"@img/sharp-libvips-linux-x64\": \"1.2.4\",\n        \"@img/sharp-libvips-linuxmusl-arm64\": \"1.2.4\",\n        \"@img/sharp-libvips-linuxmusl-x64\": \"1.2.4\",\n        \"@img/sharp-linux-arm\": \"0.34.5\",\n        \"@img/sharp-linux-arm64\": \"0.34.5\",\n        \"@img/sharp-linux-ppc64\": \"0.34.5\",\n        \"@img/sharp-linux-riscv64\": \"0.34.5\",\n        \"@img/sharp-linux-s390x\": \"0.34.5\",\n        \"@img/sharp-linux-x64\": \"0.34.5\",\n        \"@img/sharp-linuxmusl-arm64\": \"0.34.5\",\n        \"@img/sharp-linuxmusl-x64\": \"0.34.5\",\n        \"@img/sharp-wasm32\": \"0.34.5\",\n        \"@img/sharp-win32-arm64\": \"0.34.5\",\n        \"@img/sharp-win32-ia32\": \"0.34.5\",\n        \"@img/sharp-win32-x64\": \"0.34.5\"\n      }\n    },\n    \"node_modules/shebang-command\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz\",\n      \"integrity\": \"sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"shebang-regex\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/shebang-regex\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz\",\n      \"integrity\": \"sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==\",\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/side-channel\": {\n      \"version\": \"1.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz\",\n      \"integrity\": \"sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"object-inspect\": \"^1.13.3\",\n        \"side-channel-list\": \"^1.0.0\",\n        \"side-channel-map\": \"^1.0.1\",\n        \"side-channel-weakmap\": \"^1.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/side-channel-list\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz\",\n      \"integrity\": \"sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"es-errors\": \"^1.3.0\",\n        \"object-inspect\": \"^1.13.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/side-channel-map\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz\",\n      \"integrity\": \"sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"es-errors\": \"^1.3.0\",\n        \"get-intrinsic\": \"^1.2.5\",\n        \"object-inspect\": \"^1.13.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/side-channel-weakmap\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz\",\n      \"integrity\": \"sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"call-bound\": \"^1.0.2\",\n        \"es-errors\": \"^1.3.0\",\n        \"get-intrinsic\": \"^1.2.5\",\n        \"object-inspect\": \"^1.13.3\",\n        \"side-channel-map\": \"^1.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/siginfo\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz\",\n      \"integrity\": \"sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/signal-exit\": {\n      \"version\": \"3.0.7\",\n      \"resolved\": \"https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz\",\n      \"integrity\": \"sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/simple-git\": {\n      \"version\": \"3.33.0\",\n      \"resolved\": \"https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz\",\n      \"integrity\": \"sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@kwsites/file-exists\": \"^1.1.1\",\n        \"@kwsites/promise-deferred\": \"^1.1.1\",\n        \"debug\": \"^4.4.0\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/steveukx/git-js?sponsor=1\"\n      }\n    },\n    \"node_modules/sisteransi\": {\n      \"version\": \"1.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz\",\n      \"integrity\": \"sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/sleep-promise\": {\n      \"version\": \"9.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz\",\n      \"integrity\": \"sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/slice-ansi\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz\",\n      \"integrity\": \"sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-styles\": \"^6.2.3\",\n        \"is-fullwidth-code-point\": \"^5.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/slice-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/slice-ansi/node_modules/ansi-styles\": {\n      \"version\": \"6.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz\",\n      \"integrity\": \"sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/smart-buffer\": {\n      \"version\": \"4.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz\",\n      \"integrity\": \"sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 6.0.0\",\n        \"npm\": \">= 3.0.0\"\n      }\n    },\n    \"node_modules/socks\": {\n      \"version\": \"2.8.7\",\n      \"resolved\": \"https://registry.npmjs.org/socks/-/socks-2.8.7.tgz\",\n      \"integrity\": \"sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ip-address\": \"^10.0.1\",\n        \"smart-buffer\": \"^4.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 10.0.0\",\n        \"npm\": \">= 3.0.0\"\n      }\n    },\n    \"node_modules/socks-proxy-agent\": {\n      \"version\": \"8.0.5\",\n      \"resolved\": \"https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz\",\n      \"integrity\": \"sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"agent-base\": \"^7.1.2\",\n        \"debug\": \"^4.3.4\",\n        \"socks\": \"^2.8.3\"\n      },\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/socks-proxy-agent/node_modules/agent-base\": {\n      \"version\": \"7.1.4\",\n      \"resolved\": \"https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz\",\n      \"integrity\": \"sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 14\"\n      }\n    },\n    \"node_modules/sonic-boom\": {\n      \"version\": \"4.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz\",\n      \"integrity\": \"sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"atomic-sleep\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/source-map\": {\n      \"version\": \"0.6.1\",\n      \"resolved\": \"https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz\",\n      \"integrity\": \"sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==\",\n      \"license\": \"BSD-3-Clause\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/source-map-js\": {\n      \"version\": \"1.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz\",\n      \"integrity\": \"sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/source-map-support\": {\n      \"version\": \"0.5.21\",\n      \"resolved\": \"https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz\",\n      \"integrity\": \"sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"buffer-from\": \"^1.0.0\",\n        \"source-map\": \"^0.6.0\"\n      }\n    },\n    \"node_modules/split2\": {\n      \"version\": \"4.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/split2/-/split2-4.2.0.tgz\",\n      \"integrity\": \"sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10.x\"\n      }\n    },\n    \"node_modules/sqlite-vec\": {\n      \"version\": \"0.1.7-alpha.2\",\n      \"resolved\": \"https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.7-alpha.2.tgz\",\n      \"integrity\": \"sha512-rNgRCv+4V4Ed3yc33Qr+nNmjhtrMnnHzXfLVPeGb28Dx5mmDL3Ngw/Wk8vhCGjj76+oC6gnkmMG8y73BZWGBwQ==\",\n      \"license\": \"MIT OR Apache\",\n      \"peer\": true,\n      \"optionalDependencies\": {\n        \"sqlite-vec-darwin-arm64\": \"0.1.7-alpha.2\",\n        \"sqlite-vec-darwin-x64\": \"0.1.7-alpha.2\",\n        \"sqlite-vec-linux-arm64\": \"0.1.7-alpha.2\",\n        \"sqlite-vec-linux-x64\": \"0.1.7-alpha.2\",\n        \"sqlite-vec-windows-x64\": \"0.1.7-alpha.2\"\n      }\n    },\n    \"node_modules/sqlite-vec-darwin-arm64\": {\n      \"version\": \"0.1.7-alpha.2\",\n      \"resolved\": \"https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.7-alpha.2.tgz\",\n      \"integrity\": \"sha512-raIATOqFYkeCHhb/t3r7W7Cf2lVYdf4J3ogJ6GFc8PQEgHCPEsi+bYnm2JT84MzLfTlSTIdxr4/NKv+zF7oLPw==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT OR Apache\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/sqlite-vec-darwin-x64\": {\n      \"version\": \"0.1.7-alpha.2\",\n      \"resolved\": \"https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.7-alpha.2.tgz\",\n      \"integrity\": \"sha512-jeZEELsQjjRsVojsvU5iKxOvkaVuE+JYC8Y4Ma8U45aAERrDYmqZoHvgSG7cg1PXL3bMlumFTAmHynf1y4pOzA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT OR Apache\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/sqlite-vec-linux-arm64\": {\n      \"version\": \"0.1.7-alpha.2\",\n      \"resolved\": \"https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.7-alpha.2.tgz\",\n      \"integrity\": \"sha512-6Spj4Nfi7tG13jsUG+W7jnT0bCTWbyPImu2M8nWp20fNrd1SZ4g3CSlDAK8GBdavX7wRlbBHCZ+BDa++rbDewA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"license\": \"MIT OR Apache\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/sqlite-vec-linux-x64\": {\n      \"version\": \"0.1.7-alpha.2\",\n      \"resolved\": \"https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.7-alpha.2.tgz\",\n      \"integrity\": \"sha512-IcgrbHaDccTVhXDf8Orwdc2+hgDLAFORl6OBUhcvlmwswwBP1hqBTSEhovClG4NItwTOBNgpwOoQ7Qp3VDPWLg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT OR Apache\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/sqlite-vec-windows-x64\": {\n      \"version\": \"0.1.7-alpha.2\",\n      \"resolved\": \"https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.7-alpha.2.tgz\",\n      \"integrity\": \"sha512-TRP6hTjAcwvQ6xpCZvjP00pdlda8J38ArFy1lMYhtQWXiIBmWnhMaMbq4kaeCYwvTTddfidatRS+TJrwIKB/oQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"license\": \"MIT OR Apache\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"peer\": true\n    },\n    \"node_modules/stackback\": {\n      \"version\": \"0.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz\",\n      \"integrity\": \"sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/statuses\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz\",\n      \"integrity\": \"sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/std-env\": {\n      \"version\": \"4.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz\",\n      \"integrity\": \"sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/stdin-discarder\": {\n      \"version\": \"0.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz\",\n      \"integrity\": \"sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/stdout-update\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz\",\n      \"integrity\": \"sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-escapes\": \"^6.2.0\",\n        \"ansi-styles\": \"^6.2.1\",\n        \"string-width\": \"^7.1.0\",\n        \"strip-ansi\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16.0.0\"\n      }\n    },\n    \"node_modules/stdout-update/node_modules/ansi-styles\": {\n      \"version\": \"6.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz\",\n      \"integrity\": \"sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/stdout-update/node_modules/emoji-regex\": {\n      \"version\": \"10.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz\",\n      \"integrity\": \"sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/stdout-update/node_modules/string-width\": {\n      \"version\": \"7.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz\",\n      \"integrity\": \"sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"emoji-regex\": \"^10.3.0\",\n        \"get-east-asian-width\": \"^1.0.0\",\n        \"strip-ansi\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/steno\": {\n      \"version\": \"4.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/steno/-/steno-4.0.2.tgz\",\n      \"integrity\": \"sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/typicode\"\n      }\n    },\n    \"node_modules/string_decoder\": {\n      \"version\": \"1.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz\",\n      \"integrity\": \"sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"safe-buffer\": \"~5.1.0\"\n      }\n    },\n    \"node_modules/string_decoder/node_modules/safe-buffer\": {\n      \"version\": \"5.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz\",\n      \"integrity\": \"sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/string-width\": {\n      \"version\": \"8.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz\",\n      \"integrity\": \"sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"get-east-asian-width\": \"^1.5.0\",\n        \"strip-ansi\": \"^7.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">=20\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/string-width-cjs\": {\n      \"name\": \"string-width\",\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string-width-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string-width-cjs/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz\",\n      \"integrity\": \"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string-width-cjs/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/strip-ansi\": {\n      \"version\": \"7.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz\",\n      \"integrity\": \"sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-regex\": \"^6.2.2\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/strip-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/strip-ansi-cjs\": {\n      \"name\": \"strip-ansi\",\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/strip-ansi-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/strip-json-comments\": {\n      \"version\": \"3.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz\",\n      \"integrity\": \"sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/strnum\": {\n      \"version\": \"2.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz\",\n      \"integrity\": \"sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==\",\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/NaturalIntelligence\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/strtok3\": {\n      \"version\": \"10.3.4\",\n      \"resolved\": \"https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz\",\n      \"integrity\": \"sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@tokenizer/token\": \"^0.3.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Borewit\"\n      }\n    },\n    \"node_modules/supports-color\": {\n      \"version\": \"7.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz\",\n      \"integrity\": \"sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==\",\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"has-flag\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/supports-preserve-symlinks-flag\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz\",\n      \"integrity\": \"sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/tar\": {\n      \"version\": \"7.5.11\",\n      \"resolved\": \"https://registry.npmjs.org/tar/-/tar-7.5.11.tgz\",\n      \"integrity\": \"sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@isaacs/fs-minipass\": \"^4.0.0\",\n        \"chownr\": \"^3.0.0\",\n        \"minipass\": \"^7.1.2\",\n        \"minizlib\": \"^3.1.0\",\n        \"yallist\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/thenify\": {\n      \"version\": \"3.3.1\",\n      \"resolved\": \"https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz\",\n      \"integrity\": \"sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"any-promise\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/thenify-all\": {\n      \"version\": \"1.6.0\",\n      \"resolved\": \"https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz\",\n      \"integrity\": \"sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"thenify\": \">= 3.1.0 < 4\"\n      },\n      \"engines\": {\n        \"node\": \">=0.8\"\n      }\n    },\n    \"node_modules/thread-stream\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz\",\n      \"integrity\": \"sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"real-require\": \"^0.2.0\"\n      }\n    },\n    \"node_modules/tinybench\": {\n      \"version\": \"2.9.0\",\n      \"resolved\": \"https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz\",\n      \"integrity\": \"sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/tinyexec\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz\",\n      \"integrity\": \"sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/tinyglobby\": {\n      \"version\": \"0.2.15\",\n      \"resolved\": \"https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz\",\n      \"integrity\": \"sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fdir\": \"^6.5.0\",\n        \"picomatch\": \"^4.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/SuperchupuDev\"\n      }\n    },\n    \"node_modules/tinyrainbow\": {\n      \"version\": \"3.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz\",\n      \"integrity\": \"sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/toad-cache\": {\n      \"version\": \"3.7.0\",\n      \"resolved\": \"https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz\",\n      \"integrity\": \"sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/toidentifier\": {\n      \"version\": \"1.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz\",\n      \"integrity\": \"sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.6\"\n      }\n    },\n    \"node_modules/token-types\": {\n      \"version\": \"6.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz\",\n      \"integrity\": \"sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"@borewit/text-codec\": \"^0.2.1\",\n        \"@tokenizer/token\": \"^0.3.0\",\n        \"ieee754\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">=14.16\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/Borewit\"\n      }\n    },\n    \"node_modules/tr46\": {\n      \"version\": \"0.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz\",\n      \"integrity\": \"sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/ts-algebra\": {\n      \"version\": \"2.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz\",\n      \"integrity\": \"sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/ts-api-utils\": {\n      \"version\": \"2.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz\",\n      \"integrity\": \"sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.12\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4\"\n      }\n    },\n    \"node_modules/tslib\": {\n      \"version\": \"2.8.1\",\n      \"resolved\": \"https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz\",\n      \"integrity\": \"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==\",\n      \"license\": \"0BSD\"\n    },\n    \"node_modules/tslog\": {\n      \"version\": \"4.10.2\",\n      \"resolved\": \"https://registry.npmjs.org/tslog/-/tslog-4.10.2.tgz\",\n      \"integrity\": \"sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=16\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/fullstack-build/tslog?sponsor=1\"\n      }\n    },\n    \"node_modules/tsscmp\": {\n      \"version\": \"1.0.6\",\n      \"resolved\": \"https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz\",\n      \"integrity\": \"sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=0.6.x\"\n      }\n    },\n    \"node_modules/type-check\": {\n      \"version\": \"0.4.0\",\n      \"resolved\": \"https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz\",\n      \"integrity\": \"sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prelude-ls\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/type-is\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz\",\n      \"integrity\": \"sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"content-type\": \"^1.0.5\",\n        \"media-typer\": \"^1.1.0\",\n        \"mime-types\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.6\"\n      }\n    },\n    \"node_modules/typescript\": {\n      \"version\": \"5.9.3\",\n      \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz\",\n      \"integrity\": \"sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==\",\n      \"devOptional\": true,\n      \"license\": \"Apache-2.0\",\n      \"bin\": {\n        \"tsc\": \"bin/tsc\",\n        \"tsserver\": \"bin/tsserver\"\n      },\n      \"engines\": {\n        \"node\": \">=14.17\"\n      }\n    },\n    \"node_modules/typescript-eslint\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz\",\n      \"integrity\": \"sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/eslint-plugin\": \"8.57.0\",\n        \"@typescript-eslint/parser\": \"8.57.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.57.0\",\n        \"@typescript-eslint/utils\": \"8.57.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz\",\n      \"integrity\": \"sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/tsconfig-utils\": \"^8.57.0\",\n        \"@typescript-eslint/types\": \"^8.57.0\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz\",\n      \"integrity\": \"sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz\",\n      \"integrity\": \"sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/@typescript-eslint/types\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz\",\n      \"integrity\": \"sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz\",\n      \"integrity\": \"sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/project-service\": \"8.57.0\",\n        \"@typescript-eslint/tsconfig-utils\": \"8.57.0\",\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/visitor-keys\": \"8.57.0\",\n        \"debug\": \"^4.4.3\",\n        \"minimatch\": \"^10.2.2\",\n        \"semver\": \"^7.7.3\",\n        \"tinyglobby\": \"^0.2.15\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/@typescript-eslint/utils\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz\",\n      \"integrity\": \"sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.9.1\",\n        \"@typescript-eslint/scope-manager\": \"8.57.0\",\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"@typescript-eslint/typescript-estree\": \"8.57.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys\": {\n      \"version\": \"8.57.0\",\n      \"resolved\": \"https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz\",\n      \"integrity\": \"sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.57.0\",\n        \"eslint-visitor-keys\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz\",\n      \"integrity\": \"sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/brace-expansion\": {\n      \"version\": \"5.0.4\",\n      \"resolved\": \"https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz\",\n      \"integrity\": \"sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/eslint-visitor-keys\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz\",\n      \"integrity\": \"sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/typescript-eslint/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"resolved\": \"https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz\",\n      \"integrity\": \"sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/uc.micro\": {\n      \"version\": \"2.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz\",\n      \"integrity\": \"sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/uhyphen\": {\n      \"version\": \"0.2.0\",\n      \"resolved\": \"https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz\",\n      \"integrity\": \"sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/uint8array-extras\": {\n      \"version\": \"1.5.0\",\n      \"resolved\": \"https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz\",\n      \"integrity\": \"sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/undici\": {\n      \"version\": \"7.23.0\",\n      \"resolved\": \"https://registry.npmjs.org/undici/-/undici-7.23.0.tgz\",\n      \"integrity\": \"sha512-HVMxHKZKi+eL2mrUZDzDkKW3XvCjynhbtpSq20xQp4ePDFeSFuAfnvM0GIwZIv8fiKHjXFQ5WjxhCt15KRNj+g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=20.18.1\"\n      }\n    },\n    \"node_modules/undici-types\": {\n      \"version\": \"6.21.0\",\n      \"resolved\": \"https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz\",\n      \"integrity\": \"sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==\",\n      \"license\": \"MIT\"\n    },\n    \"node_modules/universal-github-app-jwt\": {\n      \"version\": \"2.2.2\",\n      \"resolved\": \"https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz\",\n      \"integrity\": \"sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/universal-user-agent\": {\n      \"version\": \"7.0.3\",\n      \"resolved\": \"https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz\",\n      \"integrity\": \"sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/universalify\": {\n      \"version\": \"2.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz\",\n      \"integrity\": \"sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 10.0.0\"\n      }\n    },\n    \"node_modules/unpipe\": {\n      \"version\": \"1.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz\",\n      \"integrity\": \"sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/uri-js\": {\n      \"version\": \"4.4.1\",\n      \"resolved\": \"https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz\",\n      \"integrity\": \"sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"punycode\": \"^2.1.0\"\n      }\n    },\n    \"node_modules/url-join\": {\n      \"version\": \"4.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz\",\n      \"integrity\": \"sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/util-deprecate\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz\",\n      \"integrity\": \"sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/validate-npm-package-name\": {\n      \"version\": \"7.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz\",\n      \"integrity\": \"sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \"^20.17.0 || >=22.9.0\"\n      }\n    },\n    \"node_modules/vary\": {\n      \"version\": \"1.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/vary/-/vary-1.1.2.tgz\",\n      \"integrity\": \"sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 0.8\"\n      }\n    },\n    \"node_modules/vite\": {\n      \"version\": \"8.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/vite/-/vite-8.0.0.tgz\",\n      \"integrity\": \"sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@oxc-project/runtime\": \"0.115.0\",\n        \"lightningcss\": \"^1.32.0\",\n        \"picomatch\": \"^4.0.3\",\n        \"postcss\": \"^8.5.8\",\n        \"rolldown\": \"1.0.0-rc.9\",\n        \"tinyglobby\": \"^0.2.15\"\n      },\n      \"bin\": {\n        \"vite\": \"bin/vite.js\"\n      },\n      \"engines\": {\n        \"node\": \"^20.19.0 || >=22.12.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/vitejs/vite?sponsor=1\"\n      },\n      \"optionalDependencies\": {\n        \"fsevents\": \"~2.3.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/node\": \"^20.19.0 || >=22.12.0\",\n        \"@vitejs/devtools\": \"^0.0.0-alpha.31\",\n        \"esbuild\": \"^0.27.0\",\n        \"jiti\": \">=1.21.0\",\n        \"less\": \"^4.0.0\",\n        \"sass\": \"^1.70.0\",\n        \"sass-embedded\": \"^1.70.0\",\n        \"stylus\": \">=0.54.8\",\n        \"sugarss\": \"^5.0.0\",\n        \"terser\": \"^5.16.0\",\n        \"tsx\": \"^4.8.1\",\n        \"yaml\": \"^2.4.2\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/node\": {\n          \"optional\": true\n        },\n        \"@vitejs/devtools\": {\n          \"optional\": true\n        },\n        \"esbuild\": {\n          \"optional\": true\n        },\n        \"jiti\": {\n          \"optional\": true\n        },\n        \"less\": {\n          \"optional\": true\n        },\n        \"sass\": {\n          \"optional\": true\n        },\n        \"sass-embedded\": {\n          \"optional\": true\n        },\n        \"stylus\": {\n          \"optional\": true\n        },\n        \"sugarss\": {\n          \"optional\": true\n        },\n        \"terser\": {\n          \"optional\": true\n        },\n        \"tsx\": {\n          \"optional\": true\n        },\n        \"yaml\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/vitest\": {\n      \"version\": \"4.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz\",\n      \"integrity\": \"sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/expect\": \"4.1.0\",\n        \"@vitest/mocker\": \"4.1.0\",\n        \"@vitest/pretty-format\": \"4.1.0\",\n        \"@vitest/runner\": \"4.1.0\",\n        \"@vitest/snapshot\": \"4.1.0\",\n        \"@vitest/spy\": \"4.1.0\",\n        \"@vitest/utils\": \"4.1.0\",\n        \"es-module-lexer\": \"^2.0.0\",\n        \"expect-type\": \"^1.3.0\",\n        \"magic-string\": \"^0.30.21\",\n        \"obug\": \"^2.1.1\",\n        \"pathe\": \"^2.0.3\",\n        \"picomatch\": \"^4.0.3\",\n        \"std-env\": \"^4.0.0-rc.1\",\n        \"tinybench\": \"^2.9.0\",\n        \"tinyexec\": \"^1.0.2\",\n        \"tinyglobby\": \"^0.2.15\",\n        \"tinyrainbow\": \"^3.0.3\",\n        \"vite\": \"^6.0.0 || ^7.0.0 || ^8.0.0-0\",\n        \"why-is-node-running\": \"^2.3.0\"\n      },\n      \"bin\": {\n        \"vitest\": \"vitest.mjs\"\n      },\n      \"engines\": {\n        \"node\": \"^20.0.0 || ^22.0.0 || >=24.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      },\n      \"peerDependencies\": {\n        \"@edge-runtime/vm\": \"*\",\n        \"@opentelemetry/api\": \"^1.9.0\",\n        \"@types/node\": \"^20.0.0 || ^22.0.0 || >=24.0.0\",\n        \"@vitest/browser-playwright\": \"4.1.0\",\n        \"@vitest/browser-preview\": \"4.1.0\",\n        \"@vitest/browser-webdriverio\": \"4.1.0\",\n        \"@vitest/ui\": \"4.1.0\",\n        \"happy-dom\": \"*\",\n        \"jsdom\": \"*\",\n        \"vite\": \"^6.0.0 || ^7.0.0 || ^8.0.0-0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@edge-runtime/vm\": {\n          \"optional\": true\n        },\n        \"@opentelemetry/api\": {\n          \"optional\": true\n        },\n        \"@types/node\": {\n          \"optional\": true\n        },\n        \"@vitest/browser-playwright\": {\n          \"optional\": true\n        },\n        \"@vitest/browser-preview\": {\n          \"optional\": true\n        },\n        \"@vitest/browser-webdriverio\": {\n          \"optional\": true\n        },\n        \"@vitest/ui\": {\n          \"optional\": true\n        },\n        \"happy-dom\": {\n          \"optional\": true\n        },\n        \"jsdom\": {\n          \"optional\": true\n        },\n        \"vite\": {\n          \"optional\": false\n        }\n      }\n    },\n    \"node_modules/web-streams-polyfill\": {\n      \"version\": \"3.3.3\",\n      \"resolved\": \"https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz\",\n      \"integrity\": \"sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/webidl-conversions\": {\n      \"version\": \"3.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz\",\n      \"integrity\": \"sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==\",\n      \"license\": \"BSD-2-Clause\",\n      \"peer\": true\n    },\n    \"node_modules/whatwg-url\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz\",\n      \"integrity\": \"sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"tr46\": \"~0.0.3\",\n        \"webidl-conversions\": \"^3.0.0\"\n      }\n    },\n    \"node_modules/which\": {\n      \"version\": \"2.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/which/-/which-2.0.2.tgz\",\n      \"integrity\": \"sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==\",\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"isexe\": \"^2.0.0\"\n      },\n      \"bin\": {\n        \"node-which\": \"bin/node-which\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/why-is-node-running\": {\n      \"version\": \"2.3.0\",\n      \"resolved\": \"https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz\",\n      \"integrity\": \"sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"siginfo\": \"^2.0.0\",\n        \"stackback\": \"0.0.2\"\n      },\n      \"bin\": {\n        \"why-is-node-running\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/win-guid\": {\n      \"version\": \"0.2.1\",\n      \"resolved\": \"https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz\",\n      \"integrity\": \"sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==\",\n      \"license\": \"MIT\",\n      \"peer\": true\n    },\n    \"node_modules/word-wrap\": {\n      \"version\": \"1.2.5\",\n      \"resolved\": \"https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz\",\n      \"integrity\": \"sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/wrap-ansi\": {\n      \"version\": \"7.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz\",\n      \"integrity\": \"sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-styles\": \"^4.0.0\",\n        \"string-width\": \"^4.1.0\",\n        \"strip-ansi\": \"^6.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs\": {\n      \"name\": \"wrap-ansi\",\n      \"version\": \"7.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz\",\n      \"integrity\": \"sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-styles\": \"^4.0.0\",\n        \"string-width\": \"^4.1.0\",\n        \"strip-ansi\": \"^6.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz\",\n      \"integrity\": \"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/string-width\": {\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz\",\n      \"integrity\": \"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi/node_modules/string-width\": {\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrappy\": {\n      \"version\": \"1.0.2\",\n      \"resolved\": \"https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz\",\n      \"integrity\": \"sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==\",\n      \"license\": \"ISC\",\n      \"peer\": true\n    },\n    \"node_modules/ws\": {\n      \"version\": \"8.19.0\",\n      \"resolved\": \"https://registry.npmjs.org/ws/-/ws-8.19.0.tgz\",\n      \"integrity\": \"sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=10.0.0\"\n      },\n      \"peerDependencies\": {\n        \"bufferutil\": \"^4.0.1\",\n        \"utf-8-validate\": \">=5.0.2\"\n      },\n      \"peerDependenciesMeta\": {\n        \"bufferutil\": {\n          \"optional\": true\n        },\n        \"utf-8-validate\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/y18n\": {\n      \"version\": \"5.0.8\",\n      \"resolved\": \"https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz\",\n      \"integrity\": \"sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/yallist\": {\n      \"version\": \"5.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz\",\n      \"integrity\": \"sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==\",\n      \"license\": \"BlueOak-1.0.0\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/yaml\": {\n      \"version\": \"2.8.2\",\n      \"resolved\": \"https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz\",\n      \"integrity\": \"sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"bin\": {\n        \"yaml\": \"bin.mjs\"\n      },\n      \"engines\": {\n        \"node\": \">= 14.6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/eemeli\"\n      }\n    },\n    \"node_modules/yargs\": {\n      \"version\": \"17.7.2\",\n      \"resolved\": \"https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz\",\n      \"integrity\": \"sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"cliui\": \"^8.0.1\",\n        \"escalade\": \"^3.1.1\",\n        \"get-caller-file\": \"^2.0.5\",\n        \"require-directory\": \"^2.1.1\",\n        \"string-width\": \"^4.2.3\",\n        \"y18n\": \"^5.0.5\",\n        \"yargs-parser\": \"^21.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/yargs-parser\": {\n      \"version\": \"21.1.1\",\n      \"resolved\": \"https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz\",\n      \"integrity\": \"sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/yargs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz\",\n      \"integrity\": \"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/yargs/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"resolved\": \"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz\",\n      \"integrity\": \"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/yargs/node_modules/string-width\": {\n      \"version\": \"4.2.3\",\n      \"resolved\": \"https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz\",\n      \"integrity\": \"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/yargs/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"resolved\": \"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz\",\n      \"integrity\": \"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/yauzl\": {\n      \"version\": \"2.10.0\",\n      \"resolved\": \"https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz\",\n      \"integrity\": \"sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"dependencies\": {\n        \"buffer-crc32\": \"~0.2.3\",\n        \"fd-slicer\": \"~1.1.0\"\n      }\n    },\n    \"node_modules/yocto-queue\": {\n      \"version\": \"0.1.0\",\n      \"resolved\": \"https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz\",\n      \"integrity\": \"sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/yoctocolors\": {\n      \"version\": \"2.1.2\",\n      \"resolved\": \"https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz\",\n      \"integrity\": \"sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/zod\": {\n      \"version\": \"4.3.6\",\n      \"resolved\": \"https://registry.npmjs.org/zod/-/zod-4.3.6.tgz\",\n      \"integrity\": \"sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==\",\n      \"license\": \"MIT\",\n      \"peer\": true,\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/colinhacks\"\n      }\n    },\n    \"node_modules/zod-to-json-schema\": {\n      \"version\": \"3.25.1\",\n      \"resolved\": \"https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz\",\n      \"integrity\": \"sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==\",\n      \"license\": \"ISC\",\n      \"peer\": true,\n      \"peerDependencies\": {\n        \"zod\": \"^3.25 || ^4\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "integrations/openclaw/package.json",
    "content": "{\n  \"name\": \"@memorilabs/openclaw-memori\",\n  \"version\": \"0.0.4\",\n  \"description\": \"Official MemoriLabs.ai long-term memory plugin for OpenClaw\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"openclaw.plugin.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"build:dev\": \"tsc --watch\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format\": \"prettier --write .\",\n    \"format:check\": \"prettier --check .\",\n    \"check\": \"npm run format:check && npm run lint && npm run typecheck\",\n    \"test\": \"vitest run\",\n    \"test:coverage\": \"vitest run --coverage\"\n  },\n  \"peerDependencies\": {\n    \"openclaw\": \"^2026.3.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.0.0\",\n    \"@types/node\": \"^20.0.0\",\n    \"@vitest/coverage-v8\": \"^4.0.18\",\n    \"eslint\": \"^9.0.0\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-plugin-tsdoc\": \"^0.5.1\",\n    \"prettier\": \"^3.0.0\",\n    \"typescript\": \"^5.0.0\",\n    \"typescript-eslint\": \"^8.0.0\",\n    \"vitest\": \"^4.0.18\"\n  },\n  \"keywords\": [\n    \"openclaw\",\n    \"plugin\",\n    \"memory\",\n    \"memori\",\n    \"long-term-memory\"\n  ],\n  \"author\": \"Memori Labs\",\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/MemoriLabs/Memori.git\",\n    \"directory\": \"integrations/openclaw\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/MemoriLabs/Memori/issues\"\n  },\n  \"homepage\": \"https://github.com/MemoriLabs/Memori\",\n  \"openclaw\": {\n    \"extensions\": [\n      \"./dist/index.js\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=22.0.0\"\n  },\n  \"dependencies\": {\n    \"@memorilabs/memori\": \"^0.0.5\"\n  },\n  \"overrides\": {\n    \"@hono/node-server\": \"^1.19.10\"\n  }\n}\n"
  },
  {
    "path": "integrations/openclaw/src/constants.ts",
    "content": "export const PLUGIN_CONFIG = {\n  ID: 'openclaw-memori',\n  NAME: 'Memori System',\n  LOG_PREFIX: '[Memori]',\n} as const;\n\nexport const RECALL_CONFIG = {\n  MIN_PROMPT_LENGTH: 2,\n} as const;\n\nexport const AUGMENTATION_CONFIG = {\n  MAX_CONTEXT_MESSAGES: 5,\n} as const;\n"
  },
  {
    "path": "integrations/openclaw/src/handlers/augmentation.ts",
    "content": "import { IntegrationRequest, IntegrationMetadata } from '@memorilabs/memori/integrations';\nimport { OpenClawEvent, OpenClawContext, MemoriPluginConfig } from '../types.js';\nimport { extractContext, MemoriLogger, initializeMemoriClient } from '../utils/index.js';\nimport { cleanText, isSystemMessage } from '../sanitizer.js';\nimport { AUGMENTATION_CONFIG } from '../constants.js';\nimport { SDK_VERSION } from '../version.js';\n\nfunction extractLLMMetadata(event: OpenClawEvent): IntegrationMetadata {\n  const messages = event.messages || [];\n  const lastAssistant = messages.findLast((m) => m.role === 'assistant');\n\n  return {\n    provider: (lastAssistant?.provider as string) || null,\n    model: (lastAssistant?.model as string) || null,\n    sdkVersion: null,\n    integrationSdkVersion: SDK_VERSION,\n    platform: 'openclaw',\n  };\n}\n\nexport async function handleAugmentation(\n  event: OpenClawEvent,\n  ctx: OpenClawContext,\n  config: MemoriPluginConfig,\n  logger: MemoriLogger\n): Promise<void> {\n  logger.section('AUGMENTATION HOOK START');\n\n  if (!event.success || !event.messages || event.messages.length < 2) {\n    logger.info('No messages or unsuccessful event. Skipping augmentation.');\n    logger.endSection('AUGMENTATION HOOK END');\n    return;\n  }\n\n  try {\n    const recentMessages = event.messages.slice(-AUGMENTATION_CONFIG.MAX_CONTEXT_MESSAGES);\n\n    let lastUserMsg: { role: string; content: string } | undefined;\n    let lastAiMsg: { role: string; content: string } | undefined;\n\n    for (let i = recentMessages.length - 1; i >= 0; i--) {\n      const msg = recentMessages[i];\n      const role = msg.role;\n\n      if (role !== 'user' && role !== 'assistant') continue;\n\n      const cleanedContent = cleanText(msg.content);\n      if (!cleanedContent) continue;\n\n      let finalContent = cleanedContent;\n      if (role === 'assistant') {\n        finalContent = finalContent.replace(/^\\[\\[.*?\\]\\]\\s*/, '');\n      }\n\n      if (role === 'assistant' && !lastAiMsg) {\n        lastAiMsg = { role, content: finalContent };\n      }\n\n      if (role === 'user' && !lastUserMsg) {\n        lastUserMsg = { role, content: finalContent };\n      }\n\n      if (lastUserMsg && lastAiMsg) break;\n    }\n\n    if (!lastUserMsg || !lastAiMsg) {\n      logger.info('Missing user or assistant message. Skipping.');\n      logger.endSection('AUGMENTATION HOOK END');\n      return;\n    }\n\n    if (isSystemMessage(lastUserMsg.content)) {\n      logger.info('User message is a system message. Skipping augmentation.');\n      logger.endSection('AUGMENTATION HOOK END');\n      return;\n    }\n\n    if (lastAiMsg.content === 'NO_REPLY' || lastAiMsg.content === 'SILENT_REPLY') {\n      logger.info('Assistant used tool-based messaging. Using synthetic response.');\n      lastAiMsg = {\n        role: 'assistant',\n        content: \"Okay, I'll remember that for you.\",\n      };\n    }\n\n    const context = extractContext(event, ctx, config.entityId);\n    const memoriClient = initializeMemoriClient(config.apiKey, context);\n\n    const payload: IntegrationRequest = {\n      userMessage: lastUserMsg.content,\n      agentResponse: lastAiMsg.content,\n      metadata: extractLLMMetadata(event),\n    };\n\n    await memoriClient.augmentation(payload);\n    logger.info('Augmentation successful!');\n  } catch (err) {\n    logger.error(`Augmentation failed: ${err instanceof Error ? err.message : String(err)}`);\n  } finally {\n    logger.endSection('AUGMENTATION HOOK END');\n  }\n}\n"
  },
  {
    "path": "integrations/openclaw/src/handlers/recall.ts",
    "content": "import { cleanText, isSystemMessage } from '../sanitizer.js';\nimport { OpenClawEvent, OpenClawContext, MemoriPluginConfig } from '../types.js';\nimport { RECALL_CONFIG } from '../constants.js';\nimport { extractContext, MemoriLogger, initializeMemoriClient } from '../utils/index.js';\n\nexport async function handleRecall(\n  event: OpenClawEvent,\n  ctx: OpenClawContext,\n  config: MemoriPluginConfig,\n  logger: MemoriLogger\n): Promise<{ prependContext: string } | undefined> {\n  logger.section('RECALL HOOK START');\n\n  try {\n    const context = extractContext(event, ctx, config.entityId);\n    const promptText = cleanText(event.prompt);\n\n    if (\n      !promptText ||\n      promptText.length < RECALL_CONFIG.MIN_PROMPT_LENGTH ||\n      isSystemMessage(promptText)\n    ) {\n      logger.info('Prompt too short or is a system message. Aborting recall.');\n      return undefined;\n    }\n\n    const memoriClient = initializeMemoriClient(config.apiKey, context);\n\n    logger.info('Executing SDK Recall...');\n\n    const recallText = await memoriClient.recall(promptText);\n    const hookReturn = recallText ? { prependContext: recallText } : undefined;\n\n    if (hookReturn) {\n      logger.info('Successfully injected memory context.');\n    } else {\n      logger.info('No relevant memories found.');\n    }\n\n    return hookReturn;\n  } catch (err) {\n    logger.error(`Recall failed: ${err instanceof Error ? err.message : String(err)}`);\n    return undefined;\n  } finally {\n    logger.endSection('RECALL HOOK END');\n  }\n}\n"
  },
  {
    "path": "integrations/openclaw/src/index.ts",
    "content": "import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';\nimport { handleRecall } from './handlers/recall.js';\nimport { handleAugmentation } from './handlers/augmentation.js';\nimport { OpenClawEvent, OpenClawContext, MemoriPluginConfig } from './types.js';\nimport { PLUGIN_CONFIG } from './constants.js';\nimport { MemoriLogger } from './utils/index.js';\n\nconst memoriPlugin = {\n  id: PLUGIN_CONFIG.ID,\n  name: PLUGIN_CONFIG.NAME,\n  description: 'Hosted memory backend',\n\n  register(api: OpenClawPluginApi) {\n    const rawConfig = api.pluginConfig;\n\n    const config: MemoriPluginConfig = {\n      apiKey: rawConfig?.apiKey as string,\n      entityId: rawConfig?.entityId as string,\n    };\n\n    if (!config.apiKey || !config.entityId) {\n      api.logger.warn(\n        `${PLUGIN_CONFIG.LOG_PREFIX} Missing apiKey or entityId in config. Plugin disabled.`\n      );\n      return;\n    }\n\n    const logger = new MemoriLogger(api);\n\n    logger.info(`\\n=== ${PLUGIN_CONFIG.LOG_PREFIX} INITIALIZING PLUGIN ===`);\n    logger.info(`${PLUGIN_CONFIG.LOG_PREFIX} Tracking Entity ID: ${config.entityId}`);\n\n    api.on('before_prompt_build', (event: unknown, ctx: unknown) =>\n      handleRecall(event as OpenClawEvent, ctx as OpenClawContext, config, logger)\n    );\n\n    api.on('agent_end', (event: unknown, ctx: unknown) =>\n      handleAugmentation(event as OpenClawEvent, ctx as OpenClawContext, config, logger)\n    );\n  },\n};\n\nexport default memoriPlugin;\n"
  },
  {
    "path": "integrations/openclaw/src/sanitizer.ts",
    "content": "import { OpenClawMessageBlock } from './types.js';\n\nconst SYSTEM_MESSAGE_PATTERNS = [\n  'a new session was started',\n  '/new or /reset',\n  'session startup sequence',\n  'use persona',\n] as const;\n\nconst TIMESTAMP_PREFIX_REGEX =\n  /^\\[[A-Z][a-z]{2}\\s+\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}\\s+[A-Z]{2,4}\\]\\s*/;\n\nfunction isMessageBlockArray(value: unknown): value is OpenClawMessageBlock[] {\n  return Array.isArray(value) && value.every((item) => item !== null && typeof item === 'object');\n}\n\nexport function isSystemMessage(text: string): boolean {\n  if (!text) return true;\n  const lowerText = text.toLowerCase();\n  return SYSTEM_MESSAGE_PATTERNS.some((pattern) => lowerText.includes(pattern));\n}\n\n/**\n * Extracts the actual user message from OpenClaw's formatted content.\n *\n * OpenClaw wraps metadata in markdown code fences (triple tick).\n * The actual message is always after the LAST closing fence.\n * The message might aslo contain a timestamp prefix like: [Day YYYY-MM-DD HH:MM TZ], that will need to be removed\n */\nfunction extractRawUserMessage(content: string): string {\n  let message = content;\n\n  if (message.includes('```')) {\n    const lastFenceIndex = message.lastIndexOf('```');\n    if (lastFenceIndex !== -1) {\n      message = message.substring(lastFenceIndex + 3).trim();\n    }\n  }\n\n  const timestampMatch = message.match(TIMESTAMP_PREFIX_REGEX);\n  if (timestampMatch) {\n    message = message.substring(timestampMatch[0].length).trim();\n  }\n\n  return message;\n}\n\nfunction extractMessageText(content: unknown): string {\n  if (!content) return '';\n\n  if (typeof content === 'string') {\n    return content;\n  }\n\n  if (isMessageBlockArray(content)) {\n    return content\n      .filter((block) => (block.type === 'text' || typeof block.text === 'string') && block.text)\n      .map((block) => block.text)\n      .join('\\n\\n');\n  }\n\n  return '';\n}\n\nexport function cleanText(rawContent: unknown): string {\n  let text = extractMessageText(rawContent);\n\n  if (!text) return '';\n\n  text = extractRawUserMessage(text);\n\n  text = text.replace(/<memori_context>[\\s\\S]*?<\\/memori_context>\\s*/g, '');\n\n  return text.trim();\n}\n"
  },
  {
    "path": "integrations/openclaw/src/types.ts",
    "content": "export interface MemoriPluginConfig {\n  apiKey: string;\n  entityId: string;\n}\n\nexport interface OpenClawMessageBlock {\n  type?: string;\n  text?: string;\n  thinking?: string;\n  [key: string]: unknown;\n}\n\nexport interface OpenClawMessage {\n  role: 'user' | 'assistant' | 'system';\n  content: string | OpenClawMessageBlock[];\n  timestamp?: number;\n  [key: string]: unknown;\n}\n\nexport interface OpenClawEvent {\n  prompt?: string;\n  messages?: OpenClawMessage[];\n  completion?: string;\n  success?: boolean;\n  error?: string;\n  durationMs?: number;\n  userId?: string;\n  sessionId?: string;\n  messageProvider?: string;\n}\n\nexport interface OpenClawContext {\n  agentId?: string;\n  sessionKey?: string;\n  sessionId?: string;\n  workspaceDir?: string;\n  messageProvider?: string;\n}\n"
  },
  {
    "path": "integrations/openclaw/src/utils/context.ts",
    "content": "import { OpenClawEvent, OpenClawContext } from '../types.js';\n\n/**\n * Extracted context information from OpenClaw events\n */\nexport interface ExtractedContext {\n  entityId: string;\n  sessionId: string;\n  provider: string;\n}\n\n/**\n * Extracts and normalizes context information from OpenClaw event and context objects.\n * Throws an error if required context fields cannot be resolved.\n *\n * @param event - OpenClaw event object\n * @param ctx - OpenClaw context object\n * @param configuredEntityId - Hardcoded entity ID from plugin config\n * @returns Normalized context with entityId, sessionId, and provider\n * @throws Error If entityId, sessionId, or provider cannot be determined\n */\nexport function extractContext(\n  event: OpenClawEvent,\n  ctx: OpenClawContext,\n  configuredEntityId: string\n): ExtractedContext {\n  const sessionId = ctx.sessionKey || event.sessionId;\n  const provider = ctx.messageProvider || event.messageProvider;\n\n  if (!sessionId) {\n    throw new Error('Failed to extract context: Missing sessionId in OpenClaw context and event.');\n  }\n\n  if (!provider) {\n    throw new Error(\n      'Failed to extract context: Missing message provider in OpenClaw context and event.'\n    );\n  }\n\n  return {\n    entityId: configuredEntityId,\n    sessionId,\n    provider,\n  };\n}\n"
  },
  {
    "path": "integrations/openclaw/src/utils/index.ts",
    "content": "export { extractContext, type ExtractedContext } from './context.js';\nexport { MemoriLogger } from './logger.js';\nexport { initializeMemoriClient } from './memori-client.js';\n"
  },
  {
    "path": "integrations/openclaw/src/utils/logger.ts",
    "content": "import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';\nimport { PLUGIN_CONFIG } from '../constants.js';\n\nexport class MemoriLogger {\n  constructor(private api: OpenClawPluginApi) {}\n\n  private prefix(msg: string): string {\n    return `${PLUGIN_CONFIG.LOG_PREFIX} ${msg}`;\n  }\n\n  info(message: string): void {\n    this.api.logger.info(this.prefix(message));\n  }\n\n  warn(message: string): void {\n    this.api.logger.warn(this.prefix(message));\n  }\n\n  error(message: string): void {\n    this.api.logger.error(this.prefix(message));\n  }\n\n  section(title: string): void {\n    this.api.logger.info(`\\n=== ${this.prefix(title)} ===`);\n  }\n\n  endSection(title: string): void {\n    this.api.logger.info(`=== ${this.prefix(title)} ===\\n`);\n  }\n}\n"
  },
  {
    "path": "integrations/openclaw/src/utils/memori-client.ts",
    "content": "import { Memori } from '@memorilabs/memori';\nimport { OpenClawIntegration } from '@memorilabs/memori/integrations';\nimport { ExtractedContext } from './context.js';\n\n/**\n * Initializes and configures a Memori OpenClaw integration instance\n *\n * @param apiKey - Memori API key\n * @param context - Extracted context information\n * @returns Configured OpenClawIntegration instance\n */\nexport function initializeMemoriClient(\n  apiKey: string,\n  context: ExtractedContext\n): OpenClawIntegration {\n  const memori = new Memori();\n  memori.config.apiKey = apiKey;\n\n  const openclaw = memori.integrate(OpenClawIntegration);\n  openclaw.setAttribution(context.entityId, context.provider);\n  openclaw.setSession(context.sessionId);\n\n  return openclaw;\n}\n"
  },
  {
    "path": "integrations/openclaw/src/version.ts",
    "content": "// This file is auto-generated during CI builds.\nexport const SDK_VERSION = '0.0.0';\n"
  },
  {
    "path": "integrations/openclaw/tests/handlers/augmentation.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { handleAugmentation } from '../../src/handlers/augmentation.js';\nimport type {\n  OpenClawEvent,\n  OpenClawContext,\n  MemoriPluginConfig,\n  OpenClawMessage,\n} from '../../src/types.js';\nimport type { MemoriLogger } from '../../src/utils/logger.js';\nimport { SDK_VERSION } from '../../src/version.js';\n\nvi.mock('../../src/sanitizer.js', () => ({\n  cleanText: vi.fn((content) => {\n    if (typeof content === 'string') return content;\n    return 'cleaned text';\n  }),\n  isSystemMessage: vi.fn(() => false),\n}));\n\nvi.mock('../../src/utils/index.js', () => ({\n  extractContext: vi.fn(() => ({\n    entityId: 'test-entity',\n    sessionId: 'test-session',\n    provider: 'test-provider',\n  })),\n  initializeMemoriClient: vi.fn(() => ({\n    augmentation: vi.fn(async () => {}),\n  })),\n}));\n\ndescribe('handlers/augmentation', () => {\n  let mockLogger: MemoriLogger;\n  let config: MemoriPluginConfig;\n  let event: OpenClawEvent;\n  let ctx: OpenClawContext;\n\n  beforeEach(() => {\n    vi.clearAllMocks();\n\n    mockLogger = {\n      section: vi.fn(),\n      endSection: vi.fn(),\n      info: vi.fn(),\n      error: vi.fn(),\n    } as unknown as MemoriLogger;\n\n    config = {\n      apiKey: 'test-api-key',\n      entityId: 'test-entity-id',\n    };\n\n    event = {\n      success: true,\n      messages: [\n        { role: 'user', content: 'Hello, how are you?' },\n        { role: 'assistant', content: \"I'm doing well, thank you!\" },\n      ],\n    };\n\n    ctx = {\n      sessionKey: 'session-123',\n      messageProvider: 'test-provider',\n    };\n  });\n\n  describe('successful augmentation', () => {\n    it('should send user and assistant messages to memori', async () => {\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          userMessage: 'Hello, how are you?',\n          agentResponse: \"I'm doing well, thank you!\",\n        })\n      );\n    });\n\n    it('should include metadata in request', async () => {\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          metadata: expect.objectContaining({\n            platform: 'openclaw',\n            integrationSdkVersion: SDK_VERSION,\n          }),\n        })\n      );\n    });\n  });\n\n  describe('event validation', () => {\n    it('should skip when event is unsuccessful', async () => {\n      event.success = false;\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'No messages or unsuccessful event. Skipping augmentation.'\n      );\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n      expect(initializeMemoriClient).not.toHaveBeenCalled();\n    });\n\n    it('should skip when messages array is empty', async () => {\n      event.messages = [];\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'No messages or unsuccessful event. Skipping augmentation.'\n      );\n    });\n\n    it('should skip when messages has only one message', async () => {\n      event.messages = [{ role: 'user', content: 'Hello' }];\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'No messages or unsuccessful event. Skipping augmentation.'\n      );\n    });\n\n    it('should skip when messages is undefined', async () => {\n      event.messages = undefined;\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'No messages or unsuccessful event. Skipping augmentation.'\n      );\n    });\n  });\n\n  describe('message extraction', () => {\n    it('should extract last user and assistant messages from recent history', async () => {\n      event.messages = [\n        { role: 'user', content: 'First message' },\n        { role: 'assistant', content: 'First response' },\n        { role: 'user', content: 'Second message' },\n        { role: 'assistant', content: 'Second response' },\n        { role: 'user', content: 'Third message' },\n        { role: 'assistant', content: 'Third response' },\n      ];\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          userMessage: 'Third message',\n          agentResponse: 'Third response',\n        })\n      );\n    });\n\n    it('should only consider last 5 messages', async () => {\n      event.messages = [\n        { role: 'user', content: 'Message 1' },\n        { role: 'assistant', content: 'Response 1' },\n        { role: 'user', content: 'Message 2' },\n        { role: 'assistant', content: 'Response 2' },\n        { role: 'user', content: 'Message 3' },\n        { role: 'assistant', content: 'Response 3' },\n        { role: 'user', content: 'Message 4' },\n        { role: 'assistant', content: 'Response 4' },\n        { role: 'user', content: 'Latest message' },\n        { role: 'assistant', content: 'Latest response' },\n      ];\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          userMessage: 'Latest message',\n          agentResponse: 'Latest response',\n        })\n      );\n    });\n\n    it('should skip system role messages', async () => {\n      event.messages = [\n        { role: 'system', content: 'System prompt' },\n        { role: 'user', content: 'User message' },\n        { role: 'assistant', content: 'Assistant response' },\n      ];\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          userMessage: 'User message',\n          agentResponse: 'Assistant response',\n        })\n      );\n    });\n\n    it('should skip when user or assistant message is missing', async () => {\n      event.messages = [\n        { role: 'system', content: 'Sys' },\n        { role: 'user', content: 'Only user message' },\n      ];\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.info).toHaveBeenCalledWith('Missing user or assistant message. Skipping.');\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n      expect(initializeMemoriClient).not.toHaveBeenCalled();\n    });\n\n    it('should skip messages with empty cleaned content', async () => {\n      const { cleanText } = await import('../../src/sanitizer.js');\n      vi.mocked(cleanText).mockReturnValueOnce('');\n\n      event.messages = [\n        { role: 'user', content: 'Some content' },\n        { role: 'assistant', content: 'Some response' },\n      ];\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.info).toHaveBeenCalledWith('Missing user or assistant message. Skipping.');\n    });\n  });\n\n  describe('system message filtering', () => {\n    it('should skip when user message is a system message', async () => {\n      const { isSystemMessage } = await import('../../src/sanitizer.js');\n      vi.mocked(isSystemMessage).mockReturnValueOnce(true);\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'User message is a system message. Skipping augmentation.'\n      );\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n      expect(initializeMemoriClient).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('synthetic responses', () => {\n    it('should replace NO_REPLY with synthetic response', async () => {\n      event.messages = [\n        { role: 'user', content: 'Remember my name is John' },\n        { role: 'assistant', content: 'NO_REPLY' },\n      ];\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          agentResponse: \"Okay, I'll remember that for you.\",\n        })\n      );\n\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'Assistant used tool-based messaging. Using synthetic response.'\n      );\n    });\n\n    it('should replace SILENT_REPLY with synthetic response', async () => {\n      event.messages = [\n        { role: 'user', content: 'Save this for later' },\n        { role: 'assistant', content: 'SILENT_REPLY' },\n      ];\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          agentResponse: \"Okay, I'll remember that for you.\",\n        })\n      );\n    });\n  });\n\n  describe('thinking block removal', () => {\n    it('should strip thinking blocks from assistant messages', async () => {\n      const { cleanText } = await import('../../src/sanitizer.js');\n      vi.mocked(cleanText).mockImplementationOnce((content) => {\n        if (typeof content === 'string' && content.includes('[[')) {\n          return '[[This is my thought process.]]\\n\\nActual response here.';\n        }\n        return content as string;\n      });\n\n      event.messages = [\n        { role: 'user', content: 'Question?' },\n        {\n          role: 'assistant',\n          content: '[[This is my thought process.]]\\n\\nActual response here.',\n        },\n      ];\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          agentResponse: 'Actual response here.',\n        })\n      );\n    });\n  });\n\n  describe('metadata extraction', () => {\n    it('should extract provider and model from last assistant message', async () => {\n      event.messages = [\n        { role: 'user', content: 'Question' },\n        {\n          role: 'assistant',\n          content: 'Answer',\n          provider: 'anthropic' as any,\n          model: 'claude-3-sonnet' as any,\n        },\n      ];\n\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          metadata: expect.objectContaining({\n            provider: 'anthropic',\n            model: 'claude-3-sonnet',\n          }),\n        })\n      );\n    });\n\n    it('should use null for missing provider and model', async () => {\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      const client = vi.mocked(initializeMemoriClient).mock.results[0].value;\n      expect(client.augmentation).toHaveBeenCalledWith(\n        expect.objectContaining({\n          metadata: expect.objectContaining({\n            provider: null,\n            model: null,\n          }),\n        })\n      );\n    });\n  });\n\n  describe('error handling', () => {\n    it('should handle errors gracefully', async () => {\n      const { extractContext } = await import('../../src/utils/index.js');\n      vi.mocked(extractContext).mockImplementationOnce(() => {\n        throw new Error('Context extraction failed');\n      });\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.error).toHaveBeenCalledWith(\n        'Augmentation failed: Context extraction failed'\n      );\n    });\n\n    it('should log non-Error objects as strings', async () => {\n      const { extractContext } = await import('../../src/utils/index.js');\n      vi.mocked(extractContext).mockImplementationOnce(() => {\n        throw 'String error';\n      });\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.error).toHaveBeenCalledWith('Augmentation failed: String error');\n    });\n\n    it('should handle API errors from memori client', async () => {\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n      vi.mocked(initializeMemoriClient).mockReturnValueOnce({\n        augmentation: vi.fn(async () => {\n          throw new Error('API connection failed');\n        }),\n      } as any);\n\n      await handleAugmentation(event, ctx, config, mockLogger);\n\n      expect(mockLogger.error).toHaveBeenCalledWith('Augmentation failed: API connection failed');\n    });\n  });\n});\n"
  },
  {
    "path": "integrations/openclaw/tests/handlers/recall.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { handleRecall } from '../../src/handlers/recall.js';\nimport type { OpenClawEvent, OpenClawContext, MemoriPluginConfig } from '../../src/types.js';\nimport type { MemoriLogger } from '../../src/utils/logger.js';\n\nvi.mock('../../src/sanitizer.js', () => ({\n  cleanText: vi.fn((text) => text),\n  isSystemMessage: vi.fn(() => false),\n}));\n\nvi.mock('../../src/utils/index.js', () => ({\n  extractContext: vi.fn(() => ({\n    entityId: 'test-entity',\n    sessionId: 'test-session',\n    provider: 'test-provider',\n  })),\n  initializeMemoriClient: vi.fn(() => ({\n    recall: vi.fn(async () => '<memori_context>Relevant memories</memori_context>'),\n  })),\n}));\n\ndescribe('handlers/recall', () => {\n  let mockLogger: MemoriLogger;\n  let config: MemoriPluginConfig;\n  let event: OpenClawEvent;\n  let ctx: OpenClawContext;\n\n  beforeEach(() => {\n    vi.clearAllMocks();\n\n    mockLogger = {\n      section: vi.fn(),\n      endSection: vi.fn(),\n      info: vi.fn(),\n      error: vi.fn(),\n    } as unknown as MemoriLogger;\n\n    config = {\n      apiKey: 'test-api-key',\n      entityId: 'test-entity-id',\n    };\n\n    event = {\n      prompt: 'What is the weather today?',\n    };\n\n    ctx = {\n      sessionKey: 'session-123',\n      messageProvider: 'test-provider',\n    };\n  });\n\n  describe('successful recall', () => {\n    it('should return prependContext when memories are found', async () => {\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toEqual({\n        prependContext: '<memori_context>Relevant memories</memori_context>',\n      });\n    });\n\n    it('should call extractContext with correct parameters', async () => {\n      const { extractContext } = await import('../../src/utils/index.js');\n      await handleRecall(event, ctx, config, mockLogger);\n\n      expect(extractContext).toHaveBeenCalledWith(event, ctx, 'test-entity-id');\n    });\n\n    it('should call initializeMemoriClient with correct parameters', async () => {\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n      await handleRecall(event, ctx, config, mockLogger);\n\n      expect(initializeMemoriClient).toHaveBeenCalledWith('test-api-key', {\n        entityId: 'test-entity',\n        sessionId: 'test-session',\n        provider: 'test-provider',\n      });\n    });\n  });\n\n  describe('no memories found', () => {\n    it('should return undefined when memori returns empty string', async () => {\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n      vi.mocked(initializeMemoriClient).mockReturnValueOnce({\n        recall: vi.fn(async () => ''),\n      } as any);\n\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n      expect(mockLogger.info).toHaveBeenCalledWith('No relevant memories found.');\n    });\n\n    it('should return undefined when memori returns null', async () => {\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n      vi.mocked(initializeMemoriClient).mockReturnValueOnce({\n        recall: vi.fn(async () => null),\n      } as any);\n\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n    });\n  });\n\n  describe('prompt validation', () => {\n    it('should abort when prompt is empty after cleaning', async () => {\n      const { cleanText } = await import('../../src/sanitizer.js');\n      vi.mocked(cleanText).mockReturnValueOnce('');\n\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'Prompt too short or is a system message. Aborting recall.'\n      );\n    });\n\n    it('should abort when prompt is too short', async () => {\n      const { cleanText } = await import('../../src/sanitizer.js');\n      vi.mocked(cleanText).mockReturnValueOnce('a');\n\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'Prompt too short or is a system message. Aborting recall.'\n      );\n    });\n\n    it('should abort when prompt is a system message', async () => {\n      const { isSystemMessage } = await import('../../src/sanitizer.js');\n      vi.mocked(isSystemMessage).mockReturnValueOnce(true);\n\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n      expect(mockLogger.info).toHaveBeenCalledWith(\n        'Prompt too short or is a system message. Aborting recall.'\n      );\n    });\n\n    it('should proceed when prompt meets minimum length', async () => {\n      const { cleanText } = await import('../../src/sanitizer.js');\n      vi.mocked(cleanText).mockReturnValueOnce('Hello');\n\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toBeDefined();\n    });\n  });\n\n  describe('error handling', () => {\n    it('should handle errors gracefully and return undefined', async () => {\n      const { extractContext } = await import('../../src/utils/index.js');\n      vi.mocked(extractContext).mockImplementationOnce(() => {\n        throw new Error('Context extraction failed');\n      });\n\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n      expect(mockLogger.error).toHaveBeenCalledWith('Recall failed: Context extraction failed');\n    });\n\n    it('should log non-Error objects as strings', async () => {\n      const { extractContext } = await import('../../src/utils/index.js');\n      vi.mocked(extractContext).mockImplementationOnce(() => {\n        throw 'String error';\n      });\n\n      await handleRecall(event, ctx, config, mockLogger);\n\n      expect(mockLogger.error).toHaveBeenCalledWith('Recall failed: String error');\n    });\n\n    it('should handle API errors from memori client', async () => {\n      const { initializeMemoriClient } = await import('../../src/utils/index.js');\n      vi.mocked(initializeMemoriClient).mockReturnValueOnce({\n        recall: vi.fn(async () => {\n          throw new Error('API connection failed');\n        }),\n      } as any);\n\n      const result = await handleRecall(event, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n      expect(mockLogger.error).toHaveBeenCalledWith('Recall failed: API connection failed');\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle missing prompt gracefully', async () => {\n      const eventWithoutPrompt: OpenClawEvent = {};\n\n      const result = await handleRecall(eventWithoutPrompt, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n    });\n\n    it('should handle null prompt', async () => {\n      const eventWithNullPrompt: OpenClawEvent = { prompt: null as any };\n\n      const result = await handleRecall(eventWithNullPrompt, ctx, config, mockLogger);\n\n      expect(result).toBeUndefined();\n    });\n\n    it('should call cleanText with the prompt', async () => {\n      const { cleanText } = await import('../../src/sanitizer.js');\n\n      await handleRecall(event, ctx, config, mockLogger);\n\n      expect(cleanText).toHaveBeenCalledWith('What is the weather today?');\n    });\n  });\n});\n"
  },
  {
    "path": "integrations/openclaw/tests/index.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport memoriPlugin from '../src/index.js';\nimport type { OpenClawPluginApi } from 'openclaw/plugin-sdk';\n\nvi.mock('../src/handlers/recall.js', () => ({\n  handleRecall: vi.fn(),\n}));\n\nvi.mock('../src/handlers/augmentation.js', () => ({\n  handleAugmentation: vi.fn(),\n}));\n\ndescribe('plugin index', () => {\n  let mockApi: OpenClawPluginApi;\n\n  beforeEach(() => {\n    vi.clearAllMocks();\n\n    mockApi = {\n      pluginConfig: {\n        apiKey: 'test-api-key',\n        entityId: 'test-entity-id',\n      },\n      logger: {\n        info: vi.fn(),\n        warn: vi.fn(),\n        error: vi.fn(),\n      },\n      on: vi.fn(),\n    } as unknown as OpenClawPluginApi;\n  });\n\n  describe('plugin metadata', () => {\n    it('should have correct id', () => {\n      expect(memoriPlugin.id).toBe('openclaw-memori');\n    });\n\n    it('should have correct name', () => {\n      expect(memoriPlugin.name).toBe('Memori System');\n    });\n\n    it('should have description', () => {\n      expect(memoriPlugin.description).toBe('Hosted memory backend');\n    });\n  });\n\n  describe('register', () => {\n    it('should register hooks when config is valid', () => {\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.on).toHaveBeenCalledWith('before_prompt_build', expect.any(Function));\n      expect(mockApi.on).toHaveBeenCalledWith('agent_end', expect.any(Function));\n      expect(mockApi.on).toHaveBeenCalledTimes(2);\n    });\n\n    it('should not register when apiKey is missing', () => {\n      mockApi.pluginConfig = {\n        entityId: 'test-entity-id',\n      };\n\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.logger.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Missing apiKey or entityId')\n      );\n      expect(mockApi.on).not.toHaveBeenCalled();\n    });\n\n    it('should not register when entityId is missing', () => {\n      mockApi.pluginConfig = {\n        apiKey: 'test-api-key',\n      };\n\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.logger.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Missing apiKey or entityId')\n      );\n      expect(mockApi.on).not.toHaveBeenCalled();\n    });\n\n    it('should not register when both apiKey and entityId are missing', () => {\n      mockApi.pluginConfig = {};\n\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.logger.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Missing apiKey or entityId')\n      );\n      expect(mockApi.on).not.toHaveBeenCalled();\n    });\n\n    it('should not register when pluginConfig is undefined', () => {\n      mockApi.pluginConfig = undefined;\n\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.logger.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Missing apiKey or entityId')\n      );\n      expect(mockApi.on).not.toHaveBeenCalled();\n    });\n\n    it('should not register when apiKey is empty string', () => {\n      mockApi.pluginConfig = {\n        apiKey: '',\n        entityId: 'test-entity-id',\n      };\n\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.logger.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Missing apiKey or entityId')\n      );\n      expect(mockApi.on).not.toHaveBeenCalled();\n    });\n\n    it('should not register when entityId is empty string', () => {\n      mockApi.pluginConfig = {\n        apiKey: 'test-api-key',\n        entityId: '',\n      };\n\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.logger.warn).toHaveBeenCalledWith(\n        expect.stringContaining('Missing apiKey or entityId')\n      );\n      expect(mockApi.on).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('hook handlers', () => {\n    it('should call handleRecall for before_prompt_build event', async () => {\n      const { handleRecall } = await import('../src/handlers/recall.js');\n\n      memoriPlugin.register(mockApi);\n\n      const beforePromptBuildHandler = vi\n        .mocked(mockApi.on)\n        .mock.calls.find((call) => call[0] === 'before_prompt_build')?.[1];\n\n      expect(beforePromptBuildHandler).toBeDefined();\n\n      const mockEvent = { prompt: 'test' } as any;\n      const mockCtx = { sessionKey: 'session-123' } as any;\n\n      await beforePromptBuildHandler?.(mockEvent, mockCtx);\n\n      expect(handleRecall).toHaveBeenCalledWith(\n        mockEvent,\n        mockCtx,\n        { apiKey: 'test-api-key', entityId: 'test-entity-id' },\n        expect.any(Object)\n      );\n    });\n\n    it('should call handleAugmentation for agent_end event', async () => {\n      const { handleAugmentation } = await import('../src/handlers/augmentation.js');\n\n      memoriPlugin.register(mockApi);\n\n      const agentEndHandler = vi\n        .mocked(mockApi.on)\n        .mock.calls.find((call) => call[0] === 'agent_end')?.[1];\n\n      expect(agentEndHandler).toBeDefined();\n\n      const mockEvent = { success: true, messages: [] } as any;\n      const mockCtx = { sessionKey: 'session-123' } as any;\n\n      await agentEndHandler?.(mockEvent, mockCtx);\n\n      expect(handleAugmentation).toHaveBeenCalledWith(\n        mockEvent,\n        mockCtx,\n        { apiKey: 'test-api-key', entityId: 'test-entity-id' },\n        expect.any(Object)\n      );\n    });\n  });\n\n  describe('configuration handling', () => {\n    it('should extract apiKey from pluginConfig', () => {\n      mockApi.pluginConfig = {\n        apiKey: 'custom-key-123',\n        entityId: 'entity-456',\n      };\n\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.on).toHaveBeenCalled();\n    });\n\n    it('should handle additional config properties gracefully', () => {\n      mockApi.pluginConfig = {\n        apiKey: 'test-api-key',\n        entityId: 'test-entity-id',\n        extraProperty: 'should be ignored',\n        anotherExtra: 12345,\n      };\n\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.on).toHaveBeenCalledTimes(2);\n    });\n  });\n\n  describe('logger creation', () => {\n    it('should create MemoriLogger with api', () => {\n      memoriPlugin.register(mockApi);\n\n      expect(mockApi.logger.info).toHaveBeenCalledWith(expect.stringContaining('[Memori]'));\n    });\n  });\n});\n"
  },
  {
    "path": "integrations/openclaw/tests/sanitizer.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { isSystemMessage, cleanText } from '../src/sanitizer.js';\nimport type { OpenClawMessageBlock } from '../src/types.js';\n\ndescribe('sanitizer', () => {\n  describe('isSystemMessage', () => {\n    it('should return true for empty text', () => {\n      expect(isSystemMessage('')).toBe(true);\n    });\n\n    it('should return true for system startup messages', () => {\n      expect(isSystemMessage('a new session was started')).toBe(true);\n      expect(isSystemMessage('A NEW SESSION WAS STARTED')).toBe(true);\n    });\n\n    it('should return true for reset command messages', () => {\n      expect(isSystemMessage('/new or /reset')).toBe(true);\n      expect(isSystemMessage('Use /new or /reset to start fresh')).toBe(true);\n    });\n\n    it('should return true for session startup sequence', () => {\n      expect(isSystemMessage('session startup sequence initiated')).toBe(true);\n    });\n\n    it('should return true for persona messages', () => {\n      expect(isSystemMessage('use persona: helpful assistant')).toBe(true);\n    });\n\n    it('should return false for regular user messages', () => {\n      expect(isSystemMessage('Hello, how are you?')).toBe(false);\n      expect(isSystemMessage('What is the weather today?')).toBe(false);\n      expect(isSystemMessage('Can you help me with this code?')).toBe(false);\n    });\n  });\n\n  describe('cleanText', () => {\n    describe('string content', () => {\n      it('should return the string as-is when no special formatting', () => {\n        const input = 'Hello, world!';\n        expect(cleanText(input)).toBe('Hello, world!');\n      });\n\n      it('should extract message after last code fence', () => {\n        const input = '```metadata\\nsome: data\\n```\\nActual message here';\n        expect(cleanText(input)).toBe('Actual message here');\n      });\n\n      it('should remove timestamp prefix from message', () => {\n        const input = '```metadata```\\n[Mon 2024-03-09 14:30 UTC] Hello there';\n        expect(cleanText(input)).toBe('Hello there');\n      });\n\n      it('should handle timestamp with timezone offset', () => {\n        const input = '[Tue 2024-03-10 09:15 EST] Good morning';\n        expect(cleanText(input)).toBe('Good morning');\n      });\n\n      it('should remove memori_context tags', () => {\n        const input = '<memori_context>Previous facts</memori_context>\\nNew message';\n        expect(cleanText(input)).toBe('New message');\n      });\n\n      it('should handle multiple memori_context blocks', () => {\n        const input =\n          '<memori_context>Block 1</memori_context>\\nText\\n<memori_context>Block 2</memori_context>';\n        expect(cleanText(input)).toBe('Text');\n      });\n\n      it('should handle complex combination of formatting', () => {\n        const input = `\\`\\`\\`metadata\nsessionId: abc-123\n\\`\\`\\`\n[Wed 2024-03-11 16:45 UTC] <memori_context>Previous context</memori_context>\nWhat is the capital of France?`;\n        expect(cleanText(input)).toBe('What is the capital of France?');\n      });\n\n      it('should return empty string when only metadata present', () => {\n        const input = '```metadata\\ndata\\n```';\n        expect(cleanText(input)).toBe('');\n      });\n\n      it('should handle content without code fences', () => {\n        const input = '[Thu 2024-03-12 10:00 PST] Hello';\n        expect(cleanText(input)).toBe('Hello');\n      });\n    });\n\n    describe('message block array content', () => {\n      it('should extract text from text blocks', () => {\n        const blocks: OpenClawMessageBlock[] = [\n          { type: 'text', text: 'First part' },\n          { type: 'text', text: 'Second part' },\n        ];\n        expect(cleanText(blocks)).toBe('First part\\n\\nSecond part');\n      });\n\n      it('should filter out non-text blocks', () => {\n        const blocks: OpenClawMessageBlock[] = [\n          { type: 'text', text: 'User message' },\n          { type: 'thinking', thinking: 'Internal thought' },\n          { type: 'tool_use', name: 'search' },\n        ];\n        expect(cleanText(blocks)).toBe('User message');\n      });\n\n      it('should handle blocks without explicit type but with text', () => {\n        const blocks: OpenClawMessageBlock[] = [{ text: 'Message 1' }, { text: 'Message 2' }];\n        expect(cleanText(blocks)).toBe('Message 1\\n\\nMessage 2');\n      });\n\n      it('should filter out empty text blocks', () => {\n        const blocks: OpenClawMessageBlock[] = [\n          { type: 'text', text: '' },\n          { type: 'text', text: 'Valid message' },\n          { type: 'text', text: '' },\n        ];\n        expect(cleanText(blocks)).toBe('Valid message');\n      });\n\n      it('should handle empty array', () => {\n        expect(cleanText([])).toBe('');\n      });\n\n      it('should apply post-processing to extracted text', () => {\n        const blocks: OpenClawMessageBlock[] = [\n          {\n            type: 'text',\n            text: '```metadata```\\n[Fri 2024-03-13 11:20 UTC] Clean this message',\n          },\n        ];\n        expect(cleanText(blocks)).toBe('Clean this message');\n      });\n    });\n\n    describe('edge cases', () => {\n      it('should return empty string for null input', () => {\n        expect(cleanText(null)).toBe('');\n      });\n\n      it('should return empty string for undefined input', () => {\n        expect(cleanText(undefined)).toBe('');\n      });\n\n      it('should handle objects that are not arrays or strings', () => {\n        expect(cleanText({ random: 'object' })).toBe('');\n      });\n\n      it('should handle number input', () => {\n        expect(cleanText(42)).toBe('');\n      });\n\n      it('should trim whitespace from final result', () => {\n        expect(cleanText('  \\n  Hello  \\n  ')).toBe('Hello');\n      });\n\n      it('should preserve internal whitespace', () => {\n        const input = 'Hello   world\\n\\nNew paragraph';\n        expect(cleanText(input)).toBe('Hello   world\\n\\nNew paragraph');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "integrations/openclaw/tests/utils/context.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { extractContext } from '../../src/utils/context.js';\nimport type { OpenClawEvent, OpenClawContext } from '../../src/types.js';\n\ndescribe('utils/context', () => {\n  describe('extractContext', () => {\n    const configuredEntityId = 'test-entity-123';\n\n    it('should extract context successfully with all required fields', () => {\n      const event: OpenClawEvent = {\n        sessionId: 'event-session-456',\n        messageProvider: 'event-provider',\n      };\n      const ctx: OpenClawContext = {\n        sessionKey: 'ctx-session-789',\n        messageProvider: 'ctx-provider',\n      };\n\n      const result = extractContext(event, ctx, configuredEntityId);\n\n      expect(result).toEqual({\n        entityId: 'test-entity-123',\n        sessionId: 'ctx-session-789',\n        provider: 'ctx-provider',\n      });\n    });\n\n    it('should use event sessionId when ctx.sessionKey is missing', () => {\n      const event: OpenClawEvent = {\n        sessionId: 'event-session-456',\n        messageProvider: 'event-provider',\n      };\n      const ctx: OpenClawContext = {\n        messageProvider: 'ctx-provider',\n      };\n\n      const result = extractContext(event, ctx, configuredEntityId);\n\n      expect(result.sessionId).toBe('event-session-456');\n    });\n\n    it('should use event messageProvider when ctx.messageProvider is missing', () => {\n      const event: OpenClawEvent = {\n        sessionId: 'event-session-456',\n        messageProvider: 'event-provider',\n      };\n      const ctx: OpenClawContext = {\n        sessionKey: 'ctx-session-789',\n      };\n\n      const result = extractContext(event, ctx, configuredEntityId);\n\n      expect(result.provider).toBe('event-provider');\n    });\n\n    it('should throw error when sessionId cannot be determined', () => {\n      const event: OpenClawEvent = {\n        messageProvider: 'event-provider',\n      };\n      const ctx: OpenClawContext = {\n        messageProvider: 'ctx-provider',\n      };\n\n      expect(() => extractContext(event, ctx, configuredEntityId)).toThrow(\n        'Failed to extract context: Missing sessionId in OpenClaw context and event.'\n      );\n    });\n\n    it('should throw error when provider cannot be determined', () => {\n      const event: OpenClawEvent = {\n        sessionId: 'event-session-456',\n      };\n      const ctx: OpenClawContext = {\n        sessionKey: 'ctx-session-789',\n      };\n\n      expect(() => extractContext(event, ctx, configuredEntityId)).toThrow(\n        'Failed to extract context: Missing message provider in OpenClaw context and event.'\n      );\n    });\n\n    it('should throw error when both sessionId and provider are missing', () => {\n      const event: OpenClawEvent = {};\n      const ctx: OpenClawContext = {};\n\n      expect(() => extractContext(event, ctx, configuredEntityId)).toThrow();\n    });\n\n    it('should handle empty string sessionId as missing', () => {\n      const event: OpenClawEvent = {\n        messageProvider: 'provider',\n      };\n      const ctx: OpenClawContext = {\n        sessionKey: '',\n        messageProvider: 'ctx-provider',\n      };\n\n      expect(() => extractContext(event, ctx, configuredEntityId)).toThrow(\n        'Failed to extract context: Missing sessionId'\n      );\n    });\n\n    it('should handle empty string provider as missing', () => {\n      const event: OpenClawEvent = {\n        sessionId: 'session-123',\n      };\n      const ctx: OpenClawContext = {\n        sessionKey: 'ctx-session-789',\n        messageProvider: '',\n      };\n\n      expect(() => extractContext(event, ctx, configuredEntityId)).toThrow(\n        'Failed to extract context: Missing message provider'\n      );\n    });\n\n    it('should always use configuredEntityId from plugin config', () => {\n      const event: OpenClawEvent = {\n        sessionId: 'session-123',\n        messageProvider: 'provider',\n        userId: 'different-user-id',\n      };\n      const ctx: OpenClawContext = {\n        sessionKey: 'session-789',\n        messageProvider: 'ctx-provider',\n        agentId: 'agent-id',\n      };\n\n      const result = extractContext(event, ctx, 'my-configured-entity');\n\n      expect(result.entityId).toBe('my-configured-entity');\n    });\n  });\n});\n"
  },
  {
    "path": "integrations/openclaw/tests/utils/logger.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { MemoriLogger } from '../../src/utils/logger.js';\nimport type { OpenClawPluginApi } from 'openclaw/plugin-sdk';\n\ndescribe('utils/logger', () => {\n  let mockApi: OpenClawPluginApi;\n  let logger: MemoriLogger;\n\n  beforeEach(() => {\n    mockApi = {\n      logger: {\n        info: vi.fn(),\n        warn: vi.fn(),\n        error: vi.fn(),\n      },\n    } as unknown as OpenClawPluginApi;\n\n    logger = new MemoriLogger(mockApi);\n  });\n\n  describe('info', () => {\n    it('should call api.logger.info with prefixed message', () => {\n      logger.info('Test message');\n      expect(mockApi.logger.info).toHaveBeenCalledWith('[Memori] Test message');\n    });\n\n    it('should handle empty strings', () => {\n      logger.info('');\n      expect(mockApi.logger.info).toHaveBeenCalledWith('[Memori] ');\n    });\n\n    it('should preserve message content exactly', () => {\n      logger.info('Message with    spaces\\nand newlines');\n      expect(mockApi.logger.info).toHaveBeenCalledWith(\n        '[Memori] Message with    spaces\\nand newlines'\n      );\n    });\n  });\n\n  describe('warn', () => {\n    it('should call api.logger.warn with prefixed message', () => {\n      logger.warn('Warning message');\n      expect(mockApi.logger.warn).toHaveBeenCalledWith('[Memori] Warning message');\n    });\n  });\n\n  describe('error', () => {\n    it('should call api.logger.error with prefixed message', () => {\n      logger.error('Error message');\n      expect(mockApi.logger.error).toHaveBeenCalledWith('[Memori] Error message');\n    });\n\n    it('should handle error messages with special characters', () => {\n      logger.error('Error: Failed to connect (code: 500)');\n      expect(mockApi.logger.error).toHaveBeenCalledWith(\n        '[Memori] Error: Failed to connect (code: 500)'\n      );\n    });\n  });\n\n  describe('section', () => {\n    it('should log section start with formatting', () => {\n      logger.section('HOOK START');\n      expect(mockApi.logger.info).toHaveBeenCalledWith('\\n=== [Memori] HOOK START ===');\n    });\n\n    it('should handle multi-word section titles', () => {\n      logger.section('AUGMENTATION HOOK START');\n      expect(mockApi.logger.info).toHaveBeenCalledWith(\n        '\\n=== [Memori] AUGMENTATION HOOK START ==='\n      );\n    });\n  });\n\n  describe('endSection', () => {\n    it('should log section end with formatting', () => {\n      logger.endSection('HOOK END');\n      expect(mockApi.logger.info).toHaveBeenCalledWith('=== [Memori] HOOK END ===\\n');\n    });\n  });\n\n  describe('prefix consistency', () => {\n    it('should use consistent prefix across all log levels', () => {\n      logger.info('info');\n      logger.warn('warn');\n      logger.error('error');\n\n      expect(mockApi.logger.info).toHaveBeenCalledWith('[Memori] info');\n      expect(mockApi.logger.warn).toHaveBeenCalledWith('[Memori] warn');\n      expect(mockApi.logger.error).toHaveBeenCalledWith('[Memori] error');\n    });\n  });\n\n  describe('call count verification', () => {\n    it('should call underlying logger exactly once per method call', () => {\n      logger.info('test');\n      expect(mockApi.logger.info).toHaveBeenCalledTimes(1);\n\n      logger.warn('test');\n      expect(mockApi.logger.warn).toHaveBeenCalledTimes(1);\n\n      logger.error('test');\n      expect(mockApi.logger.error).toHaveBeenCalledTimes(1);\n    });\n\n    it('should accumulate calls correctly', () => {\n      logger.info('first');\n      logger.info('second');\n      logger.info('third');\n\n      expect(mockApi.logger.info).toHaveBeenCalledTimes(3);\n    });\n  });\n});\n"
  },
  {
    "path": "integrations/openclaw/tests/utils/memori-client.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { initializeMemoriClient } from '../../src/utils/memori-client.js';\nimport type { ExtractedContext } from '../../src/utils/context.js';\n\nvi.mock('@memorilabs/memori', () => {\n  const mockOpenClawIntegration = {\n    setAttribution: vi.fn(),\n    setSession: vi.fn(),\n  };\n\n  const mockMemori = {\n    config: {},\n    integrate: vi.fn(() => mockOpenClawIntegration),\n  };\n\n  return {\n    Memori: vi.fn(function () {\n      return mockMemori;\n    }),\n  };\n});\n\nvi.mock('@memorilabs/memori/integrations', () => ({\n  OpenClawIntegration: class MockOpenClawIntegration {},\n}));\n\ndescribe('utils/memori-client', () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  describe('initializeMemoriClient', () => {\n    it('should create Memori instance with API key', async () => {\n      const { Memori } = await import('@memorilabs/memori');\n      const apiKey = 'test-api-key-123';\n      const context: ExtractedContext = {\n        entityId: 'entity-456',\n        sessionId: 'session-789',\n        provider: 'test-provider',\n      };\n\n      initializeMemoriClient(apiKey, context);\n\n      expect(Memori).toHaveBeenCalled();\n      const memoriInstance = vi.mocked(Memori).mock.results[0].value;\n      expect(memoriInstance.config.apiKey).toBe('test-api-key-123');\n    });\n\n    it('should integrate with OpenClawIntegration', async () => {\n      const { Memori } = await import('@memorilabs/memori');\n      const { OpenClawIntegration } = await import('@memorilabs/memori/integrations');\n\n      const apiKey = 'test-api-key';\n      const context: ExtractedContext = {\n        entityId: 'entity-123',\n        sessionId: 'session-456',\n        provider: 'provider-789',\n      };\n\n      initializeMemoriClient(apiKey, context);\n\n      const memoriInstance = vi.mocked(Memori).mock.results[0].value;\n      expect(memoriInstance.integrate).toHaveBeenCalledWith(OpenClawIntegration);\n    });\n\n    it('should set attribution with entityId and provider', async () => {\n      const { Memori } = await import('@memorilabs/memori');\n\n      const apiKey = 'test-api-key';\n      const context: ExtractedContext = {\n        entityId: 'user-abc',\n        sessionId: 'session-xyz',\n        provider: 'openai',\n      };\n\n      initializeMemoriClient(apiKey, context);\n\n      const memoriInstance = vi.mocked(Memori).mock.results[0].value;\n      const integration = memoriInstance.integrate.mock.results[0].value;\n      expect(integration.setAttribution).toHaveBeenCalledWith('user-abc', 'openai');\n    });\n\n    it('should set session with sessionId', async () => {\n      const { Memori } = await import('@memorilabs/memori');\n\n      const apiKey = 'test-api-key';\n      const context: ExtractedContext = {\n        entityId: 'entity-123',\n        sessionId: 'my-session-id',\n        provider: 'anthropic',\n      };\n\n      initializeMemoriClient(apiKey, context);\n\n      const memoriInstance = vi.mocked(Memori).mock.results[0].value;\n      const integration = memoriInstance.integrate.mock.results[0].value;\n      expect(integration.setSession).toHaveBeenCalledWith('my-session-id');\n    });\n\n    it('should return the OpenClawIntegration instance', async () => {\n      const { Memori } = await import('@memorilabs/memori');\n\n      const apiKey = 'test-api-key';\n      const context: ExtractedContext = {\n        entityId: 'entity-123',\n        sessionId: 'session-456',\n        provider: 'provider-789',\n      };\n\n      const result = initializeMemoriClient(apiKey, context);\n\n      const memoriInstance = vi.mocked(Memori).mock.results[0].value;\n      const expectedIntegration = memoriInstance.integrate.mock.results[0].value;\n      expect(result).toBe(expectedIntegration);\n    });\n\n    it('should configure client with correct sequence', async () => {\n      const { Memori } = await import('@memorilabs/memori');\n\n      const apiKey = 'secret-key';\n      const context: ExtractedContext = {\n        entityId: 'user-999',\n        sessionId: 'sess-888',\n        provider: 'google',\n      };\n\n      const result = initializeMemoriClient(apiKey, context);\n\n      expect(Memori).toHaveBeenCalled();\n      const memoriInstance = vi.mocked(Memori).mock.results[0].value;\n      expect(memoriInstance.config.apiKey).toBe('secret-key');\n      expect(memoriInstance.integrate).toHaveBeenCalled();\n      expect(result.setAttribution).toHaveBeenCalledWith('user-999', 'google');\n      expect(result.setSession).toHaveBeenCalledWith('sess-888');\n    });\n  });\n});\n"
  },
  {
    "path": "integrations/openclaw/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\", \"DOM\"],\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"declaration\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"test\", \"**/*.test.ts\", \"**/*.spec.ts\"]\n}\n"
  },
  {
    "path": "integrations/openclaw/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\nimport path from 'node:path';\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: 'node',\n    coverage: {\n      provider: 'v8',\n      reporter: ['text', 'json', 'html'],\n      include: ['src/**'],\n      exclude: [\n        'node_modules/',\n        'dist/',\n        'tests/',\n        'examples/',\n        '**/*.config.ts',\n        '**/*.d.ts',\n        '**/types/**',\n        '**/index.ts',\n      ],\n    },\n    include: ['tests/**/*.test.ts'],\n    exclude: ['node_modules/', 'dist/'],\n  },\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, './src'),\n    },\n  },\n});\n"
  },
  {
    "path": "memori/__init__.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport os\nfrom collections.abc import Callable\nfrom typing import Any\nfrom uuid import uuid4\n\nfrom memori._config import Config\nfrom memori._exceptions import (\n    MissingMemoriApiKeyError,\n    MissingPsycopgError,\n    QuotaExceededError,\n    UnsupportedLLMProviderError,\n    warn_if_legacy_memorisdk_installed,\n)\nfrom memori.embeddings import embed_texts\nfrom memori.llm._providers import Agno as LlmProviderAgno\nfrom memori.llm._providers import Anthropic as LlmProviderAnthropic\nfrom memori.llm._providers import Google as LlmProviderGoogle\nfrom memori.llm._providers import LangChain as LlmProviderLangChain\nfrom memori.llm._providers import OpenAi as LlmProviderOpenAi\nfrom memori.llm._providers import PydanticAi as LlmProviderPydanticAi\nfrom memori.llm._providers import XAi as LlmProviderXAi\nfrom memori.memory.augmentation import Manager as AugmentationManager\nfrom memori.memory.recall import Recall\nfrom memori.storage import Manager as StorageManager\n\n__all__ = [\"Memori\", \"QuotaExceededError\", \"UnsupportedLLMProviderError\"]\n\nwarn_if_legacy_memorisdk_installed()\n\n\nclass LlmRegistry:\n    def __init__(self, memori):\n        self.memori = memori\n\n    def register(\n        self,\n        client=None,\n        openai_chat=None,\n        claude=None,\n        gemini=None,\n        xai=None,\n        chatbedrock=None,\n        chatgooglegenai=None,\n        chatopenai=None,\n        chatvertexai=None,\n    ):\n        from memori.llm._registry import register_llm\n\n        return register_llm(\n            self.memori,\n            client=client,\n            openai_chat=openai_chat,\n            claude=claude,\n            gemini=gemini,\n            xai=xai,\n            chatbedrock=chatbedrock,\n            chatgooglegenai=chatgooglegenai,\n            chatopenai=chatopenai,\n            chatvertexai=chatvertexai,\n        )\n\n\nclass Memori:\n    def __init__(\n        self,\n        conn: Callable[[], Any] | Any | None = None,\n        debug_truncate: bool = True,\n    ):\n        from memori._logging import set_truncate_enabled\n\n        self.config = Config()\n        self.config.api_key = os.environ.get(\"MEMORI_API_KEY\", None)\n        self.config.session_id = uuid4()\n        self.config.debug_truncate = debug_truncate\n        set_truncate_enabled(debug_truncate)\n\n        if conn is None:\n            conn = self._get_default_connection()\n        else:\n            self.config.cloud = False\n\n        self.config.storage = StorageManager(self.config).start(conn)\n        self.config.augmentation = AugmentationManager(self.config).start(conn)\n\n        self.augmentation = self.config.augmentation\n        self.llm = LlmRegistry(self)\n        self.agno = LlmProviderAgno(self)\n        self.anthropic = LlmProviderAnthropic(self)\n        self.google = LlmProviderGoogle(self)\n        self.langchain = LlmProviderLangChain(self)\n        self.openai = LlmProviderOpenAi(self)\n        self.pydantic_ai = LlmProviderPydanticAi(self)\n        self.xai = LlmProviderXAi(self)\n\n    def _get_default_connection(self) -> Callable[[], Any] | None:\n        connection_string = os.environ.get(\"MEMORI_COCKROACHDB_CONNECTION_STRING\", None)\n        if connection_string:\n            try:\n                import psycopg\n            except ImportError as e:\n                raise MissingPsycopgError(\"CockroachDB\") from e\n\n            self.config.cloud = False\n            return lambda: psycopg.connect(connection_string)\n\n        self.config.cloud = True\n        api_key = os.environ.get(\"MEMORI_API_KEY\", None)\n        if api_key is None or api_key == \"\":\n            raise MissingMemoriApiKeyError()\n        return None\n\n    def attribution(self, entity_id=None, process_id=None):\n        if entity_id is not None:\n            entity_id = str(entity_id)\n\n            if len(entity_id) > 100:\n                raise RuntimeError(\"entity_id cannot be greater than 100 characters\")\n\n        if process_id is not None:\n            process_id = str(process_id)\n\n            if len(process_id) > 100:\n                raise RuntimeError(\"process_id cannot be greater than 100 characters\")\n\n        self.config.entity_id = entity_id\n        self.config.process_id = process_id\n\n        return self\n\n    def new_session(self):\n        self.config.session_id = uuid4()\n        self.config.reset_cache()\n        return self\n\n    def set_session(self, id):\n        self.config.session_id = id\n        return self\n\n    def recall(self, query: str, limit: int | None = None):\n        return Recall(self.config).search_facts(query, limit)\n\n    def close(self) -> None:\n        \"\"\"Close the underlying storage connection/session, if any.\n\n        This is especially important for long-running processes (e.g. web servers)\n        where you want to explicitly release database connections.\n        \"\"\"\n        storage = getattr(self.config, \"storage\", None)\n        adapter = getattr(storage, \"adapter\", None) if storage is not None else None\n        if adapter is None:\n            return\n        try:\n            adapter.close()\n        except Exception:  # nosec B110\n            pass\n\n    def __enter__(self) -> \"Memori\":\n        return self\n\n    def __exit__(self, exc_type, exc, tb) -> None:\n        self.close()\n\n    def embed_texts(self, texts: str | list[str], *, async_: bool = False) -> Any:\n        embeddings_cfg = self.config.embeddings\n        return embed_texts(\n            texts,\n            model=embeddings_cfg.model,\n            async_=async_,\n        )\n"
  },
  {
    "path": "memori/__main__.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport sys\nfrom typing import Any\n\nfrom memori._cli import Cli\nfrom memori._config import Config\nfrom memori._setup import Manager as SetupManager\nfrom memori.api._quota import Manager as ApiQuotaManager\nfrom memori.api._sign_up import Manager as ApiSignUpManager\nfrom memori.storage.cockroachdb._cluster_manager import (\n    ClusterManager as CockroachDBClusterManager,\n)\n\n\ndef main():\n    cli = Cli(Config())\n    cli.banner()\n\n    options: dict[str, dict[str, Any]] = {\n        \"cockroachdb\": {\n            \"description\": \"Manager a CockroachDB cluster\",\n            \"params\": [\"cluster\", \"<start | claim | delete>\"],\n            \"obj\": CockroachDBClusterManager,\n        },\n        \"quota\": {\n            \"description\": \"Check your quota\",\n            \"params\": [],\n            \"obj\": ApiQuotaManager,\n        },\n        \"setup\": {\n            \"description\": \"Execute suggested setup steps\",\n            \"params\": [],\n            \"obj\": SetupManager,\n        },\n        \"sign-up\": {\n            \"description\": \"Sign up for an API key\",\n            \"params\": [\"<email_address>\"],\n            \"obj\": ApiSignUpManager,\n        },\n    }\n\n    if len(sys.argv) <= 1 or sys.argv[1] not in options:\n        cli.print(\"{:<15}{:<45}{:<6}\".format(\"Option\", \"Description\", \"Params\"))\n        cli.print(\"{:<15}{:<45}{:<6}\".format(\"------\", \"-----------\", \"------\"))\n\n        for key, value in options.items():\n            params = value[\"params\"]\n            cli.print(\n                \"{:<15}{:<45}{:>6}\".format(\n                    key, value[\"description\"], \"Y\" if len(params) > 0 else \"N\"\n                )\n            )\n\n        cli.print(\"\\nusage: python -m memori <option> [params]\\n\")\n    else:\n        option = options[sys.argv[1]]\n        params = option[\"params\"]\n        obj_cls = option[\"obj\"]\n        if len(params) > 0:\n            if len(sys.argv) != 2 + len(params):\n                obj_cls(Config()).usage()\n                cli.newline()\n                sys.exit(1)\n\n        obj_cls(Config()).execute()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "memori/_cli.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport pyfiglet\n\nfrom memori._config import Config\n\n\nclass Cli:\n    def __init__(self, config: Config):\n        self.config = config\n\n    def banner(self):\n        self.print(pyfiglet.figlet_format(\"Memori\", font=\"standard\").rstrip())\n        self.print(\" \" * 18 + \"perfectam memoriam\")\n        self.print(\" \" * 23 + \"memorilabs.ai\")\n        self.print(\" \" * 30 + \"v\" + str(self.config.version) + \"\\n\")\n\n    def newline(self):\n        self.print(\"\")\n\n    def notice(self, message, ident=0, end=None):\n        prefix = \"+ \"\n        if ident > 0:\n            prefix = \"\"\n\n        self.print(prefix + \" \" * (ident * 4) + message, end=end)\n\n    def print(self, message, end=None):\n        print(message, end=end, flush=True)\n"
  },
  {
    "path": "memori/_config.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport os\nfrom concurrent.futures import ThreadPoolExecutor\nfrom importlib.metadata import version\n\n\ndef _env_bool(name: str, default: bool) -> bool:\n    raw = os.environ.get(name)\n    if raw is None:\n        return default\n    return str(raw).strip().lower() in {\"1\", \"true\", \"yes\", \"y\", \"on\"}\n\n\ndef _env_int(name: str, default: int) -> int:\n    raw = os.environ.get(name)\n    if raw is None:\n        return default\n    try:\n        return int(str(raw).strip())\n    except ValueError:\n        return default\n\n\ndef _env_str(name: str, default: str | None) -> str | None:\n    raw = os.environ.get(name)\n    if raw is None:\n        return default\n    raw = str(raw).strip()\n    return raw if raw else default\n\n\nclass Cache:\n    def __init__(self):\n        self.conversation_id = None\n        self.entity_id = None\n        self.process_id = None\n        self.session_id = None\n\n\nclass Storage:\n    def __init__(self):\n        self.cockroachdb = False\n\n\nclass Embeddings:\n    def __init__(self):\n        self.model = \"all-MiniLM-L6-v2\"\n\n\nclass Config:\n    def __init__(self):\n        self.api_key = None\n        self.augmentation = None\n        self.cache = Cache()\n        self.debug_truncate = True  # Truncate long content in debug logs\n        self.embeddings = Embeddings()\n        self.embeddings.model = (\n            _env_str(\"MEMORI_EMBEDDINGS_MODEL\", self.embeddings.model)\n            or self.embeddings.model\n        )\n        self.cloud: bool | None = None\n        self.llm = Llm()\n        self.framework = Framework()\n        self.platform = Platform()\n        self.entity_id = None\n        self.process_id = None\n        self.raise_final_request_attempt = True\n        self.recall_embeddings_limit = _env_int(\"MEMORI_RECALL_EMBEDDINGS_LIMIT\", 1000)\n        self.recall_facts_limit = 5\n        self.recall_relevance_threshold = 0.1\n        self.request_backoff_factor = 1\n        self.request_num_backoff = 5\n        self.request_secs_timeout = 5\n        self.session_id = None\n        self.session_timeout_minutes = 30\n        self.storage = None\n        self.storage_config = Storage()\n        self.thread_pool_executor = ThreadPoolExecutor(max_workers=15)\n        self.version = version(\"memori\")\n\n    def is_test_mode(self):\n        return os.environ.get(\"MEMORI_TEST_MODE\", None) is not None\n\n    def reset_cache(self):\n        self.cache = Cache()\n        return self\n\n\nclass Framework:\n    def __init__(self):\n        self.provider = None\n\n\nclass Platform:\n    def __init__(self):\n        self.provider = None\n\n\nclass Llm:\n    def __init__(self):\n        self.provider = None\n        self.provider_sdk_version = None\n        self.version = None\n"
  },
  {
    "path": "memori/_exceptions.py",
    "content": "r\"\"\"\n _  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport warnings\nfrom importlib.metadata import PackageNotFoundError, distribution\n\n\nclass QuotaExceededError(Exception):\n    def __init__(\n        self,\n        message=(\n            \"your IP address is over quota; register for an API key now: \"\n            + \"https://app.memorilabs.ai/signup\"\n        ),\n    ):\n        self.message = message\n        super().__init__(self.message)\n\n\nclass MemoriApiError(Exception):\n    pass\n\n\nclass MemoriApiClientError(MemoriApiError):\n    def __init__(\n        self,\n        status_code: int,\n        message: str | None = None,\n        details: object | None = None,\n    ):\n        self.status_code = status_code\n        self.details = details\n        super().__init__(\n            message or f\"Memori API request failed with status {status_code}\"\n        )\n\n\nclass MemoriApiValidationError(MemoriApiClientError):\n    pass\n\n\nclass MemoriApiRequestRejectedError(MemoriApiClientError):\n    pass\n\n\nclass MissingMemoriApiKeyError(RuntimeError):\n    \"\"\"Raised when cloud mode is used without a MEMORI_API_KEY.\"\"\"\n\n    def __init__(self, env_var: str = \"MEMORI_API_KEY\"):\n        self.env_var = env_var\n        super().__init__(\n            f\"A {env_var} is required to use the Memori cloud API. Sign up at https://app.memorilabs.ai/signup\"\n        )\n\n\nclass MissingPsycopgError(ImportError):\n    \"\"\"Raised when psycopg is required but not installed.\"\"\"\n\n    def __init__(self, database: str = \"PostgreSQL/CockroachDB\"):\n        super().__init__(\n            f\"psycopg is required for {database} support. \"\n            f\"Install it with: pip install 'memori[postgres]' or 'memori[cockroachdb]'\"\n        )\n\n\nclass UnsupportedLLMProviderError(RuntimeError):\n    \"\"\"Raised when an unsupported LLM provider is used.\"\"\"\n\n    def __init__(self, provider: str):\n        super().__init__(\n            f\"Unsupported LLM provider: {provider}. Please see the documentation for supported providers: https://memorilabs.ai/docs/features/llm\"\n        )\n\n\nclass UnsupportedDatabaseError(RuntimeError):\n    \"\"\"Raised when an unsupported database is used.\"\"\"\n\n    def __init__(self, database: str | None = None):\n        msg = (\n            \"Unsupported database.\"\n            if database is None\n            else f\"Unsupported database: {database}.\"\n        )\n        super().__init__(\n            f\"{msg} Please see the documentation for supported databases: https://memorilabs.ai/docs/features/databases\"\n        )\n\n\nclass MemoriLegacyPackageWarning(UserWarning):\n    \"\"\"Warning emitted when the legacy `memorisdk` package is installed.\"\"\"\n\n\ndef warn_if_legacy_memorisdk_installed() -> None:\n    try:\n        distribution(\"memorisdk\")\n    except PackageNotFoundError:\n        return\n\n    warnings.warn(\n        \"You have Memori installed under the legacy package name 'memorisdk'. \"\n        \"That name is deprecated and will stop receiving updates. \"\n        \"Please switch to 'memori':\\n\\n\"\n        \"    pip uninstall memorisdk\\n\"\n        \"    pip install memori\\n\",\n        MemoriLegacyPackageWarning,\n        stacklevel=3,\n    )\n"
  },
  {
    "path": "memori/_logging.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport copy\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n# Global setting for truncation (controlled by Config.debug_truncate)\n_truncate_enabled = True\n\n\ndef set_truncate_enabled(enabled: bool) -> None:\n    \"\"\"Set whether truncation is enabled for debug logs.\"\"\"\n    global _truncate_enabled\n    _truncate_enabled = enabled\n    logger.debug(\"Debug truncation %s\", \"enabled\" if enabled else \"disabled\")\n\n\ndef truncate(text: str, max_len: int = 200) -> str:\n    \"\"\"Truncate text for debug logging.\n\n    Respects the global _truncate_enabled setting. When disabled,\n    returns the full text regardless of length.\n\n    Args:\n        text: The text to truncate.\n        max_len: Maximum length before truncation (default: 200).\n\n    Returns:\n        Original text if truncation disabled or under max_len,\n        otherwise truncated with '...'\n    \"\"\"\n    if not text:\n        return text\n    if not _truncate_enabled:\n        return text\n    if len(text) <= max_len:\n        return text\n    return text[:max_len] + \"...\"\n\n\ndef sanitize_payload(payload: dict) -> dict:\n    \"\"\"Sanitize payload for safe logging by masking sensitive data.\n\n    Removes or masks:\n    - API keys\n    - Authorization tokens\n\n    Args:\n        payload: The payload dictionary to sanitize.\n\n    Returns:\n        A deep copy of the payload with sensitive data masked.\n    \"\"\"\n    sanitized = copy.deepcopy(payload)\n    if \"meta\" in sanitized and \"api\" in sanitized[\"meta\"]:\n        if sanitized[\"meta\"][\"api\"].get(\"key\"):\n            sanitized[\"meta\"][\"api\"][\"key\"] = \"***REDACTED***\"\n    return sanitized\n"
  },
  {
    "path": "memori/_network.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport asyncio\nimport logging\nimport os\nimport ssl\nfrom enum import Enum\n\nimport aiohttp\nimport certifi\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\n\nfrom memori._config import Config\nfrom memori._exceptions import (\n    MemoriApiClientError,\n    MemoriApiError,\n    MemoriApiRequestRejectedError,\n    MemoriApiValidationError,\n    QuotaExceededError,\n)\n\nlogger = logging.getLogger(__name__)\n\n\nclass ApiSubdomain(str, Enum):\n    DEFAULT = \"api\"\n    COLLECTOR = \"collector\"\n\n\nclass Api:\n    def __init__(self, config: Config, subdomain: ApiSubdomain = ApiSubdomain.DEFAULT):\n        test_mode = os.environ.get(\"MEMORI_TEST_MODE\") == \"1\"\n\n        self.__base = os.environ.get(\"MEMORI_API_URL_BASE\")\n\n        if self.__base is None:\n            if test_mode:\n                # Use staging for test mode\n                self.__x_api_key = \"c18b1022-7fe2-42af-ab01-b1f9139184f0\"\n                self.__base = f\"https://staging-{subdomain.value}.memorilabs.ai\"\n            else:\n                # Use production\n                self.__x_api_key = \"96a7ea3e-11c2-428c-b9ae-5a168363dc80\"\n                self.__base = f\"https://{subdomain.value}.memorilabs.ai\"\n        else:\n            # Custom URL provided, use staging key as default\n            self.__x_api_key = \"c18b1022-7fe2-42af-ab01-b1f9139184f0\"\n\n        self.config = config\n\n    async def augmentation_async(self, payload: dict) -> dict:\n        url = self.url(\"sdk/augmentation\")\n        headers = self.headers()\n        ssl_context = ssl.create_default_context(cafile=certifi.where())\n        logger.debug(\"Sending augmentation request to %s\", url)\n\n        def _default_client_error_message(status_code: int) -> str:\n            if status_code == 422:\n                return (\n                    \"Memori API rejected the request (422 validation error). \"\n                    \"Check your augmentation payload structure.\"\n                )\n            if status_code == 433:\n                return (\n                    \"The request was rejected (433). \"\n                    \"This can sometimes be caused by certificate/SSL inspection or proxy issues. \"\n                    \"If this persists, contact Memori Labs support via email at support@memorilabs.ai.\"\n                )\n            return f\"Memori API request failed with status {status_code}.\"\n\n        async def _read_error_payload(response: aiohttp.ClientResponse):\n            try:\n                data = await response.json()\n            except Exception:\n                return None, None\n\n            if isinstance(data, dict):\n                return data.get(\"message\") or data.get(\"detail\"), data\n            return None, data\n\n        async with aiohttp.ClientSession(\n            connector=aiohttp.TCPConnector(ssl=ssl_context)\n        ) as session:\n            try:\n                async with session.post(\n                    url,\n                    headers=headers,\n                    json=payload,\n                    timeout=aiohttp.ClientTimeout(total=30),\n                ) as r:\n                    logger.debug(\"Augmentation response - status: %d\", r.status)\n\n                    if r.status == 429:\n                        logger.warning(\"Rate limit exceeded (429)\")\n                        if self._is_anonymous():\n                            message, _data = await _read_error_payload(r)\n\n                            if message:\n                                raise QuotaExceededError(message)\n                            raise QuotaExceededError()\n                        else:\n                            return {}\n\n                    if r.status == 422:\n                        message, data = await _read_error_payload(r)\n                        logger.error(\"Validation error (422): %s\", message)\n                        raise MemoriApiValidationError(\n                            status_code=422,\n                            message=message or _default_client_error_message(422),\n                            details=data,\n                        )\n\n                    if r.status == 433:\n                        message, data = await _read_error_payload(r)\n                        logger.error(\"Request rejected (433): %s\", message)\n                        raise MemoriApiRequestRejectedError(\n                            status_code=433,\n                            message=message or _default_client_error_message(433),\n                            details=data,\n                        )\n\n                    if 400 <= r.status <= 499:\n                        message, data = await _read_error_payload(r)\n                        logger.error(\"Client error (%d): %s\", r.status, message)\n                        raise MemoriApiClientError(\n                            status_code=r.status,\n                            message=message or _default_client_error_message(r.status),\n                            details=data,\n                        )\n\n                    r.raise_for_status()\n                    logger.debug(\"Augmentation request successful\")\n                    return await r.json()\n            except aiohttp.ClientResponseError:\n                raise\n            except (ssl.SSLError, aiohttp.ClientSSLError) as e:\n                logger.error(\"SSL/TLS error during augmentation request: %s\", e)\n                raise MemoriApiError(\n                    \"Memori API request failed due to an SSL/TLS certificate error. \"\n                    \"This is often caused by corporate proxies/SSL inspection. \"\n                    \"Try updating your CA certificates and try again.\"\n                ) from e\n            except (aiohttp.ClientError, asyncio.TimeoutError) as e:\n                logger.error(\"Network/timeout error during augmentation request: %s\", e)\n                raise MemoriApiError(\n                    \"Memori API request failed (network/timeout). \"\n                    \"Check your connection and try again.\"\n                ) from e\n\n    def delete(self, route):\n        logger.debug(\"DELETE request to %s\", route)\n        r = self.__session().delete(self.url(route), headers=self.headers())\n        logger.debug(\"DELETE response - status: %d\", r.status_code)\n\n        r.raise_for_status()\n\n        return r.json()\n\n    def get(self, route):\n        logger.debug(\"GET request to %s\", route)\n        r = self.__session().get(self.url(route), headers=self.headers())\n        logger.debug(\"GET response - status: %d\", r.status_code)\n\n        r.raise_for_status()\n\n        return r.json()\n\n    async def get_async(self, route):\n        return await self.__request_async(\"GET\", route)\n\n    def patch(self, route, json=None):\n        logger.debug(\"PATCH request to %s\", route)\n        r = self.__session().patch(self.url(route), headers=self.headers(), json=json)\n        logger.debug(\"PATCH response - status: %d\", r.status_code)\n\n        r.raise_for_status()\n\n        return r.json()\n\n    async def patch_async(self, route, json=None):\n        return await self.__request_async(\"PATCH\", route, json=json)\n\n    def post(self, route, json=None, status_code: bool = False):\n        logger.debug(\"POST request to %s\", route)\n        r = self.__session().post(self.url(route), headers=self.headers(), json=json)\n        logger.debug(\"POST response - status: %d\", r.status_code)\n\n        if status_code:\n            return int(r.status_code)\n\n        r.raise_for_status()\n\n        return r.json()\n\n    async def post_async(self, route, json=None):\n        return await self.__request_async(\"POST\", route, json=json)\n\n    def headers(self):\n        headers = {\"X-Memori-API-Key\": self.__x_api_key}\n\n        api_key = os.environ.get(\"MEMORI_API_KEY\")\n        if api_key is not None:\n            headers[\"Authorization\"] = f\"Bearer {api_key}\"\n\n        return headers\n\n    def _is_anonymous(self):\n        return os.environ.get(\"MEMORI_API_KEY\") is None\n\n    async def __request_async(self, method: str, route: str, json=None):\n        url = self.url(route)\n        headers = self.headers()\n        attempts = 0\n        max_retries = 5\n        backoff_factor = 1\n\n        while True:\n            try:\n                async with aiohttp.ClientSession() as session:\n                    async with session.request(\n                        method.upper(),\n                        url,\n                        headers=headers,\n                        json=json,\n                        timeout=aiohttp.ClientTimeout(total=30),\n                    ) as r:\n                        logger.debug(\n                            \"Async %s response - status: %d, attempt: %d\",\n                            method.upper(),\n                            r.status,\n                            attempts + 1,\n                        )\n                        r.raise_for_status()\n                        return await r.json()\n            except aiohttp.ClientResponseError as e:\n                if e.status < 500 or e.status > 599:\n                    logger.error(\n                        \"Non-retryable error %d for %s %s\",\n                        e.status,\n                        method.upper(),\n                        url,\n                    )\n                    raise\n\n                if attempts >= max_retries:\n                    logger.error(\n                        \"Max retries (%d) exceeded for %s %s\",\n                        max_retries,\n                        method.upper(),\n                        url,\n                    )\n                    raise\n\n                sleep = backoff_factor * (2**attempts)\n                logger.debug(\n                    \"Retrying %s %s in %.1fs (attempt %d/%d) after status %d\",\n                    method.upper(),\n                    url,\n                    sleep,\n                    attempts + 2,\n                    max_retries,\n                    e.status,\n                )\n                await asyncio.sleep(sleep)\n                attempts += 1\n            except Exception as e:\n                if attempts >= max_retries:\n                    logger.error(\n                        \"Max retries (%d) exceeded for %s %s: %s\",\n                        max_retries,\n                        method.upper(),\n                        url,\n                        e,\n                    )\n                    raise\n\n                sleep = backoff_factor * (2**attempts)\n                logger.debug(\n                    \"Retrying %s %s in %.1fs (attempt %d/%d) after error: %s\",\n                    method.upper(),\n                    url,\n                    sleep,\n                    attempts + 2,\n                    max_retries,\n                    e,\n                )\n                await asyncio.sleep(sleep)\n                attempts += 1\n\n    def __session(self):\n        adapter = HTTPAdapter(\n            max_retries=_ApiRetryRecoverable(\n                allowed_methods=[\"GET\", \"PATCH\", \"POST\", \"PUT\", \"DELETE\"],\n                backoff_factor=1,\n                raise_on_status=False,\n                status=None,\n                total=5,\n            )\n        )\n\n        session = requests.Session()\n        session.mount(\"https://\", adapter)\n        session.mount(\"http://\", adapter)\n\n        return session\n\n    def url(self, route):\n        return f\"{self.__base}/v1/{route}\"\n\n\nclass _ApiRetryRecoverable(Retry):\n    def is_retry(self, method, status_code, has_retry_after=False):\n        return 500 <= status_code <= 599\n"
  },
  {
    "path": "memori/_setup.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom sentence_transformers import SentenceTransformer\n\nfrom memori._cli import Cli\nfrom memori._config import Config\n\n\nclass Manager:\n    def __init__(self, config: Config):\n        self.config = config\n\n    def execute(self):\n        cli = Cli(self.config)\n\n        cli.notice(\"Installing model all-mpnet-base-v2\")\n        cli.notice(\"this may take a moment; output to follow:\", 1)\n        cli.notice(\"-----\")\n\n        SentenceTransformer(\"all-mpnet-base-v2\")\n\n        cli.notice(\"-----\\n\")\n\n        return self\n"
  },
  {
    "path": "memori/_utils.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport hashlib\nimport json\nimport re\nfrom datetime import datetime, timezone\n\n\ndef bytes_to_json(obj):\n    if isinstance(obj, bytes):\n        obj = obj.decode()\n\n        if not isinstance(obj, str):\n            return obj\n\n        try:\n            return json.loads(obj)\n        except json.JSONDecodeError:\n            return obj\n    elif isinstance(obj, dict):\n        return {bytes_to_json(k): bytes_to_json(v) for k, v in obj.items()}\n    elif isinstance(obj, list):\n        return [bytes_to_json(i) for i in obj]\n    elif isinstance(obj, tuple):\n        return tuple(bytes_to_json(i) for i in obj)\n    elif isinstance(obj, set):\n        return {bytes_to_json(i) for i in obj}\n    else:\n        if not isinstance(obj, str):\n            return obj\n\n        try:\n            return json.loads(obj)\n        except json.JSONDecodeError:\n            return obj\n\n\ndef generate_uniq(terms: list):\n    if terms is None or len(terms) == 0:\n        return None\n\n    sha256 = hashlib.sha256()\n    sha256.update(re.sub(r\"[^a-z0-9]\", \"\", \"\".join(terms).lower()).encode(\"utf-8\"))\n\n    return sha256.hexdigest()\n\n\ndef merge_chunk(data: dict, chunk: dict):\n    for key, chunk_value in chunk.items():\n        if key in data:\n            data_value = data[key]\n\n            if isinstance(data_value, list) and isinstance(chunk_value, list):\n                data[key].extend(chunk_value)\n            elif isinstance(data_value, dict) and isinstance(chunk_value, dict):\n                merge_chunk(data_value, chunk_value)\n            else:\n                data[key] = chunk_value\n        else:\n            data[key] = chunk_value\n\n    return data\n\n\ndef format_date_created(value) -> str | None:\n    if value is None:\n        return None\n\n    if isinstance(value, datetime):\n        dt = value.astimezone(timezone.utc) if value.tzinfo else value\n        return dt.strftime(\"%Y-%m-%d %H:%M\")\n\n    if isinstance(value, str):\n        s = value.strip()\n        if not s:\n            return None\n        try:\n            normalized = s[:-1] + \"+00:00\" if s.endswith(\"Z\") else s\n            if \"T\" not in normalized and \" \" in normalized:\n                normalized = normalized.replace(\" \", \"T\", 1)\n            dt = datetime.fromisoformat(normalized)\n            dt = dt.astimezone(timezone.utc) if dt.tzinfo else dt\n            return dt.strftime(\"%Y-%m-%d %H:%M\")\n        except Exception:\n            if len(s) >= 16 and s[4] == \"-\" and s[7] == \"-\" and s[10] in (\"T\", \" \"):\n                return s[:16].replace(\"T\", \" \")\n            return None\n\n    return None\n"
  },
  {
    "path": "memori/api/_quota.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori._cli import Cli\nfrom memori._config import Config\nfrom memori._network import Api\n\n\nclass Manager:\n    def __init__(self, config: Config):\n        self.config = config\n\n    def execute(self):\n        cli = Cli(self.config)\n\n        response = Api(self.config).get(\"sdk/quota\")\n\n        cli.notice(\"Maximum # of Memories: \" + f\"{response['memories']['max']:,}\")\n        cli.notice(\"Current # of Memories: \" + f\"{response['memories']['num']:,}\\n\")\n        cli.notice(f\"{response['message']}\\n\")\n"
  },
  {
    "path": "memori/api/_sign_up.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport sys\n\nfrom requests.exceptions import HTTPError\n\nfrom memori._config import Config\nfrom memori._network import Api\n\n\nclass Manager:\n    def __init__(self, config: Config):\n        self.config = config\n\n    def execute(self):\n        try:\n            response = Api(self.config).post(\"sdk/account\", {\"email\": sys.argv[2]})\n            print(response.get(\"content\", \"You're all set! We sent you an email.\"))\n        except HTTPError as e:\n            if e.response.status_code != 422:\n                raise\n\n            print(f'The email you provided \"{sys.argv[2]}\" is not valid.')\n\n        print(\"\")\n\n    def usage(self):\n        print(\"python -m memori sign-up <email_address>\")\n"
  },
  {
    "path": "memori/embeddings/__init__.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\nEmbeddings utilities.\n\nThe public entrypoints are:\n- embed_texts\n- format_embedding_for_db\n\"\"\"\n\nfrom memori.embeddings._api import embed_texts\nfrom memori.embeddings._format import format_embedding_for_db\nfrom memori.embeddings._tei import TEI\n\n__all__ = [\"TEI\", \"embed_texts\", \"format_embedding_for_db\"]\n"
  },
  {
    "path": "memori/embeddings/_api.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nfrom collections.abc import Awaitable\nfrom functools import partial\nfrom typing import Literal, overload\n\nfrom memori.embeddings._sentence_transformers import get_sentence_transformers_embedder\nfrom memori.embeddings._tei import TEI\nfrom memori.embeddings._tei_embed import embed_texts_via_tei\nfrom memori.embeddings._utils import prepare_text_inputs\n\nlogger = logging.getLogger(__name__)\n_FALLBACK_DIMENSION = 768\n\n\ndef _embed_texts(\n    texts: str | list[str],\n    model: str,\n    *,\n    tei: TEI | None = None,\n    tokenizer: object | None = None,\n    chunk_size: int = 128,\n) -> list[list[float]]:\n    inputs = prepare_text_inputs(texts)\n    if not inputs:\n        logger.debug(\"embed_texts called with empty input\")\n        return []\n    if tei is not None:\n        return [\n            embed_texts_via_tei(\n                text=t,\n                model=model,\n                tei=tei,\n                tokenizer=tokenizer,\n                chunk_size=chunk_size,\n            )\n            for t in inputs\n        ]\n    return get_sentence_transformers_embedder(model).embed(\n        inputs, fallback_dimension=_FALLBACK_DIMENSION\n    )\n\n\nasync def _embed_texts_async(\n    texts: str | list[str],\n    model: str,\n    *,\n    tei: TEI | None = None,\n    tokenizer: object | None = None,\n    chunk_size: int = 128,\n) -> list[list[float]]:\n    loop = asyncio.get_event_loop()\n    fn = partial(\n        _embed_texts,\n        texts,\n        model,\n        tei=tei,\n        tokenizer=tokenizer,\n        chunk_size=chunk_size,\n    )\n    return await loop.run_in_executor(None, fn)\n\n\n@overload\ndef embed_texts(\n    texts: str | list[str],\n    model: str,\n    *,\n    async_: Literal[False] = False,\n    tei: TEI | None = None,\n    tokenizer: object | None = None,\n    chunk_size: int = 128,\n) -> list[list[float]]: ...\n\n\n@overload\ndef embed_texts(\n    texts: str | list[str],\n    model: str,\n    *,\n    async_: Literal[True],\n    tei: TEI | None = None,\n    tokenizer: object | None = None,\n    chunk_size: int = 128,\n) -> Awaitable[list[list[float]]]: ...\n\n\ndef embed_texts(\n    texts: str | list[str],\n    model: str,\n    *,\n    async_: bool = False,\n    tei: TEI | None = None,\n    tokenizer: object | None = None,\n    chunk_size: int = 128,\n) -> list[list[float]] | Awaitable[list[list[float]]]:\n    \"\"\"\n    Embed text(s) into vectors.\n\n    When async_=True, returns an awaitable that runs the work in a threadpool.\n    \"\"\"\n    if async_:\n        return _embed_texts_async(\n            texts, model, tei=tei, tokenizer=tokenizer, chunk_size=chunk_size\n        )\n    return _embed_texts(\n        texts, model, tei=tei, tokenizer=tokenizer, chunk_size=chunk_size\n    )\n"
  },
  {
    "path": "memori/embeddings/_chunking.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nlogger = logging.getLogger(__name__)\n\n\ndef chunk_text_by_tokens(\n    *,\n    text: str,\n    tokenizer: Any,\n    chunk_size: int,\n) -> list[str]:\n    \"\"\"\n    Chunk text by token count using a user-provided tokenizer.\n\n    Tokenizer requirements:\n    - callable: tokenizer(text, return_tensors=...) -> dict with \"input_ids\"\n    - decode: tokenizer.decode(ids_slice) -> str\n    \"\"\"\n    if chunk_size <= 0:\n        raise ValueError(\"chunk_size must be > 0\")\n\n    tokens = tokenizer(text, return_tensors=\"np\")\n    num_tokens = len(tokens[\"input_ids\"][0])\n\n    chunks = []\n    for i in range(0, num_tokens, chunk_size):\n        chunks.append(tokenizer.decode(tokens[\"input_ids\"][0][i : i + chunk_size]))\n\n    return chunks\n"
  },
  {
    "path": "memori/embeddings/_format.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport struct\nfrom typing import Any\n\n\ndef format_embedding_for_db(embedding: list[float], dialect: str) -> Any:\n    binary_data = struct.pack(f\"<{len(embedding)}f\", *embedding)\n\n    if dialect == \"mongodb\":\n        try:\n            import bson\n\n            return bson.Binary(binary_data)\n        except ImportError:\n            return binary_data\n    if dialect == \"oceanbase\":\n        try:\n            from pyobvector.util import Vector\n\n            return Vector._to_db(embedding)\n        except Exception:\n            return json.dumps(embedding)\n    return binary_data\n"
  },
  {
    "path": "memori/embeddings/_sentence_transformers.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport threading\nfrom typing import Any\n\nos.environ.setdefault(\"TOKENIZERS_PARALLELISM\", \"false\")\n\nimport numpy as np\nfrom sentence_transformers import SentenceTransformer\n\nfrom memori.embeddings._utils import embedding_dimension, zero_vectors\n\nlogger = logging.getLogger(__name__)\n\n\nclass SentenceTransformersEmbedder:\n    def __init__(self, model_name: str) -> None:\n        self._model_name = model_name\n        self._model: SentenceTransformer | None = None\n        self._model_lock = threading.Lock()\n        self._encode_lock = threading.Lock()\n\n    def _get_model(self) -> SentenceTransformer:\n        with self._model_lock:\n            if self._model is None:\n                self._model = SentenceTransformer(self._model_name)\n            return self._model\n\n    def _load_encoder(self, *, fallback_dimension: int) -> SentenceTransformer | None:\n        try:\n            return self._get_model()\n        except (OSError, RuntimeError, ValueError):\n            logger.debug(\n                \"Failed to load model %s, returning zero embeddings\", self._model_name\n            )\n            return None\n\n    def _encode_batch(\n        self, encoder: SentenceTransformer, inputs: list[str]\n    ) -> list[list[float]]:\n        with self._encode_lock:\n            embeddings = encoder.encode(\n                inputs, convert_to_numpy=True, normalize_embeddings=True\n            )\n        return embeddings.tolist()\n\n    def _encode_one_by_one(\n        self, encoder: SentenceTransformer, inputs: list[str]\n    ) -> list[list[float]]:\n        vectors: list[list[float]] = []\n        with self._encode_lock:\n            for text in inputs:\n                single = encoder.encode(\n                    [text], convert_to_numpy=True, normalize_embeddings=True\n                )\n                vectors.append(single[0].tolist())\n\n        dim_set = {len(v) for v in vectors}\n        if len(dim_set) != 1:\n            raise ValueError(\"all input arrays must have the same shape\")\n\n        return vectors\n\n    def _chunk_size_tokens(self, encoder: SentenceTransformer) -> int | None:\n        def _as_int(value: object) -> int | None:\n            try:\n                if isinstance(value, bool):\n                    return None\n                if isinstance(value, int):\n                    return value\n                if isinstance(value, float):\n                    return int(value)\n                if isinstance(value, str):\n                    return int(value.strip())\n            except Exception:\n                return None\n            return None\n\n        max_len: int | None = None\n        try:\n            max_len = _as_int(encoder.get_max_seq_length())\n        except Exception:\n            max_len = None\n\n        if max_len is None:\n            try:\n                raw = getattr(encoder, \"max_seq_length\", None)\n                max_len = _as_int(raw)\n            except Exception:\n                max_len = None\n\n        if max_len is None or max_len <= 0:\n            return None\n\n        # We chunk without special tokens; reserve a small budget so the model\n        # can still add special tokens without truncation.\n        return max(1, int(max_len) - 2)\n\n    def _tokenizer(self, encoder: SentenceTransformer) -> Any | None:\n        return getattr(encoder, \"tokenizer\", None)\n\n    def _chunk_text(\n        self, *, encoder: SentenceTransformer, text: str, chunk_size_tokens: int\n    ) -> list[str]:\n        tokenizer = self._tokenizer(encoder)\n        if tokenizer is None:\n            return [text]\n\n        try:\n            encoded = tokenizer(\n                text,\n                add_special_tokens=False,\n                return_attention_mask=False,\n                return_token_type_ids=False,\n            )\n            ids = encoded.get(\"input_ids\") if isinstance(encoded, dict) else None\n            if not isinstance(ids, list):\n                return [text]\n        except Exception:\n            return [text]\n\n        if len(ids) <= chunk_size_tokens:\n            return [text]\n\n        chunks: list[str] = []\n        for i in range(0, len(ids), chunk_size_tokens):\n            chunk_ids = ids[i : i + chunk_size_tokens]\n            chunk_text = tokenizer.decode(\n                chunk_ids,\n                skip_special_tokens=True,\n                clean_up_tokenization_spaces=True,\n            )\n            if chunk_text:\n                chunks.append(chunk_text)\n\n        return chunks or [text]\n\n    def _mean_pool_and_normalize(self, vectors: np.ndarray) -> np.ndarray:\n        mean_vec = vectors.mean(axis=0)\n        norm = float(np.linalg.norm(mean_vec))\n        if norm > 0.0:\n            mean_vec = mean_vec / norm\n        return mean_vec\n\n    def _encode_chunks(\n        self,\n        *,\n        encoder: SentenceTransformer,\n        chunks: list[str],\n    ) -> list[float]:\n        if len(chunks) == 1:\n            return self._encode_batch(encoder, chunks)[0]\n\n        with self._encode_lock:\n            chunk_vectors = encoder.encode(\n                chunks, convert_to_numpy=True, normalize_embeddings=True\n            )\n        pooled = self._mean_pool_and_normalize(\n            np.asarray(chunk_vectors, dtype=np.float32)\n        )\n        return pooled.tolist()\n\n    def _encode_inputs(\n        self,\n        *,\n        encoder: SentenceTransformer,\n        inputs: list[str],\n        chunk_size_tokens: int | None,\n    ) -> list[list[float]]:\n        if not inputs:\n            return []\n        if chunk_size_tokens is None:\n            return self._encode_batch(encoder, inputs)\n\n        short_inputs: list[str] = []\n        short_positions: list[int] = []\n        long_items: list[tuple[int, list[str]]] = []\n\n        for idx, text in enumerate(inputs):\n            chunks = self._chunk_text(\n                encoder=encoder, text=text, chunk_size_tokens=chunk_size_tokens\n            )\n            if len(chunks) == 1:\n                short_inputs.append(text)\n                short_positions.append(idx)\n            else:\n                long_items.append((idx, chunks))\n\n        out: list[list[float]] = [[] for _ in inputs]\n\n        if short_inputs:\n            short_vecs = self._encode_batch(encoder, short_inputs)\n            for pos, vec in zip(short_positions, short_vecs, strict=False):\n                out[pos] = vec\n\n        for pos, chunks in long_items:\n            out[pos] = self._encode_chunks(encoder=encoder, chunks=chunks)\n\n        return out\n\n    def _zero_result(\n        self,\n        *,\n        count: int,\n        fallback_dimension: int,\n        encoder: SentenceTransformer | None,\n    ) -> list[list[float]]:\n        dim = (\n            embedding_dimension(encoder, default=fallback_dimension)\n            if encoder is not None\n            else fallback_dimension\n        )\n        logger.warning(\n            \"Embedding encode failed for model=%s, returning zero embeddings of dim %d\",\n            self._model_name,\n            dim,\n        )\n        return zero_vectors(count, dim)\n\n    def embed(self, inputs: list[str], *, fallback_dimension: int) -> list[list[float]]:\n        if not inputs:\n            return []\n\n        logger.debug(\n            \"Generating embedding using model: %s for %d text(s)\",\n            self._model_name,\n            len(inputs),\n        )\n\n        encoder = self._load_encoder(fallback_dimension=fallback_dimension)\n        if encoder is None:\n            return zero_vectors(len(inputs), fallback_dimension)\n\n        try:\n            result = self._encode_inputs(\n                encoder=encoder,\n                inputs=inputs,\n                chunk_size_tokens=self._chunk_size_tokens(encoder),\n            )\n            if result:\n                logger.debug(\n                    \"Embedding generated - dimension: %d, count: %d\",\n                    len(result[0]),\n                    len(result),\n                )\n            return result\n        except ValueError as e:\n            if \"same shape\" not in str(e):\n                raise\n\n            try:\n                vectors = self._encode_one_by_one(encoder, inputs)\n                if vectors:\n                    logger.debug(\n                        \"Embedding generated (one-by-one) - dimension: %d, count: %d\",\n                        len(vectors[0]),\n                        len(vectors),\n                    )\n                return vectors\n            except Exception:\n                return self._zero_result(\n                    count=len(inputs),\n                    fallback_dimension=fallback_dimension,\n                    encoder=encoder,\n                )\n        except RuntimeError:\n            return self._zero_result(\n                count=len(inputs),\n                fallback_dimension=fallback_dimension,\n                encoder=encoder,\n            )\n\n\n_EMBEDDER_CACHE: dict[str, SentenceTransformersEmbedder] = {}\n_EMBEDDER_CACHE_LOCK = threading.Lock()\n\n\ndef get_sentence_transformers_embedder(model_name: str) -> SentenceTransformersEmbedder:\n    with _EMBEDDER_CACHE_LOCK:\n        embedder = _EMBEDDER_CACHE.get(model_name)\n        if embedder is None:\n            embedder = SentenceTransformersEmbedder(model_name)\n            _EMBEDDER_CACHE[model_name] = embedder\n        return embedder\n"
  },
  {
    "path": "memori/embeddings/_tei.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\n\nimport requests\n\n\n@dataclass(frozen=True, slots=True)\nclass TEI:\n    url: str\n    timeout: int | None = 30\n    headers: dict[str, str] | None = None\n\n    def _request_headers(self) -> dict[str, str]:\n        base = {\"Content-Type\": \"application/json\"}\n        if self.headers:\n            base.update(self.headers)\n        return base\n\n    def _post_embeddings(self, inputs: list[str], *, model: str) -> list[list[float]]:\n        r = requests.post(\n            self.url,\n            headers=self._request_headers(),\n            json={\"input\": inputs, \"model\": model},\n            timeout=self.timeout,\n        )\n        r.raise_for_status()\n        try:\n            payload = r.json()\n            data = payload[\"data\"]\n            if not isinstance(data, list):\n                raise TypeError\n            return [item[\"embedding\"] for item in data]\n        except Exception as e:\n            raise ValueError(\"Invalid TEI response payload\") from e\n\n    def embed(self, texts: list[str], *, model: str) -> list[list[float]]:\n        if not texts:\n            return []\n        return self._post_embeddings(texts, model=model)\n"
  },
  {
    "path": "memori/embeddings/_tei_embed.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nimport numpy as np\n\nfrom memori.embeddings._chunking import chunk_text_by_tokens\nfrom memori.embeddings._tei import TEI\n\nlogger = logging.getLogger(__name__)\n\n\ndef embed_texts_via_tei(\n    *,\n    text: str,\n    model: str,\n    tei: TEI,\n    tokenizer: Any | None = None,\n    chunk_size: int = 128,\n) -> list[float]:\n    \"\"\"\n    Embed a single text using a TEI-compatible server.\n\n    If a tokenizer is provided, texts are chunked by token count, then chunk\n    embeddings are mean-pooled and L2-normalized back to 1 vector.\n    \"\"\"\n    if not text:\n        return []\n\n    if tokenizer is None:\n        logger.debug(\"embed_texts_via_tei called with no tokenizer\")\n        return tei.embed([text], model=model)[0]\n\n    chunks = chunk_text_by_tokens(text=text, tokenizer=tokenizer, chunk_size=chunk_size)\n    chunk_vecs = tei.embed(chunks, model=model)\n    if len(chunk_vecs) != len(chunks):\n        raise ValueError(\"TEI response count does not match input count\")\n\n    if len(chunk_vecs) == 1:\n        return chunk_vecs[0]\n\n    embeddings = np.array(chunk_vecs, dtype=np.float32)\n    mean_vec = embeddings.mean(axis=0)\n    norm = float(np.linalg.norm(mean_vec))\n    if norm > 0.0:\n        mean_vec = mean_vec / norm\n    return mean_vec.tolist()\n"
  },
  {
    "path": "memori/embeddings/_utils.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom typing import Any\n\n\ndef prepare_text_inputs(texts: str | Iterable[str]) -> list[str]:\n    if isinstance(texts, str):\n        return [texts]\n    return [t for t in texts if t]\n\n\ndef embedding_dimension(model: Any, default: int) -> int:\n    try:\n        dim_value = model.get_sentence_embedding_dimension()\n        return int(dim_value) if dim_value is not None else default\n    except (RuntimeError, ValueError, AttributeError, TypeError):\n        return default\n\n\ndef zero_vectors(count: int, dim: int) -> list[list[float]]:\n    return [[0.0] * dim for _ in range(count)]\n"
  },
  {
    "path": "memori/llm/__init__.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.llm import _clients  # noqa: F401\nfrom memori.llm._registry import Registry\nfrom memori.llm.adapters import anthropic, bedrock, google, openai, xai  # noqa: F401\n\n__all__ = [\"Registry\"]\n"
  },
  {
    "path": "memori/llm/_base.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport copy\nimport inspect\nimport json\nimport logging\nfrom collections.abc import Mapping\nfrom typing import Any, cast\n\nfrom google.protobuf import json_format\n\nfrom memori._config import Config\nfrom memori._logging import truncate\nfrom memori._utils import format_date_created, merge_chunk\nfrom memori.llm._utils import (\n    agno_is_anthropic,\n    agno_is_google,\n    agno_is_openai,\n    agno_is_xai,\n    llm_is_anthropic,\n    llm_is_bedrock,\n    llm_is_google,\n    llm_is_openai,\n    llm_is_xai,\n    provider_is_langchain,\n)\nfrom memori.memory.augmentation.augmentations.memori.models import (\n    AttributionData,\n    AugmentationInputData,\n    ConversationMessage,\n    EntityData,\n    ProcessData,\n    SessionData,\n)\nfrom memori.search._types import FactSearchResult\n\nlogger = logging.getLogger(__name__)\n\n\ndef _score_for_recall_threshold(\n    fact: FactSearchResult | Mapping[str, object] | str,\n) -> float:\n    \"\"\"\n    Extract a numeric score for recall relevance thresholding.\n\n    Prefer rank_score when present; fall back to similarity.\n    Plain strings (from cloud recall API) are treated as fully relevant.\n    \"\"\"\n    if isinstance(fact, str):\n        return 1.0\n    if isinstance(fact, Mapping):\n        fact_map = cast(Mapping[str, object], fact)\n        raw = fact_map.get(\"rank_score\")\n        if raw is None:\n            raw = fact_map.get(\"similarity\", 0.0)\n    else:\n        raw = fact.rank_score\n    if raw is None:\n        return 0.0\n    if isinstance(raw, (int, float)):\n        return float(raw)\n    try:\n        return float(cast(Any, raw))\n    except (TypeError, ValueError):\n        return 0.0\n\n\nclass BaseClient:\n    def __init__(self, config: Config):\n        self.config = config\n        self.stream = False\n\n    def register(self, *args, **kwargs):\n        raise NotImplementedError(\"Subclasses must implement register()\")\n\n    def _wrap_method(\n        self,\n        obj,\n        method_name,\n        backup_obj,\n        backup_attr,\n        provider,\n        llm_provider,\n        version,\n        stream=False,\n    ):\n        \"\"\"Helper to wrap a method with the appropriate Invoke wrapper.\n\n        Automatically detects async context and chooses the correct wrapper class.\n\n        Args:\n            obj: The object containing the method to wrap (e.g., client.chat.completions)\n            method_name: Name of the method to wrap (e.g., 'create')\n            backup_obj: The object where backup is stored (e.g., client.chat)\n            backup_attr: Name of backup attribute where original is stored (e.g., '_completions_create')\n            provider: Framework provider name\n            llm_provider: LLM provider name\n            version: Provider SDK version\n            stream: Whether to use streaming wrappers\n        \"\"\"\n        from memori.llm._invoke import (\n            Invoke,\n            InvokeAsync,\n            InvokeAsyncStream,\n            InvokeStream,\n        )\n\n        original = getattr(backup_obj, backup_attr)\n\n        is_async = inspect.iscoroutinefunction(original) or type(\n            obj\n        ).__name__.startswith(\"Async\")\n\n        if is_async:\n            wrapper_class = InvokeAsyncStream if stream else InvokeAsync\n        else:\n            wrapper_class = InvokeStream if stream else Invoke\n\n        setattr(\n            obj,\n            method_name,\n            wrapper_class(self.config, original)\n            .set_client(provider, llm_provider, version)\n            .invoke,\n        )\n\n\nclass BaseInvoke:\n    def __init__(self, config: Config, method):\n        self.config = config\n        self._method = method\n        self._uses_protobuf = False\n        self._injected_message_count = 0\n        self._cloud_conversation_messages: list[dict[str, str]] = []\n\n    def _ensure_cached_conversation_id(self) -> bool:\n        if self.config.storage is None or self.config.storage.driver is None:\n            return False\n\n        if self.config.session_id is None:\n            return False\n\n        driver = self.config.storage.driver\n\n        if self.config.cache.session_id is None:\n            if not hasattr(driver.session, \"read\"):\n                return False\n            self.config.cache.session_id = driver.session.read(\n                str(self.config.session_id)\n            )\n\n        if self.config.cache.session_id is None:\n            return False\n\n        if self.config.cache.conversation_id is None:\n            if not hasattr(driver.conversation, \"read_id_by_session_id\"):\n                return False\n            self.config.cache.conversation_id = (\n                driver.conversation.read_id_by_session_id(self.config.cache.session_id)\n            )\n\n        return self.config.cache.conversation_id is not None\n\n    def configure_for_streaming_usage(self, kwargs: dict) -> dict:\n        if (\n            llm_is_openai(self.config.framework.provider, self.config.llm.provider)\n            or llm_is_xai(self.config.framework.provider, self.config.llm.provider)\n            or agno_is_openai(self.config.framework.provider, self.config.llm.provider)\n        ):\n            is_responses_api = (\n                \"input\" in kwargs or \"instructions\" in kwargs\n            ) and \"messages\" not in kwargs\n\n            if kwargs.get(\"stream\", None) and not is_responses_api:\n                stream_options = kwargs.get(\"stream_options\", None)\n                if stream_options is None or not isinstance(stream_options, dict):\n                    kwargs[\"stream_options\"] = {}\n\n                kwargs[\"stream_options\"][\"include_usage\"] = True\n\n        return kwargs\n\n    def _convert_to_json(self, obj, _seen=None):\n        \"\"\"Recursively convert objects to JSON-serializable format.\n\n        Uses a seen set to prevent infinite recursion from circular references.\n        \"\"\"\n        if _seen is None:\n            _seen = set()\n\n        obj_id = id(obj)\n        if obj_id in _seen:\n            return None\n        _seen.add(obj_id)\n\n        try:\n            if obj is None or isinstance(obj, (bool, int, float, str)):\n                return obj\n            elif isinstance(obj, list):\n                return [self._convert_to_json(item, _seen.copy()) for item in obj]\n            elif isinstance(obj, dict):\n                return {\n                    key: self._convert_to_json(value, _seen.copy())\n                    for key, value in obj.items()\n                    if not key.startswith(\"_\")\n                }\n            elif hasattr(obj, \"model_dump\"):\n                try:\n                    return obj.model_dump()\n                except Exception:\n                    pass\n            elif hasattr(obj, \"__dict__\"):\n                filtered_dict = {\n                    k: v\n                    for k, v in obj.__dict__.items()\n                    if not k.startswith(\"_\") and not callable(v)\n                }\n                if filtered_dict:\n                    return self._convert_to_json(filtered_dict, _seen.copy())\n                return None\n            return obj\n        except Exception:\n            return None\n\n    def dict_to_json(self, dict_: dict) -> dict:\n        return self._convert_to_json(dict_)\n\n    def _format_kwargs(self, kwargs):\n        if self._uses_protobuf:\n            if \"request\" in kwargs:\n                formatted_kwargs = json.loads(\n                    json_format.MessageToJson(kwargs[\"request\"].__dict__[\"_pb\"])\n                )\n            else:\n                formatted_kwargs = copy.deepcopy(kwargs)\n                formatted_kwargs = self.dict_to_json(formatted_kwargs)\n        else:\n            formatted_kwargs = copy.deepcopy(kwargs)\n            if provider_is_langchain(self.config.framework.provider):\n                if \"response_format\" in formatted_kwargs and isinstance(\n                    formatted_kwargs[\"response_format\"], object\n                ):\n                    \"\"\"\n                    We are likely processing the result of LangChain's structured\n                    output runnable. The object defined in \"response_format\" is\n                    recursive (it refers to itself) so formatting it into a dictionary\n                    will result in an RecursionError. We also do not need the data in\n                    this object so we are going to discard it here.\n                    \"\"\"\n\n                    del formatted_kwargs[\"response_format\"]\n\n            formatted_kwargs = self.dict_to_json(formatted_kwargs)\n\n        if self._injected_message_count > 0:\n            formatted_kwargs[\"_memori_injected_count\"] = self._injected_message_count\n\n        return formatted_kwargs\n\n    def _format_payload(\n        self,\n        client_provider,\n        client_title,\n        client_version,\n        start_time,\n        end_time,\n        query,\n        response,\n    ):\n        response_json = self._convert_to_json(response)\n\n        from memori.memory._conversation_messages import (  # noqa: I001\n            parse_payload_conversation_messages,\n        )\n\n        payload: dict[str, Any] = {\n            \"attribution\": {\n                \"entity\": {\"id\": self.config.entity_id},\n                \"process\": {\"id\": self.config.process_id},\n            },\n            \"conversation\": {\n                \"client\": {\n                    \"provider\": client_provider,\n                    \"title\": client_title,\n                    \"version\": client_version,\n                },\n                # Keep query/response for backwards compatibility with adapters/augmentation.\n                \"query\": query,\n                \"response\": response_json,\n            },\n            \"meta\": {\n                \"api\": {\"key\": self.config.api_key},\n                \"fnfg\": {\n                    \"exc\": None,\n                    \"status\": \"succeeded\",\n                },\n                \"sdk\": {\"client\": \"python\", \"version\": self.config.version},\n            },\n            \"session\": {\"uuid\": str(self.config.session_id)},\n            \"time\": {\"end\": end_time, \"start\": start_time},\n        }\n\n        messages = list(parse_payload_conversation_messages(payload))\n        payload[\"messages\"] = messages\n\n        if self.config.cloud is True:\n            return {\n                \"attribution\": {\n                    \"entity\": {\"id\": self.config.entity_id},\n                    \"process\": {\"id\": self.config.process_id},\n                },\n                \"messages\": messages,\n                \"session\": {\"id\": str(self.config.session_id)},\n            }\n\n        return payload\n\n    def _format_augmentation_input(self, payload: dict) -> AugmentationInputData:\n        return AugmentationInputData(\n            attribution=AttributionData(\n                entity=EntityData(id=self.config.entity_id),\n                process=ProcessData(id=self.config.process_id),\n            ),\n            messages=[\n                ConversationMessage(\n                    role=message.get(\"role\"), content=message.get(\"text\")\n                )\n                for message in payload.get(\"messages\", [])\n            ],\n            session=SessionData(id=str(self.config.session_id)),\n        )\n\n    def _safe_copy(self, obj):\n        \"\"\"Safely copy an object, handling unpicklable types like _thread.RLock.\n\n        Falls back to alternative copy methods if deepcopy fails.\n        \"\"\"\n        try:\n            return copy.deepcopy(obj)\n        except (TypeError, AttributeError):\n            pass\n\n        if isinstance(obj, list):\n            result = []\n            for item in obj:\n                result.append(self._safe_copy(item))\n            return result\n\n        if isinstance(obj, dict):\n            result = {}\n            for key, value in obj.items():\n                result[key] = self._safe_copy(value)\n            return result\n\n        if hasattr(obj, \"model_dump\"):\n            try:\n                return obj.model_dump()\n            except Exception:\n                pass\n\n        if hasattr(obj, \"to_dict\"):\n            try:\n                return obj.to_dict()\n            except Exception:\n                pass\n\n        if hasattr(obj, \"__dict__\"):\n            try:\n                return copy.copy(obj)\n            except Exception:\n                pass\n\n        return obj\n\n    def _format_response(self, raw_response):\n        formatted_response = self._safe_copy(raw_response)\n        if self._uses_protobuf:\n            if not isinstance(formatted_response, list):\n                if (\n                    hasattr(formatted_response, \"__dict__\")\n                    and \"_pb\" in formatted_response.__dict__\n                ):\n                    # Old google-generativeai format (protobuf)\n                    formatted_response = json.loads(\n                        json_format.MessageToJson(formatted_response.__dict__[\"_pb\"])\n                    )\n                elif hasattr(formatted_response, \"candidates\"):\n                    # New google-genai format (dict with candidates)\n                    result = {}\n                    if formatted_response.candidates:\n                        candidates = []\n                        for candidate in formatted_response.candidates:\n                            candidate_data = {}\n                            if hasattr(candidate, \"content\") and candidate.content:\n                                content_data = {}\n                                if (\n                                    hasattr(candidate.content, \"parts\")\n                                    and candidate.content.parts\n                                ):\n                                    parts = []\n                                    for part in candidate.content.parts:\n                                        if hasattr(part, \"text\"):\n                                            parts.append({\"text\": part.text})\n                                    content_data[\"parts\"] = parts\n                                if hasattr(candidate.content, \"role\"):\n                                    content_data[\"role\"] = candidate.content.role\n                                candidate_data[\"content\"] = content_data\n                            candidates.append(candidate_data)\n                        result[\"candidates\"] = candidates\n                    formatted_response = result\n                else:\n                    formatted_response = {}\n\n        return formatted_response\n\n    def get_response_content(self, raw_response):\n        if (\n            raw_response.__class__.__name__ == \"LegacyAPIResponse\"\n            and raw_response.__class__.__module__ == \"openai._legacy_response\"\n        ):\n            \"\"\"\n            Library: langchain-openai\n            Version: > 0.3.31\n\n            Calling the chat / invoke method of the client no longer returns the JSON\n            response but instead an object that looks like an API response. This\n            object does not inherit from a base class we can reliably identify and\n            we do not want to force the OpenAI library as a dependency.\n            \"\"\"\n\n            return json.loads(raw_response.text)\n\n        if hasattr(raw_response, \"output\") and hasattr(raw_response, \"output_text\"):\n            if hasattr(raw_response, \"model_dump\"):\n                return raw_response.model_dump()\n            elif hasattr(raw_response, \"__dict__\"):\n                return self._convert_to_json(raw_response)\n\n        return raw_response\n\n    def _extract_text_from_parts(self, parts: list) -> str:\n        \"\"\"Extract text from a list of parts (Google format).\"\"\"\n        text_parts = []\n        for part in parts:\n            if isinstance(part, str):\n                text_parts.append(part)\n            elif isinstance(part, dict) and \"text\" in part:\n                text_parts.append(part[\"text\"])\n            elif hasattr(part, \"text\") and part.text:\n                text_parts.append(part.text)\n        return \" \".join(text_parts) if text_parts else \"\"\n\n    def _extract_from_contents(self, contents) -> str:\n        \"\"\"Extract user query from Google's contents format.\"\"\"\n        if isinstance(contents, str):\n            return contents\n\n        if isinstance(contents, list):\n            for content in reversed(contents):\n                if isinstance(content, str):\n                    return content\n                elif isinstance(content, dict) and content.get(\"role\") == \"user\":\n                    text = self._extract_text_from_parts(content.get(\"parts\", []))\n                    if text:\n                        return text\n                elif getattr(content, \"role\", None) == \"user\":\n                    text = self._extract_text_from_parts(getattr(content, \"parts\", []))\n                    if text:\n                        return text\n        return \"\"\n\n    def _extract_user_query(self, kwargs: dict) -> str:\n        \"\"\"Extract the most recent user message from kwargs.\"\"\"\n        if \"messages\" in kwargs and kwargs[\"messages\"]:\n            for msg in reversed(kwargs[\"messages\"]):\n                if msg.get(\"role\") == \"user\":\n                    return msg.get(\"content\", \"\")\n\n        if \"input\" in kwargs:\n            input_val = kwargs.get(\"input\", \"\")\n            if isinstance(input_val, str):\n                return input_val\n            if isinstance(input_val, list):\n                for item in reversed(input_val):\n                    if isinstance(item, dict) and item.get(\"role\") == \"user\":\n                        content = item.get(\"content\", \"\")\n                        if isinstance(content, str):\n                            return content\n                        if isinstance(content, list):\n                            for c in content:\n                                if (\n                                    isinstance(c, dict)\n                                    and c.get(\"type\") == \"input_text\"\n                                ):\n                                    return c.get(\"text\", \"\")\n                                if isinstance(c, str):\n                                    return c\n        if \"contents\" in kwargs:\n            result = self._extract_from_contents(kwargs[\"contents\"])\n            if result:\n                return result\n\n        if \"request\" in kwargs:\n            try:\n                formatted_kwargs = json.loads(\n                    json_format.MessageToJson(kwargs[\"request\"].__dict__[\"_pb\"])\n                )\n                if \"contents\" in formatted_kwargs:\n                    return self._extract_from_contents(formatted_kwargs[\"contents\"])\n            except Exception:\n                pass\n\n        return \"\"\n\n    def _append_to_google_system_instruction_dict(self, config: dict, context: str):\n        \"\"\"Append context to system_instruction in a dict config.\"\"\"\n        if \"system_instruction\" not in config or not config[\"system_instruction\"]:\n            config[\"system_instruction\"] = context.lstrip(\"\\n\")\n            return\n\n        existing = config[\"system_instruction\"]\n\n        if isinstance(existing, str):\n            config[\"system_instruction\"] = existing + context\n        elif isinstance(existing, list):\n            self._append_to_list(existing, context, config, \"system_instruction\")\n        elif isinstance(existing, dict):\n            self._append_to_content_dict(\n                existing, context, config, \"system_instruction\"\n            )\n        else:\n            config[\"system_instruction\"] = context.lstrip(\"\\n\")\n\n    def _append_to_google_system_instruction_obj(self, config, context: str):\n        \"\"\"Append context to system_instruction in a config object.\"\"\"\n        if not hasattr(config, \"system_instruction\"):\n            return\n\n        if config.system_instruction is None:\n            config.system_instruction = context.lstrip(\"\\n\")\n        elif isinstance(config.system_instruction, str):\n            config.system_instruction = config.system_instruction + context\n        elif isinstance(config.system_instruction, list):\n            self._append_to_list_obj(config, context)\n        elif hasattr(config.system_instruction, \"text\"):\n            self._append_to_part_obj(config.system_instruction, context)\n        elif hasattr(config.system_instruction, \"parts\"):\n            self._append_to_content_obj(config.system_instruction, context)\n        else:\n            config.system_instruction = context.lstrip(\"\\n\")\n\n    def _append_to_list(self, lst: list, context: str, parent: dict, key: str):\n        \"\"\"Append context to a list (handles list[str], list[dict], empty list).\"\"\"\n        if not lst:\n            parent[key] = [{\"text\": context.lstrip(\"\\n\")}]\n        elif isinstance(lst[0], dict) and \"text\" in lst[0]:\n            lst[0][\"text\"] += context\n        elif isinstance(lst[0], str):\n            lst[0] += context\n        else:\n            lst.insert(0, {\"text\": context.lstrip(\"\\n\")})\n\n    def _append_to_list_obj(self, config, context: str):\n        \"\"\"Append context to a list in config object.\"\"\"\n        lst = config.system_instruction\n        if not lst:\n            config.system_instruction = context.lstrip(\"\\n\")\n        elif hasattr(lst[0], \"text\"):\n            lst[0].text += context\n        elif isinstance(lst[0], str):\n            lst[0] += context\n        else:\n            config.system_instruction = context.lstrip(\"\\n\")\n\n    def _append_to_content_dict(\n        self, content: dict, context: str, parent: dict, key: str\n    ):\n        \"\"\"Append context to a Content dict (has 'parts') or Part dict (has 'text').\"\"\"\n        if \"parts\" in content:\n            parts = content.get(\"parts\", [])\n            if parts and isinstance(parts[0], dict) and \"text\" in parts[0]:\n                parts[0][\"text\"] += context\n            else:\n                if not content.get(\"parts\"):\n                    content[\"parts\"] = []\n                content[\"parts\"].insert(0, {\"text\": context.lstrip(\"\\n\")})\n        elif \"text\" in content:\n            content[\"text\"] += context\n        else:\n            parent[key] = context.lstrip(\"\\n\")\n\n    def _append_to_part_obj(self, part, context: str):\n        \"\"\"Append context to a Part object.\"\"\"\n        if part.text:\n            part.text += context\n        else:\n            part.text = context.lstrip(\"\\n\")\n\n    def _append_to_content_obj(self, content, context: str):\n        \"\"\"Append context to a Content object.\"\"\"\n        if (\n            content.parts\n            and len(content.parts) > 0\n            and hasattr(content.parts[0], \"text\")\n        ):\n            if content.parts[0].text:\n                content.parts[0].text += context\n            else:\n                content.parts[0].text = context.lstrip(\"\\n\")\n\n    def _inject_google_system_instruction(self, kwargs: dict, context: str):\n        \"\"\"Inject recall context into Google/Gemini system_instruction.\"\"\"\n        if \"request\" in kwargs:\n            formatted_kwargs = json.loads(\n                json_format.MessageToJson(kwargs[\"request\"].__dict__[\"_pb\"])\n            )\n            system_instruction = formatted_kwargs.get(\"systemInstruction\", {})\n            parts = system_instruction.get(\"parts\", [])\n            if parts and isinstance(parts[0], dict) and \"text\" in parts[0]:\n                parts[0][\"text\"] += context\n            else:\n                system_instruction[\"parts\"] = [{\"text\": context.lstrip(\"\\n\")}]\n            formatted_kwargs[\"systemInstruction\"] = system_instruction\n            json_format.ParseDict(formatted_kwargs, kwargs[\"request\"].__dict__[\"_pb\"])\n        else:\n            config = kwargs.get(\"config\", None)\n            if config is None:\n                kwargs[\"config\"] = {\"system_instruction\": context.lstrip(\"\\n\")}\n            elif isinstance(config, dict):\n                self._append_to_google_system_instruction_dict(config, context)\n            else:\n                self._append_to_google_system_instruction_obj(config, context)\n\n    def _format_recalled_fact_lines(\n        self, facts: list[FactSearchResult | Mapping[str, object] | str]\n    ) -> list[str]:\n        lines: list[str] = []\n        for fact in facts:\n            if isinstance(fact, str):\n                if fact:\n                    lines.append(f\"- {fact}\")\n                continue\n            if isinstance(fact, Mapping):\n                fact_map = cast(Mapping[str, object], fact)\n                content = fact_map.get(\"content\")\n                date_created = fact_map.get(\"date_created\")\n            elif hasattr(fact, \"content\") and hasattr(fact, \"date_created\"):\n                content = fact.content\n                date_created = fact.date_created\n            else:\n                continue\n\n            if not content:\n                continue\n            ts = format_date_created(date_created)\n            suffix = f\". Stated at {ts}\" if ts else \"\"\n            lines.append(f\"- {content}{suffix}\")\n        return lines\n\n    def inject_recalled_facts(self, kwargs: dict) -> dict:\n        if self.config.cloud is True:\n            self._cloud_conversation_messages = []\n\n        if self.config.entity_id is None:\n            return kwargs\n\n        user_query = self._extract_user_query(kwargs)\n        if not user_query:\n            return kwargs\n\n        logger.debug(\"User query: %s\", truncate(user_query))\n\n        resolved_entity_id = None\n        if self.config.cloud is False:\n            if self.config.storage is None or self.config.storage.driver is None:\n                return kwargs\n\n            resolved_entity_id = self.config.storage.driver.entity.create(\n                self.config.entity_id\n            )\n            if resolved_entity_id is None:\n                return kwargs\n\n        from memori.memory.recall import Recall\n\n        recall = Recall(self.config)\n        if self.config.cloud is True:\n            data = recall._cloud_recall(user_query)\n            facts, messages = recall._parse_cloud_recall_response(data)\n            self._cloud_conversation_messages = messages\n        else:\n            facts = recall.search_facts(\n                user_query,\n                entity_id=resolved_entity_id,\n                cloud=bool(self.config.cloud),\n            )\n\n        if not facts:\n            logger.debug(\"No facts found to inject into prompt\")\n            return kwargs\n\n        relevant_facts = [\n            f\n            for f in facts\n            if _score_for_recall_threshold(f) >= self.config.recall_relevance_threshold\n        ]\n\n        if not relevant_facts:\n            logger.debug(\n                \"No facts above relevance threshold (%.2f)\",\n                self.config.recall_relevance_threshold,\n            )\n            return kwargs\n\n        logger.debug(\"Injecting %d recalled facts into prompt\", len(relevant_facts))\n\n        fact_lines = self._format_recalled_fact_lines(relevant_facts)\n        recall_context = (\n            \"\\n\\n<memori_context>\\n\"\n            \"Only use the relevant context if it is relevant to the user's query. \"\n            \"Relevant context about the user:\\n\"\n            + \"\\n\".join(fact_lines)\n            + \"\\n</memori_context>\"\n        )\n\n        if llm_is_anthropic(\n            self.config.framework.provider, self.config.llm.provider\n        ) or llm_is_bedrock(self.config.framework.provider, self.config.llm.provider):\n            existing_system = kwargs.get(\"system\", \"\")\n            kwargs[\"system\"] = existing_system + recall_context\n        elif llm_is_google(\n            self.config.framework.provider, self.config.llm.provider\n        ) or agno_is_google(self.config.framework.provider, self.config.llm.provider):\n            self._inject_google_system_instruction(kwargs, recall_context)\n        elif (\n            \"input\" in kwargs or \"instructions\" in kwargs\n        ) and \"messages\" not in kwargs:\n            existing_instructions = kwargs.get(\"instructions\", \"\") or \"\"\n            kwargs[\"instructions\"] = existing_instructions + recall_context\n        else:\n            messages = kwargs.get(\"messages\", [])\n            if messages and messages[0].get(\"role\") == \"system\":\n                messages[0][\"content\"] = messages[0][\"content\"] + recall_context\n            else:\n                context_message = {\n                    \"role\": \"system\",\n                    \"content\": recall_context.lstrip(\"\\n\"),\n                }\n                messages.insert(0, context_message)\n\n        return kwargs\n\n    def inject_conversation_messages(self, kwargs: dict) -> dict:\n        if self.config.cloud is True:\n            messages = self._cloud_conversation_messages\n            if not messages:\n                return kwargs\n\n            self._injected_message_count = len(messages)\n            logger.debug(\n                \"Injecting %d cloud conversation messages from history\",\n                len(messages),\n            )\n\n            if (\n                \"input\" in kwargs or \"instructions\" in kwargs\n            ) and \"messages\" not in kwargs:\n                history_items: list[dict[str, str]] = []\n                for msg in messages:\n                    role = msg.get(\"role\", \"user\")\n                    content = msg.get(\"content\", \"\")\n                    if role == \"system\":\n                        continue\n                    history_items.append({\"role\": role, \"content\": content})\n\n                existing_input = kwargs.get(\"input\")\n                if existing_input is None:\n                    existing_input = []\n                elif isinstance(existing_input, str):\n                    existing_input = [{\"role\": \"user\", \"content\": existing_input}]\n\n                kwargs[\"input\"] = history_items + existing_input\n                return kwargs\n\n            if (\n                llm_is_openai(self.config.framework.provider, self.config.llm.provider)\n                or agno_is_openai(\n                    self.config.framework.provider, self.config.llm.provider\n                )\n                or agno_is_xai(self.config.framework.provider, self.config.llm.provider)\n            ):\n                kwargs[\"messages\"] = messages + kwargs[\"messages\"]\n            elif (\n                llm_is_anthropic(\n                    self.config.framework.provider, self.config.llm.provider\n                )\n                or llm_is_bedrock(\n                    self.config.framework.provider, self.config.llm.provider\n                )\n                or agno_is_anthropic(\n                    self.config.framework.provider, self.config.llm.provider\n                )\n            ):\n                filtered_messages = [m for m in messages if m.get(\"role\") != \"system\"]\n                kwargs[\"messages\"] = filtered_messages + kwargs[\"messages\"]\n            elif llm_is_xai(self.config.framework.provider, self.config.llm.provider):\n                from xai_sdk.chat import assistant, user\n\n                xai_messages = []\n                for message in messages:\n                    role = message.get(\"role\", \"\")\n                    content = message.get(\"content\", \"\")\n                    if role == \"user\":\n                        xai_messages.append(user(content))\n                    elif role == \"assistant\":\n                        xai_messages.append(assistant(content))\n\n                kwargs[\"messages\"] = xai_messages + kwargs[\"messages\"]\n            elif llm_is_google(\n                self.config.framework.provider, self.config.llm.provider\n            ) or agno_is_google(\n                self.config.framework.provider, self.config.llm.provider\n            ):\n                contents = []\n                for message in messages:\n                    role = message[\"role\"]\n                    if role == \"assistant\":\n                        role = \"model\"\n                    contents.append(\n                        {\"parts\": [{\"text\": message[\"content\"]}], \"role\": role}\n                    )\n\n                if \"request\" in kwargs:\n                    formatted_kwargs = json.loads(\n                        json_format.MessageToJson(kwargs[\"request\"].__dict__[\"_pb\"])\n                    )\n                    formatted_kwargs[\"contents\"] = (\n                        contents + formatted_kwargs[\"contents\"]\n                    )\n                    json_format.ParseDict(\n                        formatted_kwargs, kwargs[\"request\"].__dict__[\"_pb\"]\n                    )\n                else:\n                    existing_contents = kwargs.get(\"contents\", [])\n                    if isinstance(existing_contents, str):\n                        existing_contents = [\n                            {\"parts\": [{\"text\": existing_contents}], \"role\": \"user\"}\n                        ]\n                    elif isinstance(existing_contents, list):\n                        normalized = []\n                        for item in existing_contents:\n                            if isinstance(item, str):\n                                normalized.append(\n                                    {\"parts\": [{\"text\": item}], \"role\": \"user\"}\n                                )\n                            else:\n                                normalized.append(item)\n                        existing_contents = normalized\n                    kwargs[\"contents\"] = contents + existing_contents\n            else:\n                raise NotImplementedError\n\n            return kwargs\n\n        if self.config.storage is None or self.config.storage.driver is None:\n            return kwargs\n\n        if self.config.cache.conversation_id is None:\n            # Try read path first (existing session/conversation in DB).\n            if self._ensure_cached_conversation_id():\n                pass\n            else:\n                # Create path: ensure session_id and conversation_id for multi-turn.\n                # Fixes issue where conversation_id wasn't available early enough (e.g. GitHub #83).\n                if self.config.cache.session_id is None:\n                    if self.config.entity_id is not None:\n                        entity_id = self.config.storage.driver.entity.create(\n                            self.config.entity_id\n                        )\n                        if entity_id is not None:\n                            self.config.cache.entity_id = entity_id\n                    if self.config.process_id is not None:\n                        process_id = self.config.storage.driver.process.create(\n                            self.config.process_id\n                        )\n                        if process_id is not None:\n                            self.config.cache.process_id = process_id\n\n                    session_id = self.config.storage.driver.session.create(\n                        self.config.session_id,\n                        self.config.cache.entity_id,\n                        self.config.cache.process_id,\n                    )\n                    if session_id is not None:\n                        self.config.cache.session_id = session_id\n\n                if self.config.cache.session_id is not None:\n                    existing_conv = self.config.storage.driver.conversation.create(\n                        self.config.cache.session_id,\n                        self.config.session_timeout_minutes,\n                    )\n                    if existing_conv is not None:\n                        self.config.cache.conversation_id = existing_conv\n\n                if self.config.cache.conversation_id is None:\n                    if not self._ensure_cached_conversation_id():\n                        return kwargs\n\n        messages = self.config.storage.driver.conversation.messages.read(\n            self.config.cache.conversation_id\n        )\n        if not messages:\n            return kwargs\n\n        self._injected_message_count = len(messages)\n        logger.debug(\"Injecting %d conversation messages from history\", len(messages))\n\n        if (\"input\" in kwargs or \"instructions\" in kwargs) and \"messages\" not in kwargs:\n            history_items = []\n            for msg in messages:\n                role = msg.get(\"role\", \"user\")\n                content = msg.get(\"content\", \"\")\n                if role == \"system\":\n                    continue\n                history_items.append({\"role\": role, \"content\": content})\n\n            existing_input = kwargs.get(\"input\")\n            if existing_input is None:\n                existing_input = []\n            elif isinstance(existing_input, str):\n                existing_input = [{\"role\": \"user\", \"content\": existing_input}]\n\n            kwargs[\"input\"] = history_items + existing_input\n            return kwargs\n\n        if (\n            llm_is_openai(self.config.framework.provider, self.config.llm.provider)\n            or agno_is_openai(self.config.framework.provider, self.config.llm.provider)\n            or agno_is_xai(self.config.framework.provider, self.config.llm.provider)\n        ):\n            kwargs[\"messages\"] = messages + kwargs[\"messages\"]\n        elif (\n            llm_is_anthropic(self.config.framework.provider, self.config.llm.provider)\n            or llm_is_bedrock(self.config.framework.provider, self.config.llm.provider)\n            or agno_is_anthropic(\n                self.config.framework.provider, self.config.llm.provider\n            )\n        ):\n            filtered_messages = [m for m in messages if m.get(\"role\") != \"system\"]\n            kwargs[\"messages\"] = filtered_messages + kwargs[\"messages\"]\n        elif llm_is_xai(self.config.framework.provider, self.config.llm.provider):\n            from xai_sdk.chat import assistant, user\n\n            xai_messages = []\n            for message in messages:\n                role = message.get(\"role\", \"\")\n                content = message.get(\"content\", \"\")\n                if role == \"user\":\n                    xai_messages.append(user(content))\n                elif role == \"assistant\":\n                    xai_messages.append(assistant(content))\n\n            kwargs[\"messages\"] = xai_messages + kwargs[\"messages\"]\n        elif llm_is_google(\n            self.config.framework.provider, self.config.llm.provider\n        ) or agno_is_google(self.config.framework.provider, self.config.llm.provider):\n            contents = []\n            for message in messages:\n                role = message[\"role\"]\n                if role == \"assistant\":\n                    role = \"model\"\n                contents.append({\"parts\": [{\"text\": message[\"content\"]}], \"role\": role})\n\n            if \"request\" in kwargs:\n                formatted_kwargs = json.loads(\n                    json_format.MessageToJson(kwargs[\"request\"].__dict__[\"_pb\"])\n                )\n                formatted_kwargs[\"contents\"] = contents + formatted_kwargs[\"contents\"]\n                json_format.ParseDict(\n                    formatted_kwargs, kwargs[\"request\"].__dict__[\"_pb\"]\n                )\n            else:\n                existing_contents = kwargs.get(\"contents\", [])\n                if isinstance(existing_contents, str):\n                    existing_contents = [\n                        {\"parts\": [{\"text\": existing_contents}], \"role\": \"user\"}\n                    ]\n                elif isinstance(existing_contents, list):\n                    normalized = []\n                    for item in existing_contents:\n                        if isinstance(item, str):\n                            normalized.append(\n                                {\"parts\": [{\"text\": item}], \"role\": \"user\"}\n                            )\n                        else:\n                            normalized.append(item)\n                    existing_contents = normalized\n                kwargs[\"contents\"] = contents + existing_contents\n        else:\n            raise NotImplementedError\n\n        return kwargs\n\n    def set_client(self, framework_provider, llm_provider, provider_sdk_version):\n        self.config.framework.provider = framework_provider\n        self.config.llm.provider = llm_provider\n        self.config.llm.provider_sdk_version = provider_sdk_version\n        return self\n\n    def uses_protobuf(self):\n        self._uses_protobuf = True\n        return self\n\n    def handle_post_response(self, kwargs, start_time, raw_response):\n        from memori.memory._manager import Manager as MemoryManager\n\n        logger = logging.getLogger(__name__)\n\n        if \"model\" in kwargs:\n            self.config.llm.version = kwargs[\"model\"]\n\n        payload = self._format_payload(\n            self.config.framework.provider,\n            self.config.llm.provider,\n            self.config.llm.version,\n            start_time,\n            __import__(\"time\").time(),\n            self._format_kwargs(kwargs),\n            self._format_response(self.get_response_content(raw_response)),\n        )\n\n        conv_id = self.config.cache.conversation_id\n        msg_count = len(\n            payload.get(\"conversation\", {}).get(\"query\", {}).get(\"messages\", [])\n        )\n        resp_count = len(\n            payload.get(\"conversation\", {}).get(\"response\", {}).get(\"choices\", [])\n        )\n        logger.debug(\n            f\"Ingesting conversation turn: conversation_id={conv_id}, \"\n            f\"messages_count={msg_count}, responses_count={resp_count}\"\n        )\n\n        MemoryManager(self.config).execute(payload)\n        if self.config.augmentation is not None:\n            from memori.memory.augmentation._handler import handle_augmentation\n\n            aug_input = self._format_augmentation_input(payload)\n\n            handle_augmentation(\n                config=self.config,\n                payload=aug_input,\n                kwargs=kwargs,\n                augmentation_manager=self.config.augmentation,\n                log_content=lambda c: logger.debug(\n                    \"Response content: %s\", truncate(str(c))\n                ),\n            )\n\n\nclass BaseIterator:\n    def __init__(self, config: Config, source_iterator):\n        self.config = config\n        self.source_iterator = source_iterator\n        self.iterator = None\n        self.raw_response: dict | list | None = None\n\n    def configure_invoke(self, invoke: BaseInvoke):\n        self.invoke = invoke\n        return self\n\n    def configure_request(self, kwargs, time_start):\n        self._kwargs = kwargs\n        self._time_start = time_start\n        return self\n\n    def process_chunk(self, chunk):\n        if hasattr(chunk, \"type\") and chunk.type == \"response.completed\":\n            if hasattr(chunk, \"response\"):\n                response = chunk.response\n                if hasattr(response, \"model_dump\"):\n                    self.raw_response = response.model_dump()\n                else:\n                    self.raw_response = self.invoke._convert_to_json(response)\n            return self\n\n        if self.invoke._uses_protobuf is True:\n            formatted_chunk = copy.deepcopy(chunk)\n            if isinstance(self.raw_response, list):\n                if \"_pb\" in formatted_chunk.__dict__:\n                    # Old google-generativeai format (protobuf)\n                    self.raw_response.append(\n                        json.loads(\n                            json_format.MessageToJson(formatted_chunk.__dict__[\"_pb\"])\n                        )\n                    )\n                elif \"candidates\" in formatted_chunk.__dict__:\n                    # New google-genai format (dict with candidates)\n                    chunk_data = {}\n                    if (\n                        hasattr(formatted_chunk, \"candidates\")\n                        and formatted_chunk.candidates\n                    ):\n                        candidates = []\n                        for candidate in formatted_chunk.candidates:\n                            candidate_data = {}\n                            if hasattr(candidate, \"content\") and candidate.content:\n                                content_data = {}\n                                if (\n                                    hasattr(candidate.content, \"parts\")\n                                    and candidate.content.parts\n                                ):\n                                    parts = []\n                                    for part in candidate.content.parts:\n                                        if hasattr(part, \"text\"):\n                                            parts.append({\"text\": part.text})\n                                    content_data[\"parts\"] = parts\n                                if hasattr(candidate.content, \"role\"):\n                                    content_data[\"role\"] = candidate.content.role\n                                candidate_data[\"content\"] = content_data\n                            candidates.append(candidate_data)\n                        chunk_data[\"candidates\"] = candidates\n                    self.raw_response.append(chunk_data)\n        else:\n            if isinstance(self.raw_response, dict):\n                self.raw_response = merge_chunk(self.raw_response, chunk.__dict__)\n\n        return self\n\n    def set_raw_response(self):\n        if self.raw_response is not None:\n            return self\n\n        self.raw_response = {}\n        if self.invoke._uses_protobuf:\n            self.raw_response = []\n\n        return self\n\n\nclass BaseLlmAdaptor:\n    def _exclude_injected_messages(self, messages, payload):\n        injected_count = (\n            payload.get(\"conversation\", {})\n            .get(\"query\", {})\n            .get(\"_memori_injected_count\", 0)\n        )\n        return messages[injected_count:]\n\n    def get_formatted_query(self, payload):\n        raise NotImplementedError\n\n    def get_formatted_response(self, payload):\n        raise NotImplementedError\n\n\nclass BaseProvider:\n    def __init__(self, entity):\n        self.client = None\n        self.entity = entity\n        self.config = entity.config\n"
  },
  {
    "path": "memori/llm/_clients.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.llm._base import BaseClient\nfrom memori.llm._constants import (\n    AGNO_FRAMEWORK_PROVIDER,\n    AGNO_GOOGLE_LLM_PROVIDER,\n    ATHROPIC_LLM_PROVIDER,\n    GOOGLE_LLM_PROVIDER,\n    LANGCHAIN_CHATBEDROCK_LLM_PROVIDER,\n    LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n    LANGCHAIN_CHATVERTEXAI_LLM_PROVIDER,\n    LANGCHAIN_FRAMEWORK_PROVIDER,\n    LANGCHAIN_OPENAI_LLM_PROVIDER,\n    OPENAI_LLM_PROVIDER,\n    PYDANTIC_AI_FRAMEWORK_PROVIDER,\n    PYDANTIC_AI_OPENAI_LLM_PROVIDER,\n)\nfrom memori.llm._invoke import (\n    Invoke,\n    InvokeAsync,\n    InvokeAsyncIterator,\n)\nfrom memori.llm._registry import Registry\n\n\n@Registry.register_client(\n    lambda client: type(client).__module__.startswith(\"anthropic\")\n)\nclass Anthropic(BaseClient):\n    def register(self, client, _provider=None):\n        if not hasattr(client, \"messages\"):\n            raise RuntimeError(\"client provided is not instance of Anthropic\")\n\n        if not hasattr(client, \"_memori_installed\"):\n            client.beta._messages_create = client.beta.messages.create\n            client._messages_create = client.messages.create\n\n            try:\n                import anthropic\n\n                client_version = anthropic.__version__\n            except (ImportError, AttributeError):\n                client_version = None\n\n            self._wrap_method(\n                client.beta.messages,\n                \"create\",\n                client.beta,\n                \"_messages_create\",\n                _provider,\n                ATHROPIC_LLM_PROVIDER,\n                client_version,\n            )\n            self._wrap_method(\n                client.messages,\n                \"create\",\n                client,\n                \"_messages_create\",\n                _provider,\n                ATHROPIC_LLM_PROVIDER,\n                client_version,\n            )\n\n            client._memori_installed = True\n\n        return self\n\n\n@Registry.register_client(\n    lambda client: type(client).__module__.startswith(\n        (\"google.generativeai\", \"google.ai.generativelanguage\", \"google.genai\")\n    )\n)\nclass Google(BaseClient):\n    def register(self, client, _provider=None):\n        if not hasattr(client, \"models\"):\n            raise RuntimeError(\"client provided is not instance of genai.Client\")\n\n        if not hasattr(client, \"_memori_installed\"):\n            client.models.actual_generate_content = client.models.generate_content\n\n            try:\n                from google import genai\n\n                client_version = genai.__version__\n            except (ImportError, AttributeError):\n                try:\n                    from importlib.metadata import version\n\n                    client_version = version(\"google-genai\")\n                except Exception:\n                    client_version = None\n\n            llm_provider = (\n                AGNO_GOOGLE_LLM_PROVIDER\n                if _provider == AGNO_FRAMEWORK_PROVIDER\n                else GOOGLE_LLM_PROVIDER\n            )\n\n            client.models.generate_content = (\n                Invoke(self.config, client.models.actual_generate_content)\n                .set_client(_provider, llm_provider, client_version)\n                .uses_protobuf()\n                .invoke\n            )\n\n            # Register sync streaming if available\n            if hasattr(client.models, \"generate_content_stream\"):\n                client.models.actual_generate_content_stream = (\n                    client.models.generate_content_stream\n                )\n                client.models.generate_content_stream = (\n                    Invoke(\n                        self.config,\n                        client.models.actual_generate_content_stream,\n                    )\n                    .set_client(_provider, llm_provider, client_version)\n                    .uses_protobuf()\n                    .invoke\n                )\n\n            # Register async client if available\n            if hasattr(client, \"aio\") and hasattr(client.aio, \"models\"):\n                client.aio.models.actual_generate_content = (\n                    client.aio.models.generate_content\n                )\n                client.aio.models.generate_content = (\n                    InvokeAsync(self.config, client.aio.models.actual_generate_content)\n                    .set_client(_provider, llm_provider, client_version)\n                    .uses_protobuf()\n                    .invoke\n                )\n\n                # Register streaming if available\n                if hasattr(client.aio.models, \"generate_content_stream\"):\n                    client.aio.models.actual_generate_content_stream = (\n                        client.aio.models.generate_content_stream\n                    )\n                    client.aio.models.generate_content_stream = (\n                        InvokeAsyncIterator(\n                            self.config,\n                            client.aio.models.actual_generate_content_stream,\n                        )\n                        .set_client(_provider, llm_provider, client_version)\n                        .uses_protobuf()\n                        .invoke\n                    )\n\n            client._memori_installed = True\n\n        return self\n\n\nclass LangChain(BaseClient):\n    def register(\n        self, chatbedrock=None, chatgooglegenai=None, chatopenai=None, chatvertexai=None\n    ):\n        if (\n            chatbedrock is None\n            and chatgooglegenai is None\n            and chatopenai is None\n            and chatvertexai is None\n        ):\n            raise RuntimeError(\"LangChain::register called without client\")\n\n        if chatbedrock is not None:\n            if not hasattr(chatbedrock, \"client\"):\n                raise RuntimeError(\"client provided is not instance of ChatBedrock\")\n\n            if not hasattr(chatbedrock.client, \"_memori_installed\"):\n                chatbedrock.client._invoke_model = chatbedrock.client.invoke_model\n                chatbedrock.client.invoke_model = (\n                    Invoke(self.config, chatbedrock.client._invoke_model)\n                    .set_client(\n                        LANGCHAIN_FRAMEWORK_PROVIDER,\n                        LANGCHAIN_CHATBEDROCK_LLM_PROVIDER,\n                        None,\n                    )\n                    .invoke\n                )\n\n                chatbedrock.client._invoke_model_with_response_stream = (\n                    chatbedrock.client.invoke_model_with_response_stream\n                )\n                chatbedrock.client.invoke_model_with_response_stream = (\n                    Invoke(\n                        self.config,\n                        chatbedrock.client._invoke_model_with_response_stream,\n                    )\n                    .set_client(\n                        LANGCHAIN_FRAMEWORK_PROVIDER,\n                        LANGCHAIN_CHATBEDROCK_LLM_PROVIDER,\n                        None,\n                    )\n                    .invoke\n                )\n\n                chatbedrock.client._memori_installed = True\n\n        if chatgooglegenai is not None:\n            if not hasattr(chatgooglegenai, \"client\"):\n                raise RuntimeError(\n                    \"client provided is not instance of ChatGoogleGenerativeAI\"\n                )\n\n            if not hasattr(chatgooglegenai.client, \"_memori_installed\"):\n                # Check if this is the new google.genai SDK (client.models.generate_content)\n                # or the old google.generativeai SDK (client.generate_content)\n                if hasattr(chatgooglegenai.client, \"models\") and hasattr(\n                    chatgooglegenai.client.models, \"generate_content\"\n                ):\n                    # New google.genai SDK - use client.models.generate_content\n                    chatgooglegenai.client.models._generate_content = (\n                        chatgooglegenai.client.models.generate_content\n                    )\n                    chatgooglegenai.client.models.generate_content = (\n                        Invoke(\n                            self.config,\n                            chatgooglegenai.client.models._generate_content,\n                        )\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .uses_protobuf()\n                        .invoke\n                    )\n\n                    # Handle async client for new SDK\n                    if (\n                        chatgooglegenai.async_client is not None\n                        and hasattr(chatgooglegenai.async_client, \"models\")\n                        and hasattr(\n                            chatgooglegenai.async_client.models, \"generate_content\"\n                        )\n                    ):\n                        chatgooglegenai.async_client.models._generate_content = (\n                            chatgooglegenai.async_client.models.generate_content\n                        )\n                        chatgooglegenai.async_client.models.generate_content = (\n                            InvokeAsync(\n                                self.config,\n                                chatgooglegenai.async_client.models._generate_content,\n                            )\n                            .set_client(\n                                LANGCHAIN_FRAMEWORK_PROVIDER,\n                                LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n                                None,\n                            )\n                            .uses_protobuf()\n                            .invoke\n                        )\n\n                        # Handle streaming for new SDK async client\n                        if hasattr(\n                            chatgooglegenai.async_client.models,\n                            \"generate_content_stream\",\n                        ):\n                            chatgooglegenai.async_client.models._stream_generate_content = chatgooglegenai.async_client.models.generate_content_stream\n                            chatgooglegenai.async_client.models.generate_content_stream = (\n                                InvokeAsyncIterator(\n                                    self.config,\n                                    chatgooglegenai.async_client.models._stream_generate_content,\n                                )\n                                .set_client(\n                                    LANGCHAIN_FRAMEWORK_PROVIDER,\n                                    LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n                                    None,\n                                )\n                                .uses_protobuf()\n                                .invoke\n                            )\n\n                    # Handle sync streaming for new SDK\n                    if hasattr(\n                        chatgooglegenai.client.models, \"generate_content_stream\"\n                    ):\n                        chatgooglegenai.client.models._stream_generate_content = (\n                            chatgooglegenai.client.models.generate_content_stream\n                        )\n                        chatgooglegenai.client.models.generate_content_stream = (\n                            Invoke(\n                                self.config,\n                                chatgooglegenai.client.models._stream_generate_content,\n                            )\n                            .set_client(\n                                LANGCHAIN_FRAMEWORK_PROVIDER,\n                                LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n                                None,\n                            )\n                            .uses_protobuf()\n                            .invoke\n                        )\n                else:\n                    # Old google.generativeai SDK - use client.generate_content directly\n                    chatgooglegenai.client._generate_content = (\n                        chatgooglegenai.client.generate_content\n                    )\n                    chatgooglegenai.client.generate_content = (\n                        Invoke(self.config, chatgooglegenai.client._generate_content)\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .uses_protobuf()\n                        .invoke\n                    )\n\n                    if chatgooglegenai.async_client is not None:\n                        chatgooglegenai.async_client._stream_generate_content = (\n                            chatgooglegenai.async_client.stream_generate_content\n                        )\n                        chatgooglegenai.async_client.stream_generate_content = (\n                            InvokeAsyncIterator(\n                                self.config,\n                                chatgooglegenai.async_client._stream_generate_content,\n                            )\n                            .set_client(\n                                LANGCHAIN_FRAMEWORK_PROVIDER,\n                                LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n                                None,\n                            )\n                            .uses_protobuf()\n                            .invoke\n                        )\n\n                chatgooglegenai.client._memori_installed = True\n\n        if chatopenai is not None:\n            if not hasattr(chatopenai, \"client\") or not hasattr(\n                chatopenai, \"async_client\"\n            ):\n                raise RuntimeError(\"client provided is not instance of ChatOpenAI\")\n\n            for client in filter(\n                None,\n                [getattr(chatopenai, \"http_client\", None), chatopenai.client._client],\n            ):\n                if not hasattr(client, \"_memori_installed\"):\n                    client.beta._chat_completions_create = (\n                        client.beta.chat.completions.create\n                    )\n                    client.beta.chat.completions.create = (\n                        Invoke(self.config, client.beta._chat_completions_create)\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_OPENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .invoke\n                    )\n\n                    client.beta._chat_completions_parse = (\n                        client.beta.chat.completions.parse\n                    )\n                    client.beta.chat.completions.parse = (\n                        Invoke(self.config, client.beta._chat_completions_parse)\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_OPENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .invoke\n                    )\n\n                    client._chat_completions_create = client.chat.completions.create\n                    client.chat.completions.create = (\n                        Invoke(self.config, client._chat_completions_create)\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_OPENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .invoke\n                    )\n\n                    client._chat_completions_parse = client.chat.completions.parse\n                    client.chat.completions.parse = (\n                        Invoke(self.config, client._chat_completions_parse)\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_OPENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .invoke\n                    )\n\n                    client._memori_installed = True\n\n            for client in filter(\n                None,\n                [\n                    getattr(chatopenai, \"async_http_client\", None),\n                    chatopenai.async_client._client,\n                ],\n            ):\n                if not hasattr(client, \"_memori_installed\"):\n                    client.beta._chat_completions_create = (\n                        client.beta.chat.completions.create\n                    )\n                    client.beta.chat.completions.create = (\n                        InvokeAsyncIterator(\n                            self.config, client.beta._chat_completions_create\n                        )\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_OPENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .invoke\n                    )\n\n                    client.beta._chat_completions_parse = (\n                        client.beta.chat.completions.parse\n                    )\n                    client.beta.chat.completions.parse = (\n                        InvokeAsyncIterator(\n                            self.config, client.beta._chat_completions_parse\n                        )\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_OPENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .invoke\n                    )\n\n                    client._chat_completions_create = client.chat.completions.create\n                    client.chat.completions.create = (\n                        InvokeAsyncIterator(\n                            self.config, client._chat_completions_create\n                        )\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_OPENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .invoke\n                    )\n\n                    client._chat_completions_parse = client.chat.completions.parse\n                    client.chat.completions.parse = (\n                        InvokeAsyncIterator(self.config, client._chat_completions_parse)\n                        .set_client(\n                            LANGCHAIN_FRAMEWORK_PROVIDER,\n                            LANGCHAIN_OPENAI_LLM_PROVIDER,\n                            None,\n                        )\n                        .invoke\n                    )\n\n                    client._memori_installed = True\n\n        if chatvertexai is not None:\n            if not hasattr(chatvertexai, \"prediction_client\"):\n                raise RuntimeError(\"client provided isnot instance of ChatVertexAI\")\n\n            if not hasattr(chatvertexai.prediction_client, \"_memori_installed\"):\n                chatvertexai.prediction_client.actual_generate_content = (\n                    chatvertexai.prediction_client.generate_content\n                )\n                chatvertexai.prediction_client.generate_content = (\n                    Invoke(\n                        self.config,\n                        chatvertexai.prediction_client.actual_generate_content,\n                    )\n                    .set_client(\n                        LANGCHAIN_FRAMEWORK_PROVIDER,\n                        LANGCHAIN_CHATVERTEXAI_LLM_PROVIDER,\n                        None,\n                    )\n                    .uses_protobuf()\n                    .invoke\n                )\n\n                chatvertexai.prediction_client._memori_installed = True\n\n        return self\n\n\ndef _detect_platform(client):\n    \"\"\"Detect hosting platform from client base_url.\"\"\"\n    if hasattr(client, \"base_url\"):\n        base_url = str(client.base_url).lower()\n        if \"nebius\" in base_url:\n            return \"nebius\"\n        elif \"deepseek\" in base_url:\n            return \"deepseek\"\n        elif \"nvidia\" in base_url:\n            return \"nvidia_nim\"\n    return None\n\n\n@Registry.register_client(lambda client: type(client).__module__.startswith(\"openai\"))\nclass OpenAi(BaseClient):\n    def register(self, client, _provider=None, stream=False):\n        if not hasattr(client, \"chat\"):\n            raise RuntimeError(\"client provided is not instance of OpenAI\")\n\n        if not hasattr(client, \"_memori_installed\"):\n            client.beta._chat_completions_parse = client.beta.chat.completions.parse\n            client.chat._completions_create = client.chat.completions.create\n\n            platform = _detect_platform(client)\n            if platform:\n                self.config.platform.provider = platform\n\n            self.config.llm.provider_sdk_version = client._version\n\n            self._wrap_method(\n                client.beta.chat.completions,\n                \"parse\",\n                client.beta,\n                \"_chat_completions_parse\",\n                _provider,\n                OPENAI_LLM_PROVIDER,\n                client._version,\n                stream,\n            )\n            self._wrap_method(\n                client.chat.completions,\n                \"create\",\n                client.chat,\n                \"_completions_create\",\n                _provider,\n                OPENAI_LLM_PROVIDER,\n                client._version,\n                stream,\n            )\n\n            if hasattr(client, \"responses\"):\n                client._responses_create = client.responses.create\n                self._wrap_method(\n                    client.responses,\n                    \"create\",\n                    client,\n                    \"_responses_create\",\n                    _provider,\n                    OPENAI_LLM_PROVIDER,\n                    client._version,\n                    stream,\n                )\n\n            client._memori_installed = True\n\n        return self\n\n\n@Registry.register_client(\n    lambda client: type(client).__module__.startswith(\"pydantic_ai\")\n)\nclass PydanticAi(BaseClient):\n    def register(self, client):\n        if not hasattr(client, \"chat\"):\n            raise RuntimeError(\"client provided was not instantiated using PydanticAi\")\n\n        if not hasattr(client, \"_memori_installed\"):\n            client.chat.completions.actual_chat_completions_create = (\n                client.chat.completions.create\n            )\n\n            client.chat.completions.create = (\n                InvokeAsyncIterator(\n                    self.config,\n                    client.chat.completions.actual_chat_completions_create,\n                )\n                .set_client(\n                    PYDANTIC_AI_FRAMEWORK_PROVIDER,\n                    PYDANTIC_AI_OPENAI_LLM_PROVIDER,\n                    client._version,\n                )\n                .invoke\n            )\n\n            client._memori_installed = True\n\n        return self\n\n\n@Registry.register_client(lambda client: \"xai\" in str(type(client).__module__).lower())\nclass XAi(BaseClient):\n    \"\"\"\n    XAI client requires special handling due to its two-step API.\n\n    Unlike other clients, the actual API call happens on the Chat object\n    returned by create(), not on the create() method itself. All wrapping\n    logic is delegated to the XAiWrappers class.\n    \"\"\"\n\n    def register(self, client, _provider=None, stream=False):\n        from memori.llm._constants import XAI_LLM_PROVIDER\n        from memori.llm._xai_wrappers import XAiWrappers\n\n        if not hasattr(client, \"chat\"):\n            raise RuntimeError(\"client provided is not instance of xAI\")\n\n        try:\n            import xai_sdk\n\n            client_version = xai_sdk.__version__\n        except (ImportError, AttributeError):\n            client_version = None\n\n        if not hasattr(client, \"_memori_installed\"):\n            if hasattr(client.chat, \"completions\"):\n                client.beta._chat_completions_parse = client.beta.chat.completions.parse\n                client.chat._completions_create = client.chat.completions.create\n\n                self.config.framework.provider = _provider\n                self.config.llm.provider = XAI_LLM_PROVIDER\n                self.config.llm.provider_sdk_version = client_version\n\n                self._wrap_method(\n                    client.beta.chat.completions,\n                    \"parse\",\n                    client.beta,\n                    \"_chat_completions_parse\",\n                    _provider,\n                    XAI_LLM_PROVIDER,\n                    client_version,\n                    stream,\n                )\n                self._wrap_method(\n                    client.chat.completions,\n                    \"create\",\n                    client.chat,\n                    \"_completions_create\",\n                    _provider,\n                    XAI_LLM_PROVIDER,\n                    client_version,\n                    stream,\n                )\n            else:\n                client.chat._create = client.chat.create\n\n                self.config.framework.provider = _provider\n                self.config.llm.provider = XAI_LLM_PROVIDER\n                self.config.llm.provider_sdk_version = client_version\n\n                wrappers = XAiWrappers(self.config)\n\n                def wrapped_create(*args, **kwargs):\n                    model = kwargs.get(\"model\")\n                    kwargs = wrappers.inject_conversation_history(kwargs)\n                    chat_obj = client.chat._create(*args, **kwargs)\n                    wrappers.wrap_chat_methods(chat_obj, client_version, model)\n                    return chat_obj\n\n                client.chat.create = wrapped_create\n\n            client._memori_installed = True\n\n        return self\n\n\nclass Agno(BaseClient):\n    def register(self, openai_chat=None, claude=None, gemini=None, xai=None):\n        if openai_chat is None and claude is None and gemini is None and xai is None:\n            raise RuntimeError(\"Agno::register called without model\")\n\n        if openai_chat is not None:\n            if not self._is_agno_openai_model(openai_chat):\n                raise RuntimeError(\n                    \"model provided is not instance of agno.models.openai.OpenAIChat\"\n                )\n            client = openai_chat.get_client()\n            OpenAi(self.config).register(client, _provider=AGNO_FRAMEWORK_PROVIDER)\n\n            if not hasattr(openai_chat, \"_memori_original_get_client\"):\n                original_get_client = openai_chat.get_client\n                openai_chat._memori_original_get_client = original_get_client\n                openai_wrapper = OpenAi(self.config)\n\n                def wrapped_get_client():\n                    client = openai_chat._memori_original_get_client()\n                    openai_wrapper.register(client, _provider=AGNO_FRAMEWORK_PROVIDER)\n                    return client\n\n                openai_chat.get_client = wrapped_get_client\n\n                # Also wrap get_async_client for async support\n                if hasattr(openai_chat, \"get_async_client\"):\n                    original_get_async_client = openai_chat.get_async_client\n                    openai_chat._memori_original_get_async_client = (\n                        original_get_async_client\n                    )\n\n                    def wrapped_get_async_client():\n                        client = openai_chat._memori_original_get_async_client()\n                        openai_wrapper.register(\n                            client, _provider=AGNO_FRAMEWORK_PROVIDER\n                        )\n                        return client\n\n                    openai_chat.get_async_client = wrapped_get_async_client\n\n        if claude is not None:\n            if not self._is_agno_anthropic_model(claude):\n                raise RuntimeError(\n                    \"model provided is not instance of agno.models.anthropic.Claude\"\n                )\n            client = claude.get_client()\n            Anthropic(self.config).register(client, _provider=AGNO_FRAMEWORK_PROVIDER)\n\n            if not hasattr(claude, \"_memori_original_get_client\"):\n                original_get_client = claude.get_client\n                claude._memori_original_get_client = original_get_client\n                anthropic_wrapper = Anthropic(self.config)\n\n                def wrapped_get_client():\n                    client = claude._memori_original_get_client()\n                    anthropic_wrapper.register(\n                        client, _provider=AGNO_FRAMEWORK_PROVIDER\n                    )\n                    return client\n\n                claude.get_client = wrapped_get_client\n\n                # Also wrap get_async_client for async support\n                if hasattr(claude, \"get_async_client\"):\n                    original_get_async_client = claude.get_async_client\n                    claude._memori_original_get_async_client = original_get_async_client\n\n                    def wrapped_get_async_client():\n                        client = claude._memori_original_get_async_client()\n                        anthropic_wrapper.register(\n                            client, _provider=AGNO_FRAMEWORK_PROVIDER\n                        )\n                        return client\n\n                    claude.get_async_client = wrapped_get_async_client\n\n        if gemini is not None:\n            if not self._is_agno_google_model(gemini):\n                raise RuntimeError(\n                    \"model provided is not instance of agno.models.google.Gemini\"\n                )\n            client = gemini.get_client()\n            Google(self.config).register(client, _provider=AGNO_FRAMEWORK_PROVIDER)\n\n            # Wrap get_client to ensure all future client instances are wrapped\n            if not hasattr(gemini, \"_memori_original_get_client\"):\n                original_get_client = gemini.get_client\n                gemini._memori_original_get_client = original_get_client\n                google_wrapper = Google(self.config)\n\n                def wrapped_get_client():\n                    client = gemini._memori_original_get_client()\n                    google_wrapper.register(client, _provider=AGNO_FRAMEWORK_PROVIDER)\n                    return client\n\n                gemini.get_client = wrapped_get_client\n\n        if xai is not None:\n            if not self._is_agno_xai_model(xai):\n                raise RuntimeError(\n                    \"model provided is not instance of agno.models.xai.xAI\"\n                )\n            client = xai.get_client()\n            XAi(self.config).register(client, _provider=AGNO_FRAMEWORK_PROVIDER)\n\n            if not hasattr(xai, \"_memori_original_get_client\"):\n                original_get_client = xai.get_client\n                xai._memori_original_get_client = original_get_client\n                xai_wrapper = XAi(self.config)\n\n                def wrapped_get_client():\n                    client = xai._memori_original_get_client()\n                    xai_wrapper.register(client, _provider=AGNO_FRAMEWORK_PROVIDER)\n                    return client\n\n                xai.get_client = wrapped_get_client\n\n                # Also wrap get_async_client for async support\n                if hasattr(xai, \"get_async_client\"):\n                    original_get_async_client = xai.get_async_client\n                    xai._memori_original_get_async_client = original_get_async_client\n\n                    def wrapped_get_async_client():\n                        client = xai._memori_original_get_async_client()\n                        xai_wrapper.register(client, _provider=AGNO_FRAMEWORK_PROVIDER)\n                        return client\n\n                    xai.get_async_client = wrapped_get_async_client\n\n        return self\n\n    def _is_agno_openai_model(self, model):\n        return \"agno.models.openai\" in str(type(model).__module__)\n\n    def _is_agno_anthropic_model(self, model):\n        return \"agno.models.anthropic\" in str(type(model).__module__)\n\n    def _is_agno_google_model(self, model):\n        return \"agno.models.google\" in str(type(model).__module__)\n\n    def _is_agno_xai_model(self, model):\n        return \"agno.models.xai\" in str(type(model).__module__)\n"
  },
  {
    "path": "memori/llm/_constants.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nAGNO_ANTHROPIC_LLM_PROVIDER = \"anthropic\"\nAGNO_FRAMEWORK_PROVIDER = \"agno\"\nAGNO_GOOGLE_LLM_PROVIDER = \"google\"\nAGNO_OPENAI_LLM_PROVIDER = \"openai\"\nAGNO_XAI_LLM_PROVIDER = \"xai\"\nATHROPIC_LLM_PROVIDER = \"anthropic\"\nGOOGLE_LLM_PROVIDER = \"google\"\nLANGCHAIN_CHATBEDROCK_LLM_PROVIDER = \"chatbedrock\"\nLANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER = \"chatgooglegenai\"\nLANGCHAIN_CHATVERTEXAI_LLM_PROVIDER = \"chatvertexai\"\nLANGCHAIN_FRAMEWORK_PROVIDER = \"langchain\"\nLANGCHAIN_OPENAI_LLM_PROVIDER = \"chatopenai\"\nOPENAI_LLM_PROVIDER = \"openai\"\nPYDANTIC_AI_FRAMEWORK_PROVIDER = \"pydantic_ai\"\nPYDANTIC_AI_OPENAI_LLM_PROVIDER = \"openai\"\nXAI_LLM_PROVIDER = \"xai\"\n"
  },
  {
    "path": "memori/llm/_invoke.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport inspect\nimport logging\nimport time\nfrom collections.abc import AsyncIterator, Iterator\n\nfrom botocore.eventstream import EventStream\nfrom grpc.experimental.aio import UnaryStreamCall\n\nfrom memori._logging import truncate\nfrom memori._utils import merge_chunk\nfrom memori.llm._base import BaseInvoke\nfrom memori.llm._iterable import Iterable as MemoriIterable\nfrom memori.llm._iterator import AsyncIterator as MemoriAsyncIterator\nfrom memori.llm._iterator import Iterator as MemoriIterator\nfrom memori.llm._streaming import StreamingBody as MemoriStreamingBody\nfrom memori.llm._utils import client_is_bedrock\n\nlogger = logging.getLogger(__name__)\n\n\nclass Invoke(BaseInvoke):\n    def invoke(self, **kwargs):\n        start = time.time()\n\n        kwargs = self.inject_conversation_messages(\n            self.inject_recalled_facts(self.configure_for_streaming_usage(kwargs))\n        )\n\n        logger.debug(\n            \"Sending request to LLM - provider: %s, model: %s\",\n            self.config.llm.provider,\n            truncate(str(kwargs.get(\"model\", \"unknown\")), 100),\n        )\n        raw_response = self._method(**kwargs)\n\n        if isinstance(raw_response, Iterator) or inspect.isgenerator(raw_response):\n            return (\n                MemoriIterator(self.config, raw_response)\n                .configure_invoke(self)\n                .configure_request(kwargs, start)\n            )\n        elif client_is_bedrock(\n            self.config.framework.provider, self.config.llm.provider\n        ):\n            if isinstance(raw_response[\"body\"], EventStream):\n                raw_response[\"body\"] = (\n                    MemoriIterable(self.config, raw_response[\"body\"])\n                    .configure_invoke(self)\n                    .configure_request(kwargs, start)\n                )\n            else:\n                raw_response[\"body\"] = (\n                    MemoriStreamingBody(self.config, raw_response[\"body\"])\n                    .configure_invoke(self)\n                    .configure_request(kwargs, start)\n                )\n\n            return raw_response\n        else:\n            self.handle_post_response(kwargs, start, raw_response)\n            return raw_response\n\n\nclass InvokeAsync(BaseInvoke):\n    async def invoke(self, **kwargs):\n        start = time.time()\n\n        kwargs = self.inject_conversation_messages(\n            self.inject_recalled_facts(self.configure_for_streaming_usage(kwargs))\n        )\n\n        logger.debug(\n            \"Sending async request to LLM - provider: %s, model: %s\",\n            self.config.llm.provider,\n            truncate(str(kwargs.get(\"model\", \"unknown\")), 100),\n        )\n        raw_response = await self._method(**kwargs)\n        self.handle_post_response(kwargs, start, raw_response)\n        return raw_response\n\n\nclass InvokeAsyncIterator(BaseInvoke):\n    async def invoke(self, **kwargs):\n        start = time.time()\n\n        kwargs = self.inject_conversation_messages(\n            self.inject_recalled_facts(self.configure_for_streaming_usage(kwargs))\n        )\n\n        raw_response = await self._method(**kwargs)\n        if isinstance(raw_response, AsyncIterator) or isinstance(\n            raw_response, UnaryStreamCall\n        ):\n            return (\n                MemoriAsyncIterator(self.config, raw_response)\n                .configure_invoke(self)\n                .configure_request(kwargs, start)\n            )\n        else:\n            self.handle_post_response(kwargs, start, raw_response)\n            return raw_response\n\n\nclass InvokeAsyncStream(BaseInvoke):\n    async def invoke(self, **kwargs):\n        start = time.time()\n\n        kwargs = self.inject_conversation_messages(\n            self.inject_recalled_facts(self.configure_for_streaming_usage(kwargs))\n        )\n\n        stream = await self._method(**kwargs)\n\n        raw_response = {}\n        async for chunk in stream:\n            raw_response = merge_chunk(raw_response, chunk.__dict__)\n            yield chunk\n\n        self.handle_post_response(kwargs, start, raw_response)\n\n\nclass InvokeStream(BaseInvoke):\n    async def invoke(self, **kwargs):\n        start = time.time()\n\n        kwargs = self.inject_conversation_messages(\n            self.inject_recalled_facts(self.configure_for_streaming_usage(kwargs))\n        )\n\n        raw_response = await self._method(**kwargs)\n\n        self.handle_post_response(kwargs, start, raw_response)\n        return raw_response\n"
  },
  {
    "path": "memori/llm/_iterable.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport copy\nimport time\n\nfrom memori._config import Config\nfrom memori._utils import bytes_to_json\nfrom memori.llm._base import BaseInvoke\nfrom memori.llm._utils import client_is_bedrock\nfrom memori.memory._manager import Manager as MemoryManager\n\n\nclass Iterable:\n    def __init__(self, config: Config, source_iterable):\n        self.config = config\n        self.source_iterable = source_iterable\n        self.raw_response = []\n\n    def __getattr__(self, name):\n        return getattr(self.source_iterable, name)\n\n    def configure_invoke(self, invoke: BaseInvoke):\n        self.invoke = invoke\n        return self\n\n    def configure_request(self, kwargs, time_start):\n        self._kwargs = kwargs\n        self._time_start = time_start\n\n        if client_is_bedrock(self.config.framework.provider, self.config.llm.provider):\n            self._kwargs = bytes_to_json(self._kwargs)\n\n        return self\n\n    def __iter__(self):\n        try:\n            for raw_event in self.source_iterable:\n                if client_is_bedrock(\n                    self.config.framework.provider, self.config.llm_provider\n                ):\n                    self.raw_response.append(bytes_to_json(copy.deepcopy(raw_event)))\n\n                yield raw_event\n        finally:\n            MemoryManager(self.config).execute(\n                self.invoke._format_payload(\n                    self.config.framework.provider,\n                    self.config.llm.provider,\n                    self.config.llm.version,\n                    self._time_start,\n                    time.time(),\n                    self.invoke._format_kwargs(self._kwargs),\n                    self.invoke._format_response(self.raw_response),\n                )\n            )\n"
  },
  {
    "path": "memori/llm/_iterator.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport time\n\nfrom memori.llm._base import BaseIterator\nfrom memori.memory._manager import Manager as MemoryManager\n\n\nclass AsyncIterator(BaseIterator):\n    def __aiter__(self):\n        self.iterator = self.source_iterator.__aiter__()\n        return self\n\n    async def __anext__(self):\n        try:\n            if self.iterator is None:\n                raise RuntimeError(\"Iterator not initialized\")\n            chunk = await self.iterator.__anext__()\n\n            self.set_raw_response()\n            self.process_chunk(chunk)\n\n            return chunk\n        except StopAsyncIteration:\n            MemoryManager(self.config).execute(\n                self.invoke._format_payload(\n                    self.config.framework.provider,\n                    self.config.llm.provider,\n                    self.config.llm.version,\n                    self._time_start,\n                    time.time(),\n                    self.invoke._format_kwargs(self._kwargs),\n                    self.invoke._format_response(self.raw_response),\n                )\n            )\n            raise\n\n    async def __aenter__(self):\n        if hasattr(self.source_iterator, \"__aenter__\"):\n            await self.source_iterator.__aenter__()\n        return self\n\n    async def __aexit__(self, exc_type, exc, tb):\n        if hasattr(self.source_iterator, \"__aexit__\"):\n            return await self.source_iterator.__aexit__(exc_type, exc, tb)\n        return False\n\n\nclass Iterator(BaseIterator):\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        try:\n            chunk = next(self.source_iterator)\n\n            self.set_raw_response()\n            self.process_chunk(chunk)\n\n            return chunk\n        except StopIteration:\n            MemoryManager(self.config).execute(\n                self.invoke._format_payload(\n                    self.config.framework.provider,\n                    self.config.llm.provider,\n                    self.config.llm.version,\n                    self._time_start,\n                    time.time(),\n                    self.invoke._format_kwargs(self._kwargs),\n                    self.invoke._format_response(self.raw_response),\n                )\n            )\n\n            raise\n\n    def __enter__(self):\n        if hasattr(self.source_iterator, \"__enter__\"):\n            self.source_iterator.__enter__()\n        return self\n\n    def __exit__(self, exc_type, exc, tb):\n        if hasattr(self.source_iterator, \"__exit__\"):\n            return self.source_iterator.__exit__(exc_type, exc, tb)\n        return False\n"
  },
  {
    "path": "memori/llm/_providers.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport warnings\n\nfrom memori.llm._base import BaseProvider\nfrom memori.llm._clients import Agno as AgnoMemoriClient\nfrom memori.llm._clients import Anthropic as AnthropicMemoriClient\nfrom memori.llm._clients import Google as GoogleMemoriClient\nfrom memori.llm._clients import LangChain as LangChainMemoriClient\nfrom memori.llm._clients import OpenAi as OpenAiMemoriClient\nfrom memori.llm._clients import PydanticAi as PydanticAiMemoriClient\nfrom memori.llm._clients import XAi as XAiMemoriClient\n\n\nclass Agno(BaseProvider):\n    def register(self, openai_chat=None, claude=None, gemini=None, xai=None):\n        warnings.warn(\n            \"memori.agno.register() is deprecated. Use memori.llm.register(client) instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if self.client is None:\n            self.client = AgnoMemoriClient(self.config).register(\n                openai_chat=openai_chat,\n                claude=claude,\n                gemini=gemini,\n                xai=xai,\n            )\n\n        return self.entity\n\n\nclass Anthropic(BaseProvider):\n    def register(self, client):\n        warnings.warn(\n            \"memori.anthropic.register() is deprecated. Use memori.llm.register(client) instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if self.client is None:\n            self.client = AnthropicMemoriClient(self.config).register(client)\n\n        return self.entity\n\n\nclass Google(BaseProvider):\n    def register(self, client):\n        warnings.warn(\n            \"memori.google.register() is deprecated. Use memori.llm.register(client) instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if self.client is None:\n            self.client = GoogleMemoriClient(self.config).register(client)\n\n        return self.entity\n\n\nclass LangChain(BaseProvider):\n    def register(\n        self, chatbedrock=None, chatgooglegenai=None, chatopenai=None, chatvertexai=None\n    ):\n        warnings.warn(\n            \"memori.langchain.register() is deprecated. Use memori.llm.register(client) instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if self.client is None:\n            self.client = LangChainMemoriClient(self.config).register(\n                chatbedrock=chatbedrock,\n                chatgooglegenai=chatgooglegenai,\n                chatopenai=chatopenai,\n                chatvertexai=chatvertexai,\n            )\n\n        return self.entity\n\n\nclass OpenAi(BaseProvider):\n    def register(self, client, stream=False):\n        warnings.warn(\n            \"memori.openai.register() is deprecated. Use memori.llm.register(client) instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if self.client is None:\n            self.client = OpenAiMemoriClient(self.config).register(\n                client, stream=stream\n            )\n\n        return self.entity\n\n\nclass PydanticAi(BaseProvider):\n    def register(self, client):\n        warnings.warn(\n            \"memori.pydantic_ai.register() is deprecated. Use memori.llm.register(client) instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if self.client is None:\n            self.client = PydanticAiMemoriClient(self.config).register(client)\n\n        return self.entity\n\n\nclass XAi(BaseProvider):\n    def register(self, client, stream=False):\n        warnings.warn(\n            \"memori.xai.register() is deprecated. Use memori.llm.register(client) instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        if self.client is None:\n            self.client = XAiMemoriClient(self.config).register(client, stream=stream)\n\n        return self.entity\n"
  },
  {
    "path": "memori/llm/_registry.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom memori._exceptions import UnsupportedLLMProviderError\nfrom memori.llm._base import BaseClient, BaseLlmAdaptor\n\n\nclass Registry:\n    _clients: dict[Callable[[Any], bool], type[BaseClient]] = {}\n    _adapters: dict[Callable[[str | None, str | None], bool], type[BaseLlmAdaptor]] = {}\n\n    @classmethod\n    def register_client(cls, matcher: Callable[[Any], bool]):\n        def decorator(client_class: type[BaseClient]):\n            cls._clients[matcher] = client_class\n            return client_class\n\n        return decorator\n\n    @classmethod\n    def register_adapter(cls, matcher: Callable[[str | None, str | None], bool]):\n        def decorator(adapter_class: type[BaseLlmAdaptor]):\n            cls._adapters[matcher] = adapter_class\n            return adapter_class\n\n        return decorator\n\n    def client(self, client_obj: Any, config) -> BaseClient:\n        for matcher, client_class in self._clients.items():\n            if matcher(client_obj):\n                return client_class(config)\n\n        module = type(client_obj).__module__\n        if module.startswith(\"langchain\"):\n            class_name = type(client_obj).__name__\n            param_hint = class_name.lower()\n            raise RuntimeError(\n                f\"LangChain models require named parameters. \"\n                f\"Use: llm.register({param_hint}=client) instead of llm.register(client)\"\n            )\n\n        provider = f\"{type(client_obj).__module__}.{type(client_obj).__name__}\"\n        raise UnsupportedLLMProviderError(provider)\n\n    def adapter(self, provider: str | None, title: str | None) -> BaseLlmAdaptor:\n        for matcher, adapter_class in self._adapters.items():\n            if matcher(provider, title):\n                return adapter_class()\n\n        provider_str = f\"framework={provider}, llm={title}\"\n        raise UnsupportedLLMProviderError(provider_str)\n\n\ndef register_llm(\n    memori,\n    client=None,\n    openai_chat=None,\n    claude=None,\n    gemini=None,\n    xai=None,\n    chatbedrock=None,\n    chatgooglegenai=None,\n    chatopenai=None,\n    chatvertexai=None,\n):\n    \"\"\"Register LLM clients or framework models.\n\n    For direct LLM clients:\n        llm.register(client)\n\n    For Agno models:\n        llm.register(openai_chat=model)\n        llm.register(claude=model)\n        llm.register(gemini=model)\n        llm.register(xai=model)\n\n    For LangChain models:\n        llm.register(chatbedrock=model)\n        llm.register(chatgooglegenai=model)\n        llm.register(chatopenai=model)\n        llm.register(chatvertexai=model)\n    \"\"\"\n    agno_args = [openai_chat, claude, gemini, xai]\n    langchain_args = [chatbedrock, chatgooglegenai, chatopenai, chatvertexai]\n\n    has_agno = any(arg is not None for arg in agno_args)\n    has_langchain = any(arg is not None for arg in langchain_args)\n\n    if client is not None and (has_agno or has_langchain):\n        raise RuntimeError(\n            \"Cannot mix direct client registration with framework registration\"\n        )\n\n    if has_agno and has_langchain:\n        raise RuntimeError(\n            \"Cannot register both Agno and LangChain clients in the same call\"\n        )\n\n    def _first_provider(*pairs: tuple[object | None, str]) -> str | None:\n        return next((provider for value, provider in pairs if value is not None), None)\n\n    if has_agno:\n        from memori.llm._constants import (\n            AGNO_ANTHROPIC_LLM_PROVIDER,\n            AGNO_FRAMEWORK_PROVIDER,\n            AGNO_GOOGLE_LLM_PROVIDER,\n            AGNO_OPENAI_LLM_PROVIDER,\n            AGNO_XAI_LLM_PROVIDER,\n        )\n\n        memori.config.framework.provider = AGNO_FRAMEWORK_PROVIDER\n        memori.config.llm.provider = _first_provider(\n            (openai_chat, AGNO_OPENAI_LLM_PROVIDER),\n            (claude, AGNO_ANTHROPIC_LLM_PROVIDER),\n            (gemini, AGNO_GOOGLE_LLM_PROVIDER),\n            (xai, AGNO_XAI_LLM_PROVIDER),\n        )\n\n        memori.agno.register(\n            openai_chat=openai_chat,\n            claude=claude,\n            gemini=gemini,\n            xai=xai,\n        )\n    elif has_langchain:\n        from memori.llm._constants import (\n            LANGCHAIN_CHATBEDROCK_LLM_PROVIDER,\n            LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n            LANGCHAIN_CHATVERTEXAI_LLM_PROVIDER,\n            LANGCHAIN_FRAMEWORK_PROVIDER,\n            LANGCHAIN_OPENAI_LLM_PROVIDER,\n        )\n\n        memori.config.framework.provider = LANGCHAIN_FRAMEWORK_PROVIDER\n        memori.config.llm.provider = _first_provider(\n            (chatbedrock, LANGCHAIN_CHATBEDROCK_LLM_PROVIDER),\n            (chatgooglegenai, LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER),\n            (chatopenai, LANGCHAIN_OPENAI_LLM_PROVIDER),\n            (chatvertexai, LANGCHAIN_CHATVERTEXAI_LLM_PROVIDER),\n        )\n\n        memori.langchain.register(\n            chatbedrock=chatbedrock,\n            chatgooglegenai=chatgooglegenai,\n            chatopenai=chatopenai,\n            chatvertexai=chatvertexai,\n        )\n    elif client is not None:\n        client_handler = Registry().client(client, memori.config)\n        client_handler.register(client)\n    else:\n        raise RuntimeError(\"No client or framework model provided to register\")\n\n    provider = getattr(memori.config.llm, \"provider\", None)\n    if provider is None or (isinstance(provider, str) and provider.strip() == \"\"):\n        raise UnsupportedLLMProviderError(\n            \"unknown (provider could not be determined during registration)\"\n        )\n\n    return memori\n"
  },
  {
    "path": "memori/llm/_streaming.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport json\nimport time\n\nfrom memori._config import Config\nfrom memori._utils import bytes_to_json\nfrom memori.llm._base import BaseInvoke\nfrom memori.llm._utils import client_is_bedrock\nfrom memori.memory._manager import Manager as MemoryManager\n\n\nclass StreamingBody:\n    def __init__(self, config: Config, source_streaming_body):\n        self.config = config\n        self.source_streaming_body = source_streaming_body\n        self.raw_response = None\n\n    def __getattr__(self, name):\n        return getattr(self.source_streaming_body, name)\n\n    def configure_invoke(self, invoke: BaseInvoke):\n        self.invoke = invoke\n        return self\n\n    def configure_request(self, kwargs, time_start):\n        self._kwargs = kwargs\n        self._time_start = time_start\n\n        if client_is_bedrock(self.config.framework.provider, self.config.llm.provider):\n            self._kwargs = bytes_to_json(self._kwargs)\n\n        return self\n\n    def read(self, *args, **kwargs):\n        data = self.source_streaming_body.read(*args, **kwargs)\n\n        MemoryManager(self.config).execute(\n            self.invoke._format_payload(\n                self.config.framework.provider,\n                self.config.llm.provider,\n                self.config.llm.version,\n                self._time_start,\n                time.time(),\n                self.invoke._format_kwargs(self._kwargs),\n                self.invoke._format_response(json.loads(data.decode())),\n            )\n        )\n\n        return data\n"
  },
  {
    "path": "memori/llm/_utils.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.llm._constants import (\n    AGNO_ANTHROPIC_LLM_PROVIDER,\n    AGNO_FRAMEWORK_PROVIDER,\n    AGNO_GOOGLE_LLM_PROVIDER,\n    AGNO_OPENAI_LLM_PROVIDER,\n    AGNO_XAI_LLM_PROVIDER,\n    ATHROPIC_LLM_PROVIDER,\n    GOOGLE_LLM_PROVIDER,\n    LANGCHAIN_CHATBEDROCK_LLM_PROVIDER,\n    LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n    LANGCHAIN_CHATVERTEXAI_LLM_PROVIDER,\n    LANGCHAIN_FRAMEWORK_PROVIDER,\n    LANGCHAIN_OPENAI_LLM_PROVIDER,\n    OPENAI_LLM_PROVIDER,\n    XAI_LLM_PROVIDER,\n)\n\n\ndef client_is_bedrock(provider, title):\n    return (\n        provider_is_langchain(provider) and title == LANGCHAIN_CHATBEDROCK_LLM_PROVIDER\n    )\n\n\ndef llm_is_anthropic(provider, title):\n    return title == ATHROPIC_LLM_PROVIDER\n\n\ndef llm_is_bedrock(provider, title):\n    return (\n        provider_is_langchain(provider) and title == LANGCHAIN_CHATBEDROCK_LLM_PROVIDER\n    )\n\n\ndef llm_is_google(provider, title):\n    return title == GOOGLE_LLM_PROVIDER or (\n        provider_is_langchain(provider)\n        and title\n        in [LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER, LANGCHAIN_CHATVERTEXAI_LLM_PROVIDER]\n    )\n\n\ndef llm_is_openai(provider, title):\n    return (\n        title == OPENAI_LLM_PROVIDER\n        or title == \"openai_responses\"\n        or (provider_is_langchain(provider) and title == LANGCHAIN_OPENAI_LLM_PROVIDER)\n    )\n\n\ndef llm_is_xai(provider, title):\n    return title == XAI_LLM_PROVIDER\n\n\ndef agno_is_anthropic(provider, title):\n    return provider_is_agno(provider) and title == AGNO_ANTHROPIC_LLM_PROVIDER\n\n\ndef agno_is_google(provider, title):\n    return provider_is_agno(provider) and title == AGNO_GOOGLE_LLM_PROVIDER\n\n\ndef agno_is_openai(provider, title):\n    return provider_is_agno(provider) and title == AGNO_OPENAI_LLM_PROVIDER\n\n\ndef agno_is_xai(provider, title):\n    return provider_is_agno(provider) and title == AGNO_XAI_LLM_PROVIDER\n\n\ndef provider_is_agno(provider):\n    return provider == AGNO_FRAMEWORK_PROVIDER\n\n\ndef provider_is_langchain(provider):\n    return provider == LANGCHAIN_FRAMEWORK_PROVIDER\n"
  },
  {
    "path": "memori/llm/_xai_wrappers.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport asyncio\nimport json\nimport time\n\nfrom google.protobuf import json_format\n\nfrom memori.llm._constants import XAI_LLM_PROVIDER\n\n\nclass XAiWrappers:\n    \"\"\"\n    Handles XAI-specific wrapping logic for the two-step API pattern.\n\n    XAI's API works differently than other providers:\n    1. create() returns a Chat object (no API call yet)\n    2. Chat.sample() or Chat.stream() makes the actual API call\n\n    This class encapsulates all the custom wrapping logic needed to handle\n    conversation injection, timing, and payload creation for XAI.\n    \"\"\"\n\n    def __init__(self, config):\n        self.config = config\n\n    def _ensure_cached_conversation_id(self) -> bool:\n        if self.config.storage is None or self.config.storage.driver is None:\n            return False\n\n        if self.config.session_id is None:\n            return False\n\n        driver = self.config.storage.driver\n\n        if self.config.cache.session_id is None:\n            if not hasattr(driver.session, \"read\"):\n                return False\n            self.config.cache.session_id = driver.session.read(\n                str(self.config.session_id)\n            )\n\n        if self.config.cache.session_id is None:\n            return False\n\n        if self.config.cache.conversation_id is None:\n            if not hasattr(driver.conversation, \"read_id_by_session_id\"):\n                return False\n            self.config.cache.conversation_id = (\n                driver.conversation.read_id_by_session_id(self.config.cache.session_id)\n            )\n\n        return self.config.cache.conversation_id is not None\n\n    def inject_conversation_history(self, kwargs):\n        \"\"\"Inject conversation history into messages using XAI message format.\"\"\"\n        from xai_sdk.chat import assistant, user\n\n        if self.config.cache.conversation_id is None:\n            if not self._ensure_cached_conversation_id():\n                return kwargs\n\n        messages = self.config.storage.driver.conversation.messages.read(\n            self.config.cache.conversation_id\n        )\n        if len(messages) == 0:\n            return kwargs\n\n        xai_messages = []\n        for message in messages:\n            role = message.get(\"role\", \"\")\n            content = message.get(\"content\", \"\")\n            if role == \"user\":\n                xai_messages.append(user(content))\n            elif role == \"assistant\":\n                xai_messages.append(assistant(content))\n\n        kwargs[\"messages\"] = xai_messages + kwargs.get(\"messages\", [])\n\n        return kwargs\n\n    def wrap_chat_methods(self, chat_obj, client_version, model=None):\n        \"\"\"Wrap the Chat object's sample() and stream() methods.\"\"\"\n        if hasattr(chat_obj, \"_memori_installed\"):\n            return\n\n        if model:\n            self.config.llm.version = model\n\n        chat_obj._sample = chat_obj.sample\n        is_async = asyncio.iscoroutinefunction(chat_obj._sample)\n\n        if is_async:\n            chat_obj.sample = self._create_async_sample_wrapper(\n                chat_obj, client_version\n            )\n        else:\n            chat_obj.sample = self._create_sync_sample_wrapper(chat_obj, client_version)\n\n        if hasattr(chat_obj, \"stream\"):\n            chat_obj._stream = chat_obj.stream\n            chat_obj.stream = self._create_stream_wrapper(chat_obj, client_version)\n\n        chat_obj._memori_installed = True\n\n    def _create_sync_sample_wrapper(self, chat_obj, client_version):\n        \"\"\"Create a synchronous wrapper for sample().\"\"\"\n        from memori.memory._manager import Manager as MemoryManager\n\n        def wrapped_sample(*sample_args, **sample_kwargs):\n            start = time.time()\n            response = chat_obj._sample(*sample_args, **sample_kwargs)\n\n            query_formatted = {\n                \"messages\": [\n                    json.loads(json_format.MessageToJson(msg))\n                    for msg in chat_obj.messages\n                ]\n            }\n            response_json = {\n                \"content\": response.content,\n                \"role\": self._normalize_role(response),\n            }\n\n            payload = self._build_payload(\n                query_formatted, response_json, client_version, start\n            )\n            MemoryManager(self.config).execute(payload)\n\n            if self.config.augmentation is not None:\n                from memori.memory.augmentation.input import AugmentationInput\n\n                messages = payload[\"conversation\"][\"query\"].get(\"messages\", [])\n                messages_for_aug = list(messages) if isinstance(messages, list) else []\n                messages_for_aug.append(\n                    {\"role\": \"assistant\", \"content\": response.content}\n                )\n\n                if self.config.entity_id or self.config.process_id:\n                    augmentation_input = AugmentationInput(\n                        conversation_id=self.config.cache.conversation_id,\n                        entity_id=self.config.entity_id,\n                        process_id=self.config.process_id,\n                        conversation_messages=messages_for_aug,\n                        system_prompt=None,\n                    )\n                    self.config.augmentation.enqueue(augmentation_input)\n\n            return response\n\n        return wrapped_sample\n\n    def _create_async_sample_wrapper(self, chat_obj, client_version):\n        \"\"\"Create an asynchronous wrapper for sample().\"\"\"\n        from memori.memory._manager import Manager as MemoryManager\n\n        async def wrapped_sample_async(*sample_args, **sample_kwargs):\n            start = time.time()\n            response = await chat_obj._sample(*sample_args, **sample_kwargs)\n\n            query_formatted = {\n                \"messages\": [\n                    json.loads(json_format.MessageToJson(msg))\n                    for msg in chat_obj.messages\n                ]\n            }\n            response_json = {\n                \"content\": response.content,\n                \"role\": self._normalize_role(response),\n            }\n\n            payload = self._build_payload(\n                query_formatted, response_json, client_version, start\n            )\n            MemoryManager(self.config).execute(payload)\n\n            if self.config.augmentation is not None:\n                from memori.memory.augmentation.input import AugmentationInput\n\n                messages = payload[\"conversation\"][\"query\"].get(\"messages\", [])\n                messages_for_aug = list(messages) if isinstance(messages, list) else []\n                messages_for_aug.append(\n                    {\"role\": \"assistant\", \"content\": response.content}\n                )\n\n                if self.config.entity_id or self.config.process_id:\n                    augmentation_input = AugmentationInput(\n                        conversation_id=self.config.cache.conversation_id,\n                        entity_id=self.config.entity_id,\n                        process_id=self.config.process_id,\n                        conversation_messages=messages_for_aug,\n                        system_prompt=None,\n                    )\n                    self.config.augmentation.enqueue(augmentation_input)\n\n            return response\n\n        return wrapped_sample_async\n\n    def _create_stream_wrapper(self, chat_obj, client_version):\n        \"\"\"Create an asynchronous wrapper for stream().\"\"\"\n        from memori.memory._manager import Manager as MemoryManager\n\n        async def wrapped_stream(*stream_args, **stream_kwargs):\n            start = time.time()\n            full_content = []\n            last_response = None\n\n            async for item in chat_obj._stream(*stream_args, **stream_kwargs):\n                if isinstance(item, tuple) and len(item) == 2:\n                    response, delta = item\n                    last_response = response\n                    if hasattr(delta, \"content\") and delta.content:\n                        full_content.append(delta.content)\n                elif hasattr(item, \"content\") and item.content:\n                    full_content.append(item.content)\n                    last_response = item\n\n                yield item\n\n            if full_content and last_response:\n                query_formatted = {\n                    \"messages\": [\n                        json.loads(json_format.MessageToJson(msg))\n                        for msg in chat_obj.messages\n                    ]\n                }\n                response_json = {\n                    \"content\": \"\".join(full_content),\n                    \"role\": self._normalize_role(last_response)\n                    if hasattr(last_response, \"role\")\n                    else \"assistant\",\n                }\n\n                payload = self._build_payload(\n                    query_formatted, response_json, client_version, start\n                )\n                MemoryManager(self.config).execute(payload)\n\n                if self.config.augmentation is not None:\n                    from memori.memory.augmentation.input import AugmentationInput\n\n                    messages = payload[\"conversation\"][\"query\"].get(\"messages\", [])\n                    messages_for_aug = (\n                        list(messages) if isinstance(messages, list) else []\n                    )\n                    messages_for_aug.append(\n                        {\"role\": \"assistant\", \"content\": \"\".join(full_content)}\n                    )\n\n                    if self.config.entity_id or self.config.process_id:\n                        augmentation_input = AugmentationInput(\n                            conversation_id=self.config.cache.conversation_id,\n                            entity_id=self.config.entity_id,\n                            process_id=self.config.process_id,\n                            conversation_messages=messages_for_aug,\n                            system_prompt=None,\n                        )\n                        self.config.augmentation.enqueue(augmentation_input)\n\n        return wrapped_stream\n\n    def _build_payload(\n        self, query_formatted, response_json, client_version, start_time\n    ):\n        \"\"\"Build the payload for memory storage.\"\"\"\n        return {\n            \"attribution\": {\n                \"entity\": {\"id\": self.config.entity_id},\n                \"process\": {\"id\": self.config.process_id},\n            },\n            \"conversation\": {\n                \"client\": {\n                    \"provider\": self.config.framework.provider,\n                    \"title\": self.config.llm.provider or XAI_LLM_PROVIDER,\n                    \"version\": client_version,\n                },\n                \"query\": query_formatted,\n                \"response\": response_json,\n            },\n            \"meta\": {\n                \"api\": {\"key\": self.config.api_key},\n                \"fnfg\": {\"exc\": None, \"status\": \"succeeded\"},\n                \"sdk\": {\"client\": \"python\", \"version\": self.config.version},\n            },\n            \"session\": {\"uuid\": str(self.config.session_id)},\n            \"time\": {\"end\": time.time(), \"start\": start_time},\n        }\n\n    def _normalize_role(self, response):\n        \"\"\"Normalize protobuf role names to standard format.\"\"\"\n        role_str = (\n            response.role.name if hasattr(response.role, \"name\") else str(response.role)\n        )\n\n        role_map = {\n            \"ROLE_ASSISTANT\": \"assistant\",\n            \"ROLE_USER\": \"user\",\n            \"ROLE_SYSTEM\": \"system\",\n        }\n\n        return role_map.get(role_str, role_str.lower().replace(\"role_\", \"\"))\n"
  },
  {
    "path": "memori/llm/adapters/anthropic/__init__.py",
    "content": "from memori.llm.adapters.anthropic._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/llm/adapters/anthropic/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.llm._base import BaseLlmAdaptor\nfrom memori.llm._registry import Registry\nfrom memori.llm._utils import agno_is_anthropic, llm_is_anthropic\n\n\n@Registry.register_adapter(llm_is_anthropic)\n@Registry.register_adapter(agno_is_anthropic)\nclass Adapter(BaseLlmAdaptor):\n    def get_formatted_query(self, payload):\n        \"\"\"\n        [\n            {\n                \"content\": \"...\",\n                \"role\": \"...\"\n            }\n        ]\n        \"\"\"\n\n        try:\n            messages = payload[\"conversation\"][\"query\"].get(\"messages\", [])\n            return self._exclude_injected_messages(messages, payload)\n        except KeyError:\n            return []\n\n    def get_formatted_response(self, payload):\n        try:\n            content = payload[\"conversation\"][\"response\"].get(\"content\", None)\n        except KeyError:\n            return []\n\n        response = []\n        if content is not None:\n            # Unstreamed\n            # [\n            #   {\n            #       \"text\": \"...\",\n            #       \"type\": \"...\"\n            #   }\n            # ]\n            for entry in content:\n                response.append(\n                    {\n                        \"role\": payload[\"conversation\"][\"response\"].get(\"role\", None),\n                        \"text\": entry[\"text\"],\n                        \"type\": entry[\"type\"],\n                    }\n                )\n        else:\n            # Streamed\n            \"\"\"\n            REQUEST FOR CONTRIBUTION\n\n            How do we parse the messages when Anthropic streams the response?\n            \"\"\"\n            raise RuntimeError(\"REQUEST FOR CONTRIBUTION\")\n\n        return response\n"
  },
  {
    "path": "memori/llm/adapters/bedrock/__init__.py",
    "content": "from memori.llm.adapters.bedrock._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/llm/adapters/bedrock/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.llm._base import BaseLlmAdaptor\nfrom memori.llm._registry import Registry\nfrom memori.llm._utils import llm_is_bedrock\n\n\n@Registry.register_adapter(llm_is_bedrock)\nclass Adapter(BaseLlmAdaptor):\n    def get_formatted_query(self, payload):\n        \"\"\"\n        [\n            {\n                \"content\": \"...\",\n                \"role\": \"...\"\n            }\n        ]\n        \"\"\"\n\n        try:\n            messages = payload[\"conversation\"][\"query\"][\"body\"][\"messages\"]\n            return self._exclude_injected_messages(messages, payload)\n        except KeyError:\n            return []\n\n    def get_formatted_response(self, payload):\n        try:\n            if not isinstance(payload[\"conversation\"][\"response\"], list):\n                return []\n        except KeyError:\n            return []\n\n        response = []\n        if \"chunk\" not in payload[\"conversation\"][\"response\"][0]:\n            # Unstreamed\n            \"\"\"\n            REQUEST FOR CONTRIBUTION\n\n            How do we parse the messages when Bedrock does not stream the response?\n            \"\"\"\n            raise RuntimeError(\"REQUEST FOR CONTRIBUTION\")\n        else:\n            # Streamed\n            # [\n            #   {\n            #       \"chunk\": {\n            #           \"bytes\": {\n            #               \"delta\": {\n            #                   \"text\": \"...\",\n            #                   \"type\": \"...\"\n            #               }\n            #           }\n            #       }\n            #   }\n            # ]\n            response = []\n            text = []\n            role = None\n            for entry in payload[\"conversation\"][\"response\"]:\n                chunk = entry.get(\"chunk\", None)\n                if chunk is not None:\n                    bytes_ = chunk.get(\"bytes\", None)\n                    if bytes_ is not None:\n                        message = bytes_.get(\"message\", None)\n                        if message is not None:\n                            role = message[\"role\"]\n                        else:\n                            delta = bytes_.get(\"delta\", None)\n                            if delta is not None:\n                                text_content = delta.get(\"text\", None)\n                                if text_content is not None and len(text_content) > 0:\n                                    text.append(text_content)\n\n            if len(text) > 0:\n                response.append({\"role\": role, \"text\": \"\".join(text), \"type\": \"text\"})\n\n        return response\n"
  },
  {
    "path": "memori/llm/adapters/google/__init__.py",
    "content": "from memori.llm.adapters.google._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/llm/adapters/google/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.llm._base import BaseLlmAdaptor\nfrom memori.llm._registry import Registry\nfrom memori.llm._utils import agno_is_google, llm_is_google\n\n\n@Registry.register_adapter(llm_is_google)\n@Registry.register_adapter(agno_is_google)\nclass Adapter(BaseLlmAdaptor):\n    def get_formatted_query(self, payload):\n        \"\"\"\n        [\n          {\n              \"parts\": [\n                  {\n                      \"text\": \"...\"\n                  }\n              ],\n              \"role\": \"...\"\n          }\n        ]\n        \"\"\"\n\n        messages = []\n\n        try:\n            system_instruction = payload[\"conversation\"][\"query\"].get(\n                \"systemInstruction\", None\n            )\n        except KeyError:\n            system_instruction = None\n\n        if system_instruction is not None:\n            parts = system_instruction.get(\"parts\", None)\n            content = []\n            if parts is not None:\n                for part in parts:\n                    text = part.get(\"text\", None)\n                    if text is not None and len(text) > 0:\n                        content.append(text)\n\n            if len(content) > 0:\n                messages.append({\"content\": \" \".join(content), \"role\": \"system\"})\n\n        try:\n            contents = payload[\"conversation\"][\"query\"].get(\"contents\", None)\n        except KeyError:\n            return []\n\n        if contents is not None:\n            if isinstance(contents, str):\n                messages.append({\"content\": contents, \"role\": \"user\"})\n            else:\n                for entry in contents:\n                    if isinstance(entry, str):\n                        messages.append({\"content\": entry, \"role\": \"user\"})\n                        continue\n\n                    parts = entry.get(\"parts\", None)\n                    content = []\n                    if parts is not None:\n                        for part in parts:\n                            if isinstance(part, str):\n                                content.append(part)\n                            else:\n                                text = part.get(\"text\", None)\n                                if text is not None and len(text) > 0:\n                                    content.append(text)\n\n                    if len(content) > 0:\n                        messages.append(\n                            {\n                                \"content\": \" \".join(content),\n                                \"role\": entry.get(\"role\", \"user\"),\n                            }\n                        )\n\n        return self._exclude_injected_messages(messages, payload)\n\n    def get_formatted_response(self, payload):\n        try:\n            payload[\"conversation\"][\"response\"]\n        except KeyError:\n            return []\n\n        response = []\n        if not isinstance(payload[\"conversation\"][\"response\"], list):\n            try:\n                candidates = payload[\"conversation\"][\"response\"].get(\"candidates\", None)\n            except KeyError:\n                return []\n\n            if candidates is not None:\n                # Unstreamed\n                # [\n                #   {\n                #       \"content\": {\n                #           \"parts\": [\n                #               {\n                #                   \"text\": \"...\"\n                #               }\n                #           ],\n                #           \"role\": \"model\"\n                #       }\n                #   }\n                # ]\n                for candidate in candidates:\n                    content = candidate.get(\"content\", None)\n                    if content is not None:\n                        parts = content.get(\"parts\", None)\n                        if parts is not None:\n                            text = []\n                            for part in parts:\n                                text_content = part.get(\"text\", None)\n                                if text_content is not None:\n                                    text.append(text_content)\n\n                            if len(text) > 0:\n                                response.append(\n                                    {\n                                        \"role\": content[\"role\"],\n                                        \"text\": \"\".join(text),\n                                        \"type\": \"text\",\n                                    }\n                                )\n        elif (\n            len(payload[\"conversation\"][\"response\"]) > 0\n            and \"candidates\" in payload[\"conversation\"][\"response\"][0]\n        ):\n            # Streamed\n            # [\n            #     {\n            #         \"candidates\": [\n            #             {\n            #                 \"content\": {\n            #                     \"parts\": [\n            #                         {\n            #                             \"text\": \"...\"\n            #                         }\n            #                     ],\n            #                     \"role\": \"model\"\n            #                 }\n            #             }\n            #         ]\n            #     }\n            # ]\n            text = []\n            role = None\n            for entry in payload[\"conversation\"][\"response\"]:\n                candidates = entry.get(\"candidates\", None)\n                if candidates is not None:\n                    for candidate in candidates:\n                        content = candidate.get(\"content\", None)\n                        if content is not None:\n                            parts = content.get(\"parts\", None)\n                            if parts is not None:\n                                for part in parts:\n                                    text_content = part.get(\"text\", None)\n                                    if (\n                                        text_content is not None\n                                        and len(text_content) > 0\n                                    ):\n                                        text.append(text_content)\n\n                            if role is None:\n                                role = content.get(\"role\", None)\n\n            if len(text) > 0:\n                response.append({\"role\": role, \"text\": \"\".join(text), \"type\": \"text\"})\n\n        return response\n"
  },
  {
    "path": "memori/llm/adapters/openai/__init__.py",
    "content": "from memori.llm.adapters.openai._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/llm/adapters/openai/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.llm._base import BaseLlmAdaptor\nfrom memori.llm._registry import Registry\nfrom memori.llm._utils import agno_is_openai, llm_is_openai\n\n\n@Registry.register_adapter(llm_is_openai)\n@Registry.register_adapter(agno_is_openai)\nclass Adapter(BaseLlmAdaptor):\n    def get_formatted_query(self, payload):\n        \"\"\"\n        [\n            {\n                \"content\": \"...\",\n                \"role\": \"...\"\n            }\n        ]\n        \"\"\"\n\n        try:\n            query = payload[\"conversation\"][\"query\"]\n        except KeyError:\n            return []\n\n        if \"input\" in query or \"instructions\" in query:\n            messages = []\n            instructions = query.get(\"instructions\", \"\")\n            if instructions:\n                clean = (\n                    instructions.split(\"<memori_context>\")[0].strip()\n                    if \"<memori_context>\" in instructions\n                    else instructions\n                )\n                if clean:\n                    messages.append({\"role\": \"system\", \"content\": clean})\n\n            input_val = query.get(\"input\", [])\n            if isinstance(input_val, str):\n                messages.append({\"role\": \"user\", \"content\": input_val})\n            elif isinstance(input_val, list):\n                for item in input_val:\n                    if isinstance(item, dict):\n                        role, content = (\n                            item.get(\"role\", \"user\"),\n                            item.get(\"content\", \"\"),\n                        )\n                        if isinstance(content, str):\n                            messages.append({\"role\": role, \"content\": content})\n                        elif isinstance(content, list):\n                            text_parts = []\n                            for c in content:\n                                if (\n                                    isinstance(c, dict)\n                                    and c.get(\"type\") == \"input_text\"\n                                ):\n                                    text_parts.append(c.get(\"text\", \"\"))\n                                elif isinstance(c, str):\n                                    text_parts.append(c)\n                            text = \" \".join(text_parts)\n                            if text.strip():\n                                messages.append({\"role\": role, \"content\": text})\n            return self._exclude_injected_messages(messages, payload)\n\n        messages = query.get(\"messages\", [])\n        return self._exclude_injected_messages(messages, payload)\n\n    def get_formatted_response(self, payload):\n        try:\n            response = payload[\"conversation\"][\"response\"]\n        except KeyError:\n            return []\n\n        if \"output\" in response or \"output_text\" in response:\n            results = []\n            for item in response.get(\"output\", []):\n                if isinstance(item, dict) and item.get(\"type\") == \"message\":\n                    for content in item.get(\"content\", []):\n                        if isinstance(content, dict):\n                            if content.get(\"type\") == \"output_text\":\n                                results.append(\n                                    {\n                                        \"role\": \"assistant\",\n                                        \"text\": content.get(\"text\", \"\"),\n                                        \"type\": \"text\",\n                                    }\n                                )\n                            elif content.get(\"type\") == \"refusal\":\n                                results.append(\n                                    {\n                                        \"role\": \"assistant\",\n                                        \"text\": content.get(\"refusal\", \"\"),\n                                        \"type\": \"refusal\",\n                                    }\n                                )\n            if not results and response.get(\"output_text\"):\n                results.append(\n                    {\n                        \"role\": \"assistant\",\n                        \"text\": response.get(\"output_text\", \"\"),\n                        \"type\": \"text\",\n                    }\n                )\n            return results\n\n        # Chat Completions API format\n        choices = response.get(\"choices\", None)\n        results = []\n        if choices is not None:\n            if payload[\"conversation\"][\"query\"].get(\"stream\", None) is None:\n                # Unstreamed\n                # [\n                #   {\n                #       \"finish_reason\": \"...\",\n                #       \"index\": ...,\n                #       \"logprobs\": ...,\n                #       \"message\": {\n                #           \"annotations\": ...,\n                #           \"audio\": ...,\n                #           \"content\": \"...\",\n                #           \"functional_calls\": ...,\n                #           \"parsed\": ...,\n                #           \"refusal\": ...,\n                #           \"role\": \"...\",\n                #           \"tool_calls\": ...\n                #       }\n                #   }\n                # ]\n                for choice in choices:\n                    message = choice.get(\"message\", None)\n                    if message is not None:\n                        content = message.get(\"content\", None)\n                        if content is not None:\n                            results.append(\n                                {\n                                    \"role\": message[\"role\"],\n                                    \"text\": content,\n                                    \"type\": \"text\",\n                                }\n                            )\n            else:\n                # Streamed\n                # [\n                #   {\n                #       \"delta\": {\n                #           \"content\": \"...\",\n                #           \"function_call\": ...,\n                #           \"refusal\": ...,\n                #           \"role\": \"...\",\n                #           \"tool_calls\": ...\n                #       }\n                #   }\n                # ]\n                content = []\n                role = None\n                for choice in choices:\n                    delta = choice.get(\"delta\", None)\n                    if delta is not None:\n                        if role is None:\n                            role = delta.get(\"role\", None)\n\n                        text_content = delta.get(\"content\", None)\n                        if text_content is not None and len(text_content) > 0:\n                            content.append(text_content)\n\n                if len(content) > 0:\n                    results.append(\n                        {\"role\": role, \"text\": \"\".join(content), \"type\": \"text\"}\n                    )\n\n        return results\n"
  },
  {
    "path": "memori/llm/adapters/xai/__init__.py",
    "content": "from memori.llm.adapters.xai._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/llm/adapters/xai/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nfrom memori.llm._base import BaseLlmAdaptor\nfrom memori.llm._registry import Registry\nfrom memori.llm._utils import agno_is_xai, llm_is_xai\n\n\n@Registry.register_adapter(llm_is_xai)\n@Registry.register_adapter(agno_is_xai)\nclass Adapter(BaseLlmAdaptor):\n    def get_formatted_query(self, payload):\n        try:\n            raw_messages = payload[\"conversation\"][\"query\"].get(\"messages\", [])\n        except KeyError:\n            return []\n\n        messages = []\n        for msg in raw_messages:\n            role_str = msg.get(\"role\", \"\")\n            role = None\n\n            if role_str == \"ROLE_USER\":\n                role = \"user\"\n            elif role_str == \"ROLE_ASSISTANT\":\n                role = \"assistant\"\n            elif role_str == \"ROLE_SYSTEM\":\n                role = \"system\"\n            elif role_str in (\"user\", \"assistant\", \"system\"):\n                role = role_str\n\n            if role is not None:\n                content_parts = msg.get(\"content\", [])\n\n                if isinstance(content_parts, str):\n                    messages.append({\"role\": role, \"content\": content_parts})\n                elif isinstance(content_parts, list):\n                    content_texts = []\n                    for part in content_parts:\n                        text = part.get(\"text\", None)\n                        if text is not None:\n                            content_texts.append(text)\n\n                    if len(content_texts) > 0:\n                        messages.append(\n                            {\"role\": role, \"content\": \" \".join(content_texts)}\n                        )\n\n        return self._exclude_injected_messages(messages, payload)\n\n    def get_formatted_response(self, payload):\n        try:\n            response_data = payload[\"conversation\"][\"response\"]\n        except KeyError:\n            return []\n\n        response = []\n\n        if \"choices\" in response_data:\n            for choice in response_data.get(\"choices\", []):\n                message = choice.get(\"message\", {})\n                content = message.get(\"content\", None)\n                role = message.get(\"role\", None)\n                if content is not None and role is not None:\n                    response.append({\"role\": role, \"text\": content, \"type\": \"text\"})\n        else:\n            content = response_data.get(\"content\", None)\n            role = response_data.get(\"role\", None)\n\n            if content is not None and role is not None:\n                if isinstance(content, str):\n                    response.append({\"role\": role, \"text\": content, \"type\": \"text\"})\n                elif isinstance(content, list):\n                    for item in content:\n                        if isinstance(item, dict):\n                            text = item.get(\"text\", None)\n                            if text is not None:\n                                response.append(\n                                    {\"role\": role, \"text\": text, \"type\": \"text\"}\n                                )\n                        elif isinstance(item, str):\n                            response.append(\n                                {\"role\": role, \"text\": item, \"type\": \"text\"}\n                            )\n\n        return response\n"
  },
  {
    "path": "memori/memory/_collector.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport json\nimport os\nimport pprint\nimport traceback\n\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\n\nfrom memori._config import Config\n\n\nclass Api:\n    def __init__(self, config: Config):\n        self.__base = os.environ.get(\"MEMORI_API_URL_BASE\")\n        if self.__base is None:\n            self.__base = \"https://api.memorilabs.ai\"\n\n        self.config = config\n\n    def get(self, route):\n        r = self.__session().get(\n            self.url(route), headers={\"Authorization\": f\"Bearer {self.config.api_key}\"}\n        )\n\n        r.raise_for_status()\n\n        return r.json()\n\n    def patch(self, route, json=None):\n        if json is None:\n            json = {}\n        r = self.__session().patch(\n            self.url(route),\n            headers={\"Authorization\": f\"Bearer {self.config.api_key}\"},\n            json=json,\n        )\n\n        r.raise_for_status()\n\n        return r.json()\n\n    def post(self, route, json=None):\n        if json is None:\n            json = {}\n        r = self.__session().post(\n            self.url(route),\n            headers={\"Authorization\": f\"Bearer {self.config.api_key}\"},\n            json=json,\n        )\n\n        r.raise_for_status()\n\n        return r.json()\n\n    def __session(self):\n        adapter = HTTPAdapter(\n            max_retries=_ApiRetryRecoverable(\n                allowed_methods=[\"GET\", \"PATCH\", \"POST\", \"PUT\", \"DELETE\"],\n                backoff_factor=1,\n                raise_on_status=False,\n                status=None,\n                total=5,\n            )\n        )\n\n        session = requests.Session()\n        session.mount(\"https://\", adapter)\n        session.mount(\"http://\", adapter)\n\n        return session\n\n    def url(self, route):\n        return f\"{self.__base}/v1/-/{route}\"\n\n\nclass _ApiRetryRecoverable(Retry):\n    def is_retry(self, method, status_code, has_retry_after=False):\n        return 500 <= status_code <= 599\n\n\nclass Collector:\n    def __init__(self, config: Config):\n        self.__base = os.environ.get(\"MEMORI_COLLECTOR_URL_BASE\")\n        if self.__base is None:\n            self.__base = \"https://api.memorilabs.ai\"\n\n        self.config = config\n\n    def fire_and_forget(self, payload):\n        if not self.config.is_test_mode():\n            try:\n                requests.post(\n                    f\"{self.__base}/rec\",\n                    json=payload,\n                    timeout=self.config.request_secs_timeout,\n                )\n            except Exception:\n                payload[\"meta\"][\"fnfg\"] = {\n                    \"exc\": traceback.format_exc(),\n                    \"status\": \"recovered\",\n                }\n\n                try:\n                    requests.post(\n                        f\"{self.__base}/rec\",\n                        json=json.loads(json.dumps(payload, default=str)),\n                        timeout=self.config.request_secs_timeout,\n                    )\n                except Exception:\n                    if self.config.raise_final_request_attempt is True:\n                        raise\n        else:\n            pprint.pprint(payload)\n\n        return self\n"
  },
  {
    "path": "memori/memory/_conversation_messages.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport json\nfrom collections.abc import Iterator\nfrom typing import Any, TypedDict\n\n\nclass ConversationMessage(TypedDict):\n    role: str\n    type: str | None\n    text: str\n\n\ndef _stringify_content(content: Any) -> str:\n    if isinstance(content, dict | list):\n        return json.dumps(content)\n    return str(content)\n\n\ndef parse_payload_conversation_messages(\n    payload: dict,\n    *,\n    adapter: Any | None = None,\n    registry: Any | None = None,\n) -> Iterator[ConversationMessage]:\n    \"\"\"Yield normalized conversation messages parsed from an LLM payload.\n\n    Normalization rules:\n    - Query messages: set `type=None`, stringify `content`\n    \"\"\"\n    conversation = payload.get(\"conversation\") if isinstance(payload, dict) else None\n    if isinstance(conversation, dict):\n        existing = conversation.get(\"messages\")\n        if (\n            isinstance(existing, list)\n            and \"query\" not in conversation\n            and \"response\" not in conversation\n        ):\n            for message in existing:\n                if not isinstance(message, dict):\n                    continue\n                role = message.get(\"role\")\n                text = message.get(\"text\")\n                if role is None or text is None:\n                    continue\n                yield {\n                    \"role\": str(role),\n                    \"type\": message.get(\"type\"),\n                    \"text\": str(text),\n                }\n            return\n\n    if adapter is None:\n        if registry is None:\n            from memori.llm._registry import Registry as LlmRegistry\n\n            registry = LlmRegistry()\n        adapter = registry.adapter(\n            payload[\"conversation\"][\"client\"][\"provider\"],\n            payload[\"conversation\"][\"client\"][\"title\"],\n        )\n\n    for message in adapter.get_formatted_query(payload) or []:\n        yield {\n            \"role\": message[\"role\"],\n            \"type\": None,\n            \"text\": _stringify_content(message[\"content\"]),\n        }\n\n    for response in adapter.get_formatted_response(payload) or []:\n        yield {\n            \"role\": response[\"role\"],\n            \"type\": response.get(\"type\"),\n            \"text\": response[\"text\"],\n        }\n"
  },
  {
    "path": "memori/memory/_manager.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport logging\nimport time\n\nfrom memori._config import Config\nfrom memori._exceptions import MemoriApiError\nfrom memori._network import Api\nfrom memori.memory._writer import Writer\n\nlogger = logging.getLogger(__name__)\n\n\nclass Manager:\n    def __init__(self, config: Config):\n        self.config = config\n\n    def execute(self, payload):\n        logger.debug(\"Memory manager execution started\")\n        # Make a copy of the payload and strip out the system messages while preserving the original\n        payload_stripped = payload.copy()\n        payload_stripped[\"messages\"] = [\n            message\n            for message in payload[\"messages\"]\n            if message.get(\"role\") != \"system\"\n        ]\n\n        if self.config.cloud is True:\n            self._handle_cloud(payload_stripped)\n        else:\n            Writer(self.config).execute(payload_stripped)\n        logger.debug(\"Memory manager execution completed\")\n\n        return self\n\n    def _handle_cloud(self, payload):\n        api = Api(self.config)\n        attempts = max(1, int(getattr(self.config, \"request_num_backoff\", 1) or 1))\n        backoff_factor = float(getattr(self.config, \"request_backoff_factor\", 1) or 1)\n\n        last_error: Exception | None = None\n        last_status: int | None = None\n\n        for attempt in range(attempts):\n            try:\n                last_status = api.post(\n                    \"cloud/conversation/messages\",\n                    payload,\n                    status_code=True,\n                )\n                if last_status == 201:\n                    self._persist_cloud_messages_locally(payload)\n                    return\n                last_error = None\n            except Exception as e:  # noqa: BLE001\n                last_error = e\n\n            if attempt < attempts - 1:\n                time.sleep(backoff_factor * (2**attempt))\n\n        if last_error is not None:\n            raise last_error\n\n        raise MemoriApiError(\n            f\"Expected 201 from cloud API but received {last_status} after {attempts} attempts\"\n        )\n\n    def _ensure_cached_id(self, cache_attr: str, create_func, *create_args) -> int:\n        cached_id = getattr(self.config.cache, cache_attr)\n        if cached_id is None:\n            cached_id = create_func(*create_args)\n            if cached_id is None:\n                raise RuntimeError(f\"{cache_attr} is unexpectedly None\")\n            setattr(self.config.cache, cache_attr, cached_id)\n        return cached_id\n\n    def _persist_cloud_messages_locally(self, payload: dict) -> None:\n        storage = getattr(self.config, \"storage\", None)\n        driver = getattr(storage, \"driver\", None) if storage is not None else None\n        if driver is None:\n            return\n\n        if self.config.entity_id is not None:\n            self._ensure_cached_id(\n                \"entity_id\",\n                driver.entity.create,\n                self.config.entity_id,\n            )\n\n        if self.config.process_id is not None:\n            self._ensure_cached_id(\n                \"process_id\",\n                driver.process.create,\n                self.config.process_id,\n            )\n\n        self._ensure_cached_id(\n            \"session_id\",\n            driver.session.create,\n            self.config.session_id,\n            self.config.cache.entity_id,\n            self.config.cache.process_id,\n        )\n\n        self._ensure_cached_id(\n            \"conversation_id\",\n            driver.conversation.create,\n            self.config.cache.session_id,\n            self.config.session_timeout_minutes,\n        )\n\n        messages = payload.get(\"messages\") if isinstance(payload, dict) else None\n        if not isinstance(messages, list):\n            return\n\n        for message in messages:\n            if not isinstance(message, dict):\n                continue\n            role = message.get(\"role\")\n            text = message.get(\"text\")\n            if role is None or text is None:\n                continue\n            driver.conversation.message.create(\n                self.config.cache.conversation_id,\n                role,\n                message.get(\"type\"),\n                str(text),\n            )\n\n        adapter = getattr(storage, \"adapter\", None)\n        if adapter is not None:\n            adapter.flush()\n            adapter.commit()\n"
  },
  {
    "path": "memori/memory/_struct.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\n\ndef build_fact_text_from_triple_entry(entry: dict) -> str | None:\n    content = entry.get(\"content\")\n    if isinstance(content, str) and content:\n        return content\n\n    subject = entry.get(\"subject\") or {}\n    predicate = entry.get(\"predicate\")\n    object_ = entry.get(\"object\") or {}\n\n    subject_name = subject.get(\"name\")\n    object_name = object_.get(\"name\")\n    if not subject_name or not predicate or not object_name:\n        return None\n\n    return f\"{subject_name} {predicate} {object_name}\"\n\n\nclass Conversation:\n    def __init__(self):\n        self.summary: str | None = None\n\n    def configure_from_advanced_augmentation(self, json_: dict) -> \"Conversation\":\n        conversation = json_.get(\"conversation\", None)\n        if conversation is None:\n            return self\n\n        self.summary = conversation.get(\"summary\", None)\n\n        return self\n\n\nclass Entity:\n    def __init__(self):\n        self.facts: list[str] = []\n        self.fact_embeddings: list[list[float]] = []\n        self.semantic_triples: list[SemanticTriple] = []\n\n    def configure_from_advanced_augmentation(self, json_: dict) -> \"Entity\":\n        entity = json_.get(\"entity\", None)\n        if entity is None:\n            return self\n\n        self.facts.extend(entity.get(\"facts\", []))\n        self.fact_embeddings.extend(entity.get(\"fact_embeddings\", []))\n\n        semantic_triples = entity.get(\"semantic_triples\", [])\n        triples = entity.get(\"triples\", [])\n\n        for entry in semantic_triples:\n            triple = self._parse_semantic_triple(entry)\n            if triple is not None:\n                self.semantic_triples.append(triple)\n\n        for entry in triples:\n            triple = self._parse_semantic_triple(entry)\n            if triple is not None:\n                self.semantic_triples.append(triple)\n                fact_text = build_fact_text_from_triple_entry(entry)\n                if fact_text is not None:\n                    self.facts.append(fact_text)\n\n        return self\n\n    def _parse_semantic_triple(self, entry: dict) -> \"SemanticTriple | None\":\n        \"\"\"Parse a semantic triple from API response.\"\"\"\n        subject = entry.get(\"subject\")\n        predicate = entry.get(\"predicate\")\n        object_ = entry.get(\"object\")\n\n        if not subject or not predicate or not object_:\n            return None\n\n        subject_name = subject.get(\"name\")\n        subject_type = subject.get(\"type\")\n        object_name = object_.get(\"name\")\n        object_type = object_.get(\"type\")\n\n        if not all([subject_name, subject_type, object_name, object_type]):\n            return None\n\n        triple = SemanticTriple()\n        triple.subject_name = subject_name\n        triple.subject_type = subject_type.lower()\n        triple.predicate = predicate\n        triple.object_name = object_name\n        triple.object_type = object_type.lower()\n\n        return triple\n\n\nclass Memories:\n    def __init__(self):\n        self.conversation: Conversation = Conversation()\n        self.entity: Entity = Entity()\n        self.process: Process = Process()\n\n    def configure_from_advanced_augmentation(self, json_: dict) -> \"Memories\":\n        self.conversation = Conversation().configure_from_advanced_augmentation(json_)\n        self.entity = Entity().configure_from_advanced_augmentation(json_)\n        self.process = Process().configure_from_advanced_augmentation(json_)\n        return self\n\n\nclass Process:\n    def __init__(self):\n        self.attributes: list[str] = []\n\n    def configure_from_advanced_augmentation(self, json_: dict) -> \"Process\":\n        process = json_.get(\"process\", None)\n        if process is None:\n            return self\n\n        self.attributes.extend(process.get(\"attributes\", []))\n\n        return self\n\n\nclass SemanticTriple:\n    def __init__(self):\n        self.subject_name: str | None = None\n        self.subject_type: str | None = None\n        self.predicate: str | None = None\n        self.object_name: str | None = None\n        self.object_type: str | None = None\n"
  },
  {
    "path": "memori/memory/_writer.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport logging\nimport time\n\nfrom sqlalchemy.exc import OperationalError\n\nfrom memori._config import Config\n\nlogger = logging.getLogger(__name__)\n\nMAX_RETRIES = 3\nRETRY_BACKOFF_BASE = 0.1\n\n\nclass Writer:\n    def __init__(self, config: Config):\n        self.config = config\n\n    def execute(self, payload: dict, max_retries: int = MAX_RETRIES) -> \"Writer\":\n        if self.config.storage is None or self.config.storage.driver is None:\n            logger.debug(\"Writer.execute skipped - storage not configured\")\n            return self\n\n        logger.debug(\"Writing response to memori DB\")\n\n        for attempt in range(max_retries):\n            try:\n                self._execute_transaction(payload)\n                return self\n            except OperationalError as e:\n                if \"restart transaction\" in str(e) and attempt < max_retries - 1:\n                    logger.debug(\n                        \"Writer retry attempt %d due to OperationalError\", attempt + 1\n                    )\n                    if self.config.storage.adapter:\n                        self.config.storage.adapter.rollback()\n                    time.sleep(RETRY_BACKOFF_BASE * (2**attempt))\n                    continue\n                raise\n        return self\n\n    def _ensure_cached_id(self, cache_attr: str, create_func, *create_args) -> int:\n        \"\"\"Ensure an ID is cached, creating it if necessary.\"\"\"\n        cached_id = getattr(self.config.cache, cache_attr)\n        if cached_id is None:\n            cached_id = create_func(*create_args)\n            if cached_id is None:\n                raise RuntimeError(f\"{cache_attr} is unexpectedly None\")\n            setattr(self.config.cache, cache_attr, cached_id)\n        return cached_id\n\n    def _execute_transaction(self, payload: dict) -> None:\n        if self.config.entity_id is not None:\n            self._ensure_cached_id(\n                \"entity_id\",\n                self.config.storage.driver.entity.create,\n                self.config.entity_id,\n            )\n\n        if self.config.process_id is not None:\n            self._ensure_cached_id(\n                \"process_id\",\n                self.config.storage.driver.process.create,\n                self.config.process_id,\n            )\n\n        self._ensure_cached_id(\n            \"session_id\",\n            self.config.storage.driver.session.create,\n            self.config.session_id,\n            self.config.cache.entity_id,\n            self.config.cache.process_id,\n        )\n\n        # Ensure conversation_id exists - may have been set earlier in\n        # inject_conversation_messages. If not, create/get it now.\n        # conversation.create is idempotent and returns existing conversation\n        # if within timeout, so it's safe to call multiple times.\n        self._ensure_cached_id(\n            \"conversation_id\",\n            self.config.storage.driver.conversation.create,\n            self.config.cache.session_id,\n            self.config.session_timeout_minutes,\n        )\n\n        for message in payload.get(\"messages\", []):\n            self.config.storage.driver.conversation.message.create(\n                self.config.cache.conversation_id,\n                message[\"role\"],\n                message[\"type\"],\n                message[\"text\"],\n            )\n\n        if self.config.storage is not None and self.config.storage.adapter is not None:\n            self.config.storage.adapter.flush()\n            self.config.storage.adapter.commit()\n            logger.debug(\n                \"Transaction committed - conversation_id: %s\",\n                self.config.cache.conversation_id,\n            )\n"
  },
  {
    "path": "memori/memory/augmentation/__init__.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.memory.augmentation._manager import Manager\nfrom memori.memory.augmentation.augmentations import memori  # noqa: F401\n\n__all__ = [\"Manager\"]\n"
  },
  {
    "path": "memori/memory/augmentation/_base.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.memory.augmentation.input import AugmentationInput\n\n\nclass AugmentationContext:\n    def __init__(self, payload: AugmentationInput):\n        self.payload = payload\n        self.data = {}\n        self.writes = []\n\n    def add_write(self, method_path: str, *args, **kwargs):\n        self.writes.append({\"method_path\": method_path, \"args\": args, \"kwargs\": kwargs})\n        return self\n\n\nclass BaseAugmentation:\n    def __init__(self, config=None, enabled: bool = True):\n        self.config = config\n        self.enabled = enabled\n\n    async def process(self, ctx: AugmentationContext, driver) -> AugmentationContext:\n        raise NotImplementedError(\"Augmentation must implement process() method\")\n"
  },
  {
    "path": "memori/memory/augmentation/_db_writer.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport logging\nimport queue as queue_module\nimport threading\nimport time\nfrom collections.abc import Callable\n\nfrom memori.storage._connection import connection_context\n\nlogger = logging.getLogger(__name__)\n\n\nclass WriteTask:\n    def __init__(\n        self,\n        *,\n        conn_factory: Callable,\n        method_path: str,\n        args: tuple | None = None,\n        kwargs: dict | None = None,\n    ):\n        self.conn_factory = conn_factory\n        self.method_path = method_path\n        self.args = args or ()\n        self.kwargs = kwargs or {}\n\n    def execute(self, driver):\n        method = self._resolve_method(driver, self.method_path)\n        if method:\n            return method(*self.args, **self.kwargs)\n\n    def _resolve_method(self, driver, method_path: str):\n        parts = method_path.split(\".\")\n        obj = driver\n\n        for part in parts:\n            if not hasattr(obj, part):\n                return None\n            obj = getattr(obj, part)\n\n        return obj if callable(obj) else None\n\n\nclass DbWriterRuntime:\n    def __init__(self):\n        self.queue = None\n        self.batch_size = 100\n        self.batch_timeout = 0.1\n        self.thread = None\n        self.lock = threading.Lock()\n        self.started = False\n\n    def configure(self, config):\n        self.batch_size = config.db_writer_batch_size\n        self.batch_timeout = config.db_writer_batch_timeout\n\n        if self.queue is None:\n            self.queue = queue_module.Queue(maxsize=config.db_writer_queue_size)\n\n        return self\n\n    def ensure_started(self) -> None:\n        with self.lock:\n            if self.started:\n                return\n\n            self.thread = threading.Thread(\n                target=self._run_loop, daemon=True, name=\"memori-db-writer\"\n            )\n            self.thread.start()\n            self.started = True\n\n    def enqueue_write(self, task: WriteTask, timeout: float = 5.0) -> bool:\n        try:\n            if self.queue is None:\n                return False\n            self.queue.put(task, timeout=timeout)\n            return True\n        except queue_module.Full:\n            return False\n\n    def _run_loop(self) -> None:\n        logger.debug(\"AA DB writer thread started\")\n        while True:\n            try:\n                self._drain_batches()\n            except Exception:\n                import traceback\n\n                traceback.print_exc()\n                time.sleep(1)\n\n    def _drain_batches(self) -> None:\n        \"\"\"Drain queued writes, only holding a DB connection while busy.\n\n        This opens a DB connection when there is at least one pending task, processes\n        batches until the queue is idle, then closes the connection by exiting the\n        connection context.\n        \"\"\"\n        if self.queue is None:\n            return\n\n        batch = self._collect_batch()\n        if not batch:\n            return\n\n        while batch:\n            by_factory: dict[Callable, list[WriteTask]] = {}\n            for task in batch:\n                by_factory.setdefault(task.conn_factory, []).append(task)\n\n            for factory, tasks in by_factory.items():\n                with connection_context(factory) as (_conn, adapter, driver):\n                    logger.debug(\"AA DB writer batch started - %d writes\", len(tasks))\n                    try:\n                        for task in tasks:\n                            task.execute(driver)\n\n                        if adapter:\n                            adapter.flush()\n                            adapter.commit()\n                        logger.debug(\"AA DB writer completing - batch committed\")\n                    except Exception:\n                        import traceback\n\n                        logger.debug(\"AA DB writer batch failed - rolling back\")\n                        traceback.print_exc()\n                        if adapter:\n                            try:\n                                adapter.rollback()\n                            except Exception:  # nosec B110\n                                pass\n\n            batch = self._collect_batch()\n\n    def _collect_batch(self) -> list[WriteTask]:\n        batch = []\n        deadline = time.time() + self.batch_timeout\n\n        while len(batch) < self.batch_size and time.time() < deadline:\n            try:\n                timeout = max(0.01, deadline - time.time())\n                task = self.queue.get(timeout=timeout)\n                batch.append(task)\n            except queue_module.Empty:\n                break\n\n        return batch\n\n\n_db_writer = DbWriterRuntime()\n\n\ndef get_db_writer() -> DbWriterRuntime:\n    return _db_writer\n"
  },
  {
    "path": "memori/memory/augmentation/_handler.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport logging\nimport time\nfrom collections.abc import Callable\nfrom dataclasses import asdict\nfrom typing import Any\n\nfrom memori._exceptions import MemoriApiError\nfrom memori._network import Api, ApiSubdomain\nfrom memori.memory.augmentation.augmentations.memori.models import AugmentationInputData\nfrom memori.memory.augmentation.input import AugmentationInput\n\nlogger = logging.getLogger(__name__)\n\n\ndef _build_meta(config) -> dict[str, object]:\n    sdk = {\n        \"lang\": \"python\",\n        \"version\": getattr(config, \"version\", None),\n    }\n\n    framework = getattr(getattr(config, \"framework\", None), \"provider\", None)\n    framework = {\"provider\": framework} if framework else None\n\n    llm = {\n        \"model\": {\n            \"provider\": getattr(getattr(config, \"llm\", None), \"provider\", None),\n            \"sdk\": {\n                \"version\": getattr(\n                    getattr(config, \"llm\", None), \"provider_sdk_version\", None\n                ),\n            },\n            \"version\": getattr(getattr(config, \"llm\", None), \"version\", None),\n        },\n    }\n\n    provider = getattr(getattr(config, \"platform\", None), \"provider\", None)\n    platform = {\"provider\": provider} if provider else None\n\n    if config.cloud is True:\n        storage = None\n    else:\n        storage = {\n            \"cockroachdb\": getattr(\n                getattr(config, \"storage_config\", None), \"cockroachdb\", None\n            ),\n            \"dialect\": getattr(\n                getattr(config, \"storage_config\", None), \"dialect\", None\n            ),\n        }\n\n    return {\n        \"attribution\": {\n            \"entity\": {\"id\": config.entity_id},\n            \"process\": {\"id\": config.process_id},\n        },\n        \"framework\": framework,\n        \"llm\": llm,\n        \"platform\": platform,\n        \"sdk\": sdk,\n        \"storage\": storage,\n    }\n\n\ndef _post_cloud_augmentation(config, payload: dict) -> None:\n    api = Api(config, ApiSubdomain.COLLECTOR)\n    attempts = max(1, int(getattr(config, \"request_num_backoff\", 1) or 1))\n    backoff_factor = float(getattr(config, \"request_backoff_factor\", 1) or 1)\n\n    last_error: Exception | None = None\n    last_status: int | None = None\n\n    for attempt in range(attempts):\n        try:\n            last_status = api.post(\"cloud/augmentation\", payload, status_code=True)\n            if 200 <= last_status <= 299:\n                return\n            last_error = None\n        except Exception as e:  # noqa: BLE001\n            last_error = e\n\n        if attempt < attempts - 1:\n            time.sleep(backoff_factor * (2**attempt))\n\n    if last_error is not None:\n        raise last_error\n\n    raise MemoriApiError(\n        f\"cloud augmentation request failed (status={last_status}) after {attempts} attempts\"\n    )\n\n\ndef _send_cloud_augmentation_background(config, payload: dict) -> None:\n    try:\n        _post_cloud_augmentation(config, payload)\n    except Exception as e:  # noqa: BLE001\n        logger.error(\"cloud augmentation background task failed: %s\", e, exc_info=True)\n\n\ndef handle_augmentation(\n    *,\n    config,\n    payload: AugmentationInputData,\n    kwargs: dict,\n    augmentation_manager: Any,\n    log_content: Callable[[str], None] | None = None,\n) -> None:\n    if not config.entity_id and not config.process_id:\n        return\n\n    payload_dict = asdict(payload)\n    if config.cloud is True:\n        aug_payload = {\n            \"conversation\": {\n                \"messages\": payload_dict[\"messages\"],\n                \"summary\": None,\n            },\n            \"meta\": _build_meta(config),\n            \"session\": {\"id\": str(config.session_id)},\n        }\n\n        executor = getattr(config, \"thread_pool_executor\", None)\n        if executor is not None:\n            executor.submit(_send_cloud_augmentation_background, config, aug_payload)\n        else:\n            _send_cloud_augmentation_background(config, aug_payload)\n        return\n\n    augmentation_input = AugmentationInput(\n        conversation_id=config.cache.conversation_id,\n        entity_id=config.entity_id,\n        process_id=config.process_id,\n        conversation_messages=payload.messages,\n    )\n    augmentation_manager.enqueue(augmentation_input)\n"
  },
  {
    "path": "memori/memory/augmentation/_manager.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport asyncio\nimport logging\nfrom collections.abc import Callable\nfrom concurrent.futures import Future\nfrom typing import Any\n\nfrom memori._config import Config\nfrom memori.memory.augmentation._base import AugmentationContext\nfrom memori.memory.augmentation._db_writer import WriteTask, get_db_writer\nfrom memori.memory.augmentation._registry import Registry as AugmentationRegistry\nfrom memori.memory.augmentation._runtime import get_runtime\nfrom memori.memory.augmentation.input import AugmentationInput\nfrom memori.storage._connection import connection_context\n\nlogger = logging.getLogger(__name__)\n\nMAX_WORKERS = 50\nDB_WRITER_BATCH_SIZE = 100\nDB_WRITER_BATCH_TIMEOUT = 0.1\nDB_WRITER_QUEUE_SIZE = 1000\nRUNTIME_READY_TIMEOUT = 1.0\n\n\nclass Manager:\n    def __init__(self, config: Config) -> None:\n        self.config = config\n        self.augmentations = AugmentationRegistry().augmentations(config=config)\n        self.conn_factory: Callable | None = None\n        self._active = False\n        self.max_workers = MAX_WORKERS\n        self.db_writer_batch_size = DB_WRITER_BATCH_SIZE\n        self.db_writer_batch_timeout = DB_WRITER_BATCH_TIMEOUT\n        self.db_writer_queue_size = DB_WRITER_QUEUE_SIZE\n        self._quota_error: Exception | None = None\n        self._pending_futures: list[Future[Any]] = []\n\n    def start(self, conn: Callable | Any) -> \"Manager\":\n        \"\"\"Start the augmentation manager with a database connection.\n\n        Args:\n            conn: Either a callable that returns a connection (e.g. sessionmaker)\n                  or a connection instance (will be wrapped in a lambda).\n        \"\"\"\n        if conn is None:\n            return self\n\n        if callable(conn):\n            self.conn_factory = conn\n        else:\n            self.conn_factory = lambda: conn\n\n        self._active = True\n\n        runtime = get_runtime()\n        runtime.ensure_started(self.max_workers)\n\n        db_writer = get_db_writer()\n        db_writer.configure(self)\n        db_writer.ensure_started()\n\n        return self\n\n    def enqueue(self, input_data: AugmentationInput) -> \"Manager\":\n        if self._quota_error:\n            raise self._quota_error\n\n        if not self._active or not self.conn_factory:\n            logger.debug(\"Augmentation enqueue skipped - not active or no connection\")\n            return self\n\n        runtime = get_runtime()\n\n        if not runtime.ready.wait(timeout=RUNTIME_READY_TIMEOUT):\n            raise RuntimeError(\"Augmentation runtime is not available\")\n\n        if runtime.loop is None:\n            raise RuntimeError(\"Event loop is not available\")\n\n        logger.debug(\"AA enqueued - scheduling augmentation processing\")\n        future = asyncio.run_coroutine_threadsafe(\n            self._process_augmentations(input_data), runtime.loop\n        )\n        self._pending_futures.append(future)\n        future.add_done_callback(lambda f: self._handle_augmentation_result(f))\n        return self\n\n    def _handle_augmentation_result(self, future: Future[Any]) -> None:\n        from memori._exceptions import QuotaExceededError\n\n        try:\n            future.result()\n        except QuotaExceededError as e:\n            self._quota_error = e\n            self._active = False\n            logger.error(f\"Quota exceeded, disabling augmentation: {e}\")\n        except Exception as e:\n            logger.error(f\"Augmentation task failed: {e}\", exc_info=True)\n        finally:\n            if future in self._pending_futures:\n                self._pending_futures.remove(future)\n\n    async def _process_augmentations(self, input_data: AugmentationInput) -> None:\n        if not self.augmentations:\n            logger.debug(\"No augmentations configured\")\n            return\n\n        runtime = get_runtime()\n        if runtime.semaphore is None:\n            return\n\n        logger.debug(\"AA processing started\")\n        async with runtime.semaphore:\n            ctx = AugmentationContext(payload=input_data)\n\n            try:\n                with connection_context(self.conn_factory) as (conn, adapter, driver):\n                    for aug in self.augmentations:\n                        if aug.enabled:\n                            try:\n                                logger.debug(\n                                    \"Running augmentation: %s\", aug.__class__.__name__\n                                )\n                                ctx = await aug.process(ctx, driver)\n                            except Exception as e:\n                                from memori._exceptions import QuotaExceededError\n\n                                if isinstance(e, QuotaExceededError):\n                                    raise\n                                logger.error(\n                                    f\"Error in augmentation {aug.__class__.__name__}: {e}\",\n                                    exc_info=True,\n                                )\n\n                    if ctx.writes:\n                        logger.debug(\"AA scheduling %d DB writes\", len(ctx.writes))\n                        self._enqueue_writes(ctx.writes)\n            except Exception as e:\n                from memori._exceptions import QuotaExceededError\n\n                if isinstance(e, QuotaExceededError):\n                    raise\n                logger.error(f\"Error processing augmentations: {e}\", exc_info=True)\n\n    def _enqueue_writes(self, writes: list[dict[str, Any]]) -> None:\n        db_writer = get_db_writer()\n        if self.conn_factory is None:\n            return\n\n        for write_op in writes:\n            task = WriteTask(\n                conn_factory=self.conn_factory,\n                method_path=write_op[\"method_path\"],\n                args=write_op[\"args\"],\n                kwargs=write_op[\"kwargs\"],\n            )\n            db_writer.enqueue_write(task)\n\n    def wait(self, timeout: float | None = None) -> bool:\n        import concurrent.futures\n        import time\n\n        start_time = time.time()\n\n        # Wait for pending futures to complete\n        if self._pending_futures:\n            try:\n                concurrent.futures.wait(\n                    self._pending_futures,\n                    timeout=timeout,\n                    return_when=concurrent.futures.ALL_COMPLETED,\n                )\n            except Exception:\n                return False\n\n            if self._pending_futures:\n                return False\n\n        # Wait for db_writer queue to drain and batch to process\n        db_writer = get_db_writer()\n        if db_writer.queue is None:\n            return True\n\n        deadline = None if timeout is None else start_time + timeout\n        poll_interval = 0.01\n\n        # Wait for queue to be empty\n        while not db_writer.queue.empty():\n            if deadline and time.time() >= deadline:\n                return False\n            time.sleep(poll_interval)\n\n        # Wait for final batch processing (2x batch_timeout)\n        extra_wait = db_writer.batch_timeout * 2\n        if deadline:\n            extra_wait = min(extra_wait, deadline - time.time())\n\n        if extra_wait > 0:\n            time.sleep(extra_wait)\n\n        return True\n"
  },
  {
    "path": "memori/memory/augmentation/_message.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass ConversationMessage:\n    role: str\n    content: str\n\n    def to_dict(self) -> dict[str, str]:\n        return {\"role\": self.role, \"content\": self.content}\n"
  },
  {
    "path": "memori/memory/augmentation/_models.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport hashlib\nfrom dataclasses import dataclass, field\n\n\ndef hash_id(value: str | None) -> str | None:\n    if not value:\n        return None\n    return hashlib.sha256(value.encode()).hexdigest()\n\n\n@dataclass\nclass ConversationData:\n    \"\"\"Conversation data structure for augmentation payload.\"\"\"\n\n    messages: list\n    summary: str | None = None\n\n\n@dataclass\nclass SdkVersionData:\n    \"\"\"SDK version data structure.\"\"\"\n\n    version: str | None = None\n\n\n@dataclass\nclass ModelData:\n    \"\"\"Model metadata structure.\"\"\"\n\n    provider: str | None = None\n    sdk: SdkVersionData = field(default_factory=SdkVersionData)\n    version: str | None = None\n\n\n@dataclass\nclass FrameworkData:\n    \"\"\"Framework metadata structure.\"\"\"\n\n    provider: str | None = None\n\n\n@dataclass\nclass LlmData:\n    \"\"\"LLM metadata structure.\"\"\"\n\n    model: ModelData = field(default_factory=ModelData)\n\n\n@dataclass\nclass PlatformData:\n    \"\"\"Platform metadata structure.\"\"\"\n\n    provider: str | None = None\n\n\n@dataclass\nclass SdkData:\n    \"\"\"SDK metadata structure.\"\"\"\n\n    lang: str = \"python\"\n    version: str | None = None\n\n\n@dataclass\nclass StorageData:\n    \"\"\"Storage metadata structure.\"\"\"\n\n    cockroachdb: bool = False\n    dialect: str | None = None\n\n\n@dataclass\nclass EntityData:\n    \"\"\"Entity metadata structure.\"\"\"\n\n    id: str | None = None\n\n\n@dataclass\nclass ProcessData:\n    \"\"\"Process metadata structure.\"\"\"\n\n    id: str | None = None\n\n\n@dataclass\nclass AttributionData:\n    \"\"\"Attribution metadata structure.\"\"\"\n\n    entity: EntityData = field(default_factory=EntityData)\n    process: ProcessData = field(default_factory=ProcessData)\n\n\n@dataclass\nclass MetaData:\n    \"\"\"Meta information structure for augmentation payload.\"\"\"\n\n    framework: FrameworkData = field(default_factory=FrameworkData)\n    llm: LlmData = field(default_factory=LlmData)\n    platform: PlatformData = field(default_factory=PlatformData)\n    sdk: SdkData = field(default_factory=SdkData)\n    storage: StorageData = field(default_factory=StorageData)\n    attribution: AttributionData = field(default_factory=AttributionData)\n\n\n@dataclass\nclass AugmentationPayload:\n    \"\"\"Complete augmentation API payload structure.\"\"\"\n\n    conversation: ConversationData\n    meta: MetaData\n\n    def to_dict(self) -> dict:\n        \"\"\"Convert the dataclass to a dictionary for API submission.\"\"\"\n        return {\n            \"conversation\": {\n                \"messages\": self.conversation.messages,\n                \"summary\": self.conversation.summary,\n            },\n            \"meta\": {\n                \"attribution\": {\n                    \"entity\": {\n                        \"id\": self.meta.attribution.entity.id,\n                    },\n                    \"process\": {\n                        \"id\": self.meta.attribution.process.id,\n                    },\n                },\n                \"framework\": {\n                    \"provider\": self.meta.framework.provider,\n                },\n                \"llm\": {\n                    \"model\": {\n                        \"provider\": self.meta.llm.model.provider,\n                        \"sdk\": {\n                            \"version\": self.meta.llm.model.sdk.version,\n                        },\n                        \"version\": self.meta.llm.model.version,\n                    }\n                },\n                \"platform\": {\n                    \"provider\": self.meta.platform.provider,\n                },\n                \"sdk\": {\n                    \"lang\": self.meta.sdk.lang,\n                    \"version\": self.meta.sdk.version,\n                },\n                \"storage\": {\n                    \"cockroachdb\": self.meta.storage.cockroachdb,\n                    \"dialect\": self.meta.storage.dialect,\n                },\n            },\n        }\n"
  },
  {
    "path": "memori/memory/augmentation/_registry.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\n\nclass Registry:\n    _augmentations: dict[str, type] = {}\n\n    @classmethod\n    def register(cls, name: str):\n        def decorator(augmentation_class: type):\n            cls._augmentations[name] = augmentation_class\n            return augmentation_class\n\n        return decorator\n\n    def augmentations(self, config=None):\n        return [aug_class(config=config) for aug_class in self._augmentations.values()]\n"
  },
  {
    "path": "memori/memory/augmentation/_runtime.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport asyncio\nimport threading\n\n\nclass AugmentationRuntime:\n    def __init__(self):\n        self.loop = None\n        self.ready = threading.Event()\n        self.semaphore = None\n        self.max_workers = 50\n        self.thread = None\n        self.lock = threading.Lock()\n        self.started = False\n\n    def ensure_started(self, max_workers: int):\n        with self.lock:\n            self.max_workers = max_workers\n            if self.started:\n                loop = self.loop\n                if loop is not None and self.semaphore is not None:\n\n                    def _set_semaphore() -> None:\n                        self.semaphore = asyncio.Semaphore(self.max_workers)\n\n                    loop.call_soon_threadsafe(_set_semaphore)\n                return\n\n            self.thread = threading.Thread(\n                target=self._run_loop, daemon=True, name=\"memori-augmentation\"\n            )\n            self.thread.start()\n            self.started = True\n\n    def _run_loop(self) -> None:\n        self.loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(self.loop)\n        self.semaphore = asyncio.Semaphore(self.max_workers)\n        self.ready.set()\n        self.loop.run_forever()\n\n\n_runtime = AugmentationRuntime()\n\n\ndef get_runtime() -> AugmentationRuntime:\n    return _runtime\n"
  },
  {
    "path": "memori/memory/augmentation/augmentations/memori/__init__.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.memory.augmentation.augmentations.memori._augmentation import (\n    AdvancedAugmentation,\n)\n\n__all__ = [\n    \"AdvancedAugmentation\",\n    \"AugmentationInputData\",\n    \"AttributionData\",\n    \"EntityData\",\n    \"ProcessData\",\n    \"SessionData\",\n]\nfrom memori.memory.augmentation.augmentations.memori.models import (\n    AttributionData,\n    AugmentationInputData,\n    EntityData,\n    ProcessData,\n    SessionData,\n)\n\n__all__ = [\n    \"AdvancedAugmentation\",\n    \"AugmentationInputData\",\n    \"AttributionData\",\n    \"EntityData\",\n    \"ProcessData\",\n    \"SessionData\",\n]\n"
  },
  {
    "path": "memori/memory/augmentation/augmentations/memori/_augmentation.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport logging\nfrom dataclasses import asdict, is_dataclass\n\nfrom memori._network import Api\nfrom memori.embeddings import embed_texts\nfrom memori.memory._struct import Memories, build_fact_text_from_triple_entry\nfrom memori.memory.augmentation._base import AugmentationContext, BaseAugmentation\nfrom memori.memory.augmentation._models import (\n    AttributionData,\n    AugmentationPayload,\n    ConversationData,\n    EntityData,\n    FrameworkData,\n    LlmData,\n    MetaData,\n    ModelData,\n    PlatformData,\n    ProcessData,\n    SdkData,\n    SdkVersionData,\n    StorageData,\n    hash_id,\n)\nfrom memori.memory.augmentation._registry import Registry\n\nlogger = logging.getLogger(__name__)\n\n\n@Registry.register(\"advanced_augmentation\")\nclass AdvancedAugmentation(BaseAugmentation):\n    def __init__(self, config=None, enabled: bool = True):\n        super().__init__(config=config, enabled=enabled)\n\n    def _get_conversation_summary(self, driver, conversation_id: str) -> str:\n        try:\n            conversation = driver.conversation.read(conversation_id)\n            if conversation and conversation.get(\"summary\"):\n                return conversation[\"summary\"]\n        except Exception:\n            pass\n        return \"\"\n\n    def _select_messages_for_summary(self, messages: list, summary: str) -> list:\n        if not summary:\n            return messages\n        if not messages or not isinstance(messages, list):\n            return []\n\n        def _role(msg) -> str | None:\n            if isinstance(msg, dict):\n                return msg.get(\"role\")\n            return getattr(msg, \"role\", None)\n\n        assistant_idx = None\n        for i in range(len(messages) - 1, -1, -1):\n            msg = messages[i]\n            if _role(msg) == \"assistant\":\n                assistant_idx = i\n                break\n\n        if assistant_idx is None:\n            return messages\n\n        user_idx = None\n        for i in range(assistant_idx - 1, -1, -1):\n            msg = messages[i]\n            if _role(msg) == \"user\":\n                user_idx = i\n                break\n\n        if user_idx is None:\n            return [messages[assistant_idx]]\n\n        return [messages[user_idx], messages[assistant_idx]]\n\n    def _build_api_payload(\n        self,\n        messages: list,\n        summary: str,\n        system_prompt: str | None,\n        dialect: str,\n        entity_id: str | None,\n        process_id: str | None,\n    ) -> dict:\n        \"\"\"Build API payload using structured dataclasses.\"\"\"\n        conversation = ConversationData(\n            messages=messages,\n            summary=summary if summary else None,\n        )\n\n        meta = MetaData(\n            attribution=AttributionData(\n                entity=EntityData(id=hash_id(entity_id)),\n                process=ProcessData(id=hash_id(process_id)),\n            ),\n            framework=FrameworkData(provider=self.config.framework.provider),\n            llm=LlmData(\n                model=ModelData(\n                    provider=self.config.llm.provider,\n                    sdk=SdkVersionData(version=self.config.llm.provider_sdk_version),\n                    version=self.config.llm.version,\n                )\n            ),\n            platform=PlatformData(provider=self.config.platform.provider),\n            sdk=SdkData(lang=\"python\", version=self.config.version),\n            storage=StorageData(\n                cockroachdb=self.config.storage_config.cockroachdb,\n                dialect=dialect,\n            ),\n        )\n\n        payload = AugmentationPayload(conversation=conversation, meta=meta)\n        return payload.to_dict()\n\n    async def process(self, ctx: AugmentationContext, driver) -> AugmentationContext:\n        if not ctx.payload.entity_id:\n            return ctx\n        if not self.config:\n            return ctx\n        if not ctx.payload.conversation_id:\n            return ctx\n\n        api = Api(self.config)\n        dialect = driver.conversation.conn.get_dialect()\n        summary = self._get_conversation_summary(driver, ctx.payload.conversation_id)\n\n        messages = self._select_messages_for_summary(\n            ctx.payload.conversation_messages, summary\n        )\n\n        normalized_messages: list[dict] = []\n        for m in messages:\n            if is_dataclass(m):\n                normalized_messages.append(asdict(m))\n            elif isinstance(m, dict):\n                normalized_messages.append(m)\n\n        payload = self._build_api_payload(\n            normalized_messages,\n            summary,\n            ctx.payload.system_prompt,\n            dialect,\n            ctx.payload.entity_id,\n            ctx.payload.process_id,\n        )\n\n        logger.debug(\"AA submitting payload to API\")\n        try:\n            api_response = await api.augmentation_async(payload)\n        except Exception as e:\n            from memori._exceptions import QuotaExceededError\n\n            if isinstance(e, QuotaExceededError):\n                raise\n            logger.debug(\"AA API call failed: %s\", type(e).__name__)\n            return ctx\n\n        if not api_response:\n            logger.debug(\"AA API returned empty response\")\n            return ctx\n\n        memories = await self._process_api_response(api_response)\n\n        ctx.data[\"memories\"] = memories\n\n        await self._schedule_entity_writes(ctx, driver, memories)\n        self._schedule_process_writes(ctx, driver, memories)\n        self._schedule_conversation_writes(ctx, memories)\n\n        return ctx\n\n    async def _process_api_response(self, api_response: dict) -> Memories:\n        entity_data = api_response.get(\"entity\", {})\n        facts = entity_data.get(\"facts\", [])\n        triples = entity_data.get(\"triples\", [])\n\n        if not facts and triples:\n            facts = [\n                fact_text\n                for triple in triples\n                if (fact_text := build_fact_text_from_triple_entry(triple)) is not None\n            ]\n\n        if facts:\n            embeddings_config = self.config.embeddings\n            fact_embeddings = await embed_texts(\n                facts,\n                model=embeddings_config.model,\n                async_=True,\n            )\n            api_response[\"entity\"][\"fact_embeddings\"] = fact_embeddings\n\n        return Memories().configure_from_advanced_augmentation(api_response)\n\n    async def _schedule_entity_writes(\n        self, ctx: AugmentationContext, driver, memories: Memories\n    ):\n        if not ctx.payload.entity_id:\n            return\n\n        entity_id = driver.entity.create(ctx.payload.entity_id)\n        if not entity_id:\n            return\n\n        facts_to_write = memories.entity.facts\n        embeddings_to_write = memories.entity.fact_embeddings\n\n        if memories.entity.semantic_triples and (\n            not facts_to_write or not embeddings_to_write\n        ):\n            facts_from_triples = [\n                f\"{triple.subject_name} {triple.predicate} {triple.object_name}\"\n                for triple in memories.entity.semantic_triples\n            ]\n\n            if facts_from_triples:\n                embeddings_config = self.config.embeddings\n                embeddings_from_triples = await embed_texts(\n                    facts_from_triples,\n                    model=embeddings_config.model,\n                    async_=True,\n                )\n                facts_to_write = (facts_to_write or []) + facts_from_triples\n                embeddings_to_write = (\n                    embeddings_to_write or []\n                ) + embeddings_from_triples\n\n        if facts_to_write and embeddings_to_write:\n            ctx.add_write(\n                \"entity_fact.create\",\n                entity_id,\n                facts_to_write,\n                embeddings_to_write,\n            )\n\n        if memories.entity.semantic_triples:\n            ctx.add_write(\n                \"knowledge_graph.create\",\n                entity_id,\n                memories.entity.semantic_triples,\n            )\n\n    def _schedule_process_writes(\n        self, ctx: AugmentationContext, driver, memories: Memories\n    ):\n        if not ctx.payload.process_id:\n            return\n\n        process_id = driver.process.create(ctx.payload.process_id)\n        if process_id and memories.process.attributes:\n            ctx.add_write(\n                \"process_attribute.create\", process_id, memories.process.attributes\n            )\n\n    def _schedule_conversation_writes(\n        self, ctx: AugmentationContext, memories: Memories\n    ):\n        if not ctx.payload.conversation_id:\n            return\n\n        if memories.conversation.summary:\n            ctx.add_write(\n                \"conversation.update\",\n                ctx.payload.conversation_id,\n                memories.conversation.summary,\n            )\n"
  },
  {
    "path": "memori/memory/augmentation/augmentations/memori/models.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom dataclasses import dataclass, field\n\nfrom memori.memory.augmentation._message import ConversationMessage\n\n\n@dataclass\nclass EntityData:\n    \"\"\"Entity metadata structure.\"\"\"\n\n    id: str | None = None\n\n\n@dataclass\nclass ProcessData:\n    \"\"\"Process metadata structure.\"\"\"\n\n    id: str | None = None\n\n\n@dataclass\nclass AttributionData:\n    \"\"\"Attribution metadata structure.\"\"\"\n\n    entity: EntityData = field(default_factory=EntityData)\n    process: ProcessData = field(default_factory=ProcessData)\n\n    def to_dict(self) -> dict[str, object]:\n        return {\n            \"entity\": {\"id\": self.entity.id},\n            \"process\": {\"id\": self.process.id},\n        }\n\n\n@dataclass\nclass SessionData:\n    \"\"\"Session metadata structure.\"\"\"\n\n    id: str | None = None\n\n    def to_dict(self) -> dict[str, object]:\n        return {\"id\": self.id}\n\n\n@dataclass\nclass AugmentationInputData:\n    attribution: AttributionData\n    messages: list[ConversationMessage]\n    session: SessionData\n\n    def messages_as_dicts(self) -> list[dict[str, str]]:\n        return [{\"role\": m.role, \"content\": m.content} for m in self.messages]\n\n    def to_dict(self) -> dict[str, object]:\n        return {\n            \"attribution\": self.attribution.to_dict(),\n            \"messages\": [m.to_dict() for m in self.messages],\n            \"session\": self.session.to_dict(),\n        }\n"
  },
  {
    "path": "memori/memory/augmentation/input.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom dataclasses import dataclass\n\nfrom memori.memory.augmentation._message import ConversationMessage\n\n\n@dataclass\nclass AugmentationInput:\n    \"\"\"Data class for augmentation input.\"\"\"\n\n    conversation_id: str | None\n    entity_id: str | None\n    process_id: str | None\n    conversation_messages: list[ConversationMessage]\n    system_prompt: str | None = None\n"
  },
  {
    "path": "memori/memory/augmentation/memories/_conversation.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\n\nclass Conversation:\n    def __init__(self):\n        self.entities = []\n        self.summary = None\n"
  },
  {
    "path": "memori/memory/augmentation/memories/_entity.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\n\nclass Entity:\n    def __init__(self):\n        self.facts = []\n        self.knowledge_graph = []\n"
  },
  {
    "path": "memori/memory/augmentation/memories/_process.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\n\nclass Process:\n    def __init__(self):\n        self.attributes = []\n"
  },
  {
    "path": "memori/memory/recall.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport logging\nimport time\nfrom collections.abc import Mapping\nfrom typing import TypeGuard, cast\n\nfrom sqlalchemy.exc import OperationalError\n\nfrom memori._config import Config\nfrom memori._logging import truncate\nfrom memori._network import Api\nfrom memori.embeddings import embed_texts\nfrom memori.search import search_facts as search_facts_api\nfrom memori.search._types import FactSearchResult\n\nlogger = logging.getLogger(__name__)\n\nMAX_RETRIES = 3\nRETRY_BACKOFF_BASE = 0.05\n\n\ndef _is_str_object_mapping(value: object) -> TypeGuard[Mapping[str, object]]:\n    if not isinstance(value, Mapping):\n        return False\n    return all(isinstance(k, str) for k in value.keys())\n\n\nclass Recall:\n    def __init__(self, config: Config) -> None:\n        self.config = config\n\n    def _resolve_entity_id(self, entity_id: int | None) -> int | None:\n        if entity_id is not None:\n            return entity_id\n\n        if self.config.entity_id is None:\n            logger.debug(\"Recall aborted - no entity_id configured\")\n            return None\n\n        entity_id = self.config.storage.driver.entity.create(self.config.entity_id)\n        logger.debug(\"Entity ID resolved: %s\", entity_id)\n        if entity_id is None:\n            logger.debug(\"Recall aborted - entity_id is None after resolution\")\n        return entity_id\n\n    def _resolve_limit(self, limit: int | None) -> int:\n        return self.config.recall_facts_limit if limit is None else limit\n\n    def _embed_query(self, query: str) -> list[float]:\n        logger.debug(\"Generating query embedding\")\n        embeddings_config = self.config.embeddings\n        return embed_texts(\n            query,\n            model=embeddings_config.model,\n        )[0]\n\n    def _search_with_retries(\n        self, *, entity_id: int, query: str, query_embedding: list[float], limit: int\n    ) -> list[FactSearchResult]:\n        facts: list[FactSearchResult] = []\n        for attempt in range(MAX_RETRIES):\n            try:\n                logger.debug(\n                    f\"Executing search_facts - entity_id: {entity_id}, limit: {limit}, embeddings_limit: {self.config.recall_embeddings_limit}\"\n                )\n                facts = search_facts_api(\n                    self.config.storage.driver.entity_fact,\n                    entity_id,\n                    query_embedding,\n                    limit,\n                    self.config.recall_embeddings_limit,\n                    query_text=query,\n                )\n                logger.debug(\"Recall complete - found %d facts\", len(facts))\n                break\n            except OperationalError as e:\n                if \"restart transaction\" in str(e) and attempt < MAX_RETRIES - 1:\n                    logger.debug(\n                        \"Retry attempt %d due to OperationalError\", attempt + 1\n                    )\n                    time.sleep(RETRY_BACKOFF_BASE * (2**attempt))\n                    continue\n                raise\n\n        return facts\n\n    def _search_with_retries_cloud(\n        self, *, query: str, limit: int\n    ) -> list[FactSearchResult | Mapping[str, object] | str]:\n        data = self._cloud_recall(query, limit=limit)\n        facts, _messages = self._parse_cloud_recall_response(data)\n        return facts\n\n    def _cloud_recall(self, query: str, *, limit: int | None = None) -> object:\n        if self.config.entity_id is None:\n            logger.debug(\"Cloud recall aborted - no entity_id configured\")\n            return []\n\n        api = Api(self.config)\n        resolved_limit = self._resolve_limit(limit)\n        process = None\n        if self.config.process_id is not None:\n            process = {\"id\": self.config.process_id}\n        payload = {\n            \"attribution\": {\n                \"entity\": {\"id\": str(self.config.entity_id)},\n                \"process\": process,\n            },\n            \"query\": query,\n            \"session\": {\"id\": str(self.config.session_id)},\n            \"limit\": resolved_limit,\n        }\n        return api.post(\"cloud/recall\", payload)\n\n    @staticmethod\n    def _parse_cloud_recall_response(\n        data: object,\n    ) -> tuple[\n        list[FactSearchResult | Mapping[str, object] | str], list[dict[str, str]]\n    ]:\n        if isinstance(data, list):\n            facts_from_list: list[FactSearchResult | Mapping[str, object] | str] = []\n            for item in data:\n                if isinstance(item, str):\n                    facts_from_list.append(item)\n                elif _is_str_object_mapping(item):\n                    facts_from_list.append(item)\n            return facts_from_list, []\n\n        if not isinstance(data, dict):\n            return [], []\n\n        data_map = cast(Mapping[str, object], data)\n\n        def _extract_list(*keys: str) -> list[object] | None:\n            for k in keys:\n                v = data_map.get(k)\n                if isinstance(v, list):\n                    return cast(list[object], v)\n            return None\n\n        facts_raw = _extract_list(\"facts\", \"results\", \"memories\", \"data\") or []\n        facts: list[FactSearchResult | Mapping[str, object] | str] = []\n        for item in facts_raw:\n            if isinstance(item, str):\n                facts.append(item)\n            elif _is_str_object_mapping(item):\n                facts.append(item)\n\n        messages_raw: list[object] = (\n            _extract_list(\"messages\", \"conversation_messages\", \"history\") or []\n        )\n        if not messages_raw:\n            convo = data_map.get(\"conversation\")\n            if _is_str_object_mapping(convo):\n                nested = convo.get(\"messages\")\n                if isinstance(nested, list):\n                    messages_raw = cast(list[object], nested)\n\n        messages: list[dict[str, str]] = []\n        for msg in messages_raw:\n            if not _is_str_object_mapping(msg):\n                continue\n            role = msg.get(\"role\")\n            content = msg.get(\"content\")\n            if content is None:\n                content = msg.get(\"text\")\n            if not isinstance(role, str) or not isinstance(content, str):\n                continue\n            messages.append({\"role\": role, \"content\": content})\n\n        return facts, messages\n\n    def search_facts(\n        self,\n        query: str,\n        limit: int | None = None,\n        entity_id: int | None = None,\n        cloud: bool = False,\n    ) -> list[FactSearchResult | Mapping[str, object] | str]:\n        logger.debug(\n            \"Recall started - query: %s (%d chars), limit: %s\",\n            truncate(query, 50),\n            len(query),\n            limit,\n        )\n\n        if self.config.cloud:\n            if self.config.entity_id is None:\n                logger.debug(\"Recall aborted - no entity_id configured\")\n                return []\n\n            logger.debug(\n                \"Recall started - query: %s (%d chars), limit: %s, cloud: true\",\n                truncate(query, 50),\n                len(query),\n                limit,\n            )\n            resolved_limit = self._resolve_limit(limit)\n            return self._search_with_retries_cloud(query=query, limit=resolved_limit)\n\n        if self.config.storage is None or self.config.storage.driver is None:\n            logger.debug(\"Recall aborted - storage not configured\")\n            return []\n\n        entity_id = self._resolve_entity_id(entity_id)\n        if entity_id is None:\n            return []\n\n        limit = self._resolve_limit(limit)\n        query_embedding = self._embed_query(query)\n        return cast(\n            list[FactSearchResult | Mapping[str, object] | str],\n            self._search_with_retries(\n                entity_id=entity_id,\n                query=query,\n                query_embedding=query_embedding,\n                limit=limit,\n            ),\n        )\n"
  },
  {
    "path": "memori/py.typed",
    "content": ""
  },
  {
    "path": "memori/search/__init__.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\nSearch utilities for Memori.\n\nPublic entrypoints:\n- parse_embedding\n- find_similar_embeddings\n- search_facts\n- FactCandidate\n- FactSearchResult\n\"\"\"\n\nfrom memori.search._api import search_facts\nfrom memori.search._faiss import find_similar_embeddings\nfrom memori.search._parsing import parse_embedding\nfrom memori.search._types import FactCandidate, FactSearchResult\n\n__all__ = [\n    \"find_similar_embeddings\",\n    \"parse_embedding\",\n    \"search_facts\",\n    \"FactCandidate\",\n    \"FactSearchResult\",\n]\n"
  },
  {
    "path": "memori/search/_api.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom memori.search._core import (\n    search_entity_facts_core,\n)\nfrom memori.search._faiss import find_similar_embeddings\nfrom memori.search._lexical import dense_lexical_weights, lexical_scores_for_ids\nfrom memori.search._types import FactCandidate, FactSearchResult\n\n\ndef search_facts(\n    entity_fact_driver: Any | None = None,\n    entity_id: int | None = None,\n    query_embedding: list[float] | None = None,\n    limit: int = 5,\n    embeddings_limit: int = 1000,\n    *,\n    query_text: str | None = None,\n    candidates: list[FactCandidate] | None = None,\n) -> list[FactSearchResult]:\n    \"\"\"\n    Unified search entrypoint.\n\n    - DB-backed mode: provide entity_fact_driver, entity_id, query_embedding, embeddings_limit\n    - Pre-scored mode: provide candidates (list[FactCandidate])\n    \"\"\"\n    if candidates is not None:\n        return search_entity_facts_core(\n            entity_fact_driver=None,\n            entity_id=0,\n            query_embedding=[],\n            limit=limit,\n            embeddings_limit=0,\n            query_text=query_text,\n            fact_candidates=candidates,\n            find_similar_embeddings=find_similar_embeddings,\n            lexical_scores_for_ids=lexical_scores_for_ids,\n            dense_lexical_weights=dense_lexical_weights,\n        )\n\n    if entity_fact_driver is None:\n        raise ValueError(\"entity_fact_driver is required when candidates is not set\")\n    if entity_id is None:\n        raise ValueError(\"entity_id is required when candidates is not set\")\n    if query_embedding is None:\n        raise ValueError(\"query_embedding is required when candidates is not set\")\n\n    return search_entity_facts_core(\n        entity_fact_driver,\n        entity_id,\n        query_embedding,\n        limit,\n        embeddings_limit,\n        query_text=query_text,\n        find_similar_embeddings=find_similar_embeddings,\n        lexical_scores_for_ids=lexical_scores_for_ids,\n        dense_lexical_weights=dense_lexical_weights,\n    )\n"
  },
  {
    "path": "memori/search/_core.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import Callable, Mapping\nfrom typing import Any, cast\n\nfrom memori.search._types import FactCandidate, FactId, FactSearchResult\n\nlogger = logging.getLogger(__name__)\n\n\ndef _candidate_pool_from_candidates(\n    candidates: list[FactCandidate], *, limit: int, query_text: str | None\n) -> tuple[\n    list[int], dict[int, float], dict[int, str], dict[int, FactId], dict[int, dict]\n]:\n    if not candidates:\n        return [], {}, {}, {}, {}\n\n    idx_to_original_id = {i: r.id for i, r in enumerate(candidates)}\n    content_map = {i: r.content for i, r in enumerate(candidates)}\n    similarities_map = {i: float(r.score) for i, r in enumerate(candidates)}\n    date_created_map = {i: r.date_created for i, r in enumerate(candidates)}\n\n    cand_limit = _candidate_limit(\n        limit=limit, total_embeddings=len(candidates), query_text=query_text\n    )\n    candidate_ids = sorted(\n        similarities_map,\n        key=lambda i: float(similarities_map.get(i, 0.0)),\n        reverse=True,\n    )[:cand_limit]\n\n    # Mimic DB shape just enough for _build_fact_rows.\n    fact_rows: dict[int, dict] = {\n        i: {\n            \"id\": idx_to_original_id.get(i),\n            \"date_created\": date_created_map.get(i, \"\"),\n        }\n        for i in candidate_ids\n    }\n\n    return candidate_ids, similarities_map, content_map, idx_to_original_id, fact_rows\n\n\ndef _get_embeddings_rows(\n    entity_fact_driver: Any, *, entity_id: int, embeddings_limit: int\n) -> list[dict]:\n    logger.debug(\n        \"Executing memori_entity_fact query - entity_id: %s, embeddings_limit: %s\",\n        entity_id,\n        embeddings_limit,\n    )\n    results = entity_fact_driver.get_embeddings(entity_id, embeddings_limit)\n    if not results:\n        logger.debug(\"No embeddings found in database for entity_id: %s\", entity_id)\n        return []\n    logger.debug(\"Retrieved %d embeddings from database\", len(results))\n    return results\n\n\ndef _candidate_limit(\n    *, limit: int, total_embeddings: int, query_text: str | None\n) -> int:\n    if query_text:\n        return max(limit, min(total_embeddings, max(limit * 10, 50)))\n    return int(limit)\n\n\ndef _fetch_content_maps(\n    entity_fact_driver: Any, *, candidate_ids: list[FactId]\n) -> tuple[dict[FactId, dict], dict[FactId, str]]:\n    logger.debug(\"Fetching content for %d fact IDs\", len(candidate_ids))\n    content_results = entity_fact_driver.get_facts_by_ids(candidate_ids)\n\n    fact_rows: dict[FactId, dict] = {}\n    for row in content_results or []:\n        if not isinstance(row, Mapping):\n            continue\n        rid: FactId = row.get(\"id\")\n        if rid is None:\n            continue\n        fact_rows[rid] = dict(row)\n\n    content_map: dict[FactId, str] = {}\n    for fid, row in fact_rows.items():\n        content = row.get(\"content\")\n        if isinstance(content, str):\n            content_map[fid] = content\n    return fact_rows, content_map\n\n\ndef _rank_candidates(\n    *,\n    candidate_ids: list[FactId],\n    similarities_map: dict[FactId, float],\n    query_text: str | None,\n    content_map: dict[FactId, str],\n    lexical_scores_for_ids: Callable[..., dict[FactId, float]],\n    dense_lexical_weights: Callable[..., tuple[float, float]],\n) -> tuple[list[FactId], dict[FactId, float], dict[FactId, float]]:\n    lex_scores: dict[FactId, float] = {}\n\n    if query_text:\n        lex_scores = lexical_scores_for_ids(\n            query_text=query_text, ids=candidate_ids, content_map=content_map\n        )\n        w_cos, w_lex = dense_lexical_weights(query_text=query_text)\n        rank_score_map = {\n            fid: (w_cos * float(similarities_map.get(fid, 0.0)))\n            + (w_lex * float(lex_scores.get(fid, 0.0)))\n            for fid in candidate_ids\n        }\n\n        def key(fid: FactId) -> tuple[float, float]:\n            return (\n                float(rank_score_map.get(fid, 0.0)),\n                float(similarities_map.get(fid, 0.0)),\n            )\n\n        base_order = sorted(candidate_ids, key=key, reverse=True)\n        return base_order, rank_score_map, lex_scores\n\n    rank_score_map = {\n        fid: float(similarities_map.get(fid, 0.0)) for fid in candidate_ids\n    }\n    return list(candidate_ids), rank_score_map, lex_scores\n\n\ndef _build_fact_rows(\n    *,\n    ordered_ids: list[FactId],\n    fact_rows: dict[FactId, dict],\n    content_map: dict[FactId, str],\n    similarities_map: dict[FactId, float],\n    rank_score_map: dict[FactId, float],\n) -> list[FactSearchResult]:\n    facts_with_similarity: list[FactSearchResult] = []\n    for fact_id in ordered_ids:\n        fact_row = fact_rows.get(fact_id, {})\n        content = content_map.get(fact_id)\n        if content is None:\n            continue\n        date_created = fact_row.get(\"date_created\")\n        similarity = float(similarities_map.get(fact_id, 0.0))\n        rank_score = float(rank_score_map.get(fact_id, similarity))\n        facts_with_similarity.append(\n            FactSearchResult(\n                id=fact_id,\n                content=content,\n                similarity=similarity,\n                rank_score=rank_score,\n                date_created=str(date_created) if date_created is not None else \"\",\n            )\n        )\n\n    return facts_with_similarity\n\n\ndef search_entity_facts_core(\n    entity_fact_driver: Any,\n    entity_id: int,\n    query_embedding: list[float],\n    limit: int,\n    embeddings_limit: int,\n    *,\n    query_text: str | None,\n    fact_candidates: list[FactCandidate] | None = None,\n    find_similar_embeddings: Callable[\n        [list[tuple[FactId, Any]], list[float], int], list[tuple[FactId, float]]\n    ],\n    lexical_scores_for_ids: Callable[..., dict[FactId, float]],\n    dense_lexical_weights: Callable[..., tuple[float, float]],\n) -> list[FactSearchResult]:\n    idx_to_original_id: dict[int, FactId] = {}\n    if fact_candidates is not None:\n        (\n            candidate_ids,\n            similarities_map,\n            content_map,\n            idx_to_original_id,\n            fact_rows,\n        ) = _candidate_pool_from_candidates(\n            fact_candidates, limit=limit, query_text=query_text\n        )\n        if not candidate_ids:\n            return []\n    else:\n        results = _get_embeddings_rows(\n            entity_fact_driver, entity_id=entity_id, embeddings_limit=embeddings_limit\n        )\n        if not results:\n            return []\n\n        embeddings = [(row[\"id\"], row[\"content_embedding\"]) for row in results]\n        cand_limit = _candidate_limit(\n            limit=limit, total_embeddings=len(embeddings), query_text=query_text\n        )\n        similar = find_similar_embeddings(embeddings, query_embedding, cand_limit)\n        if not similar:\n            logger.debug(\"No similar embeddings found\")\n            return []\n\n        candidate_ids = [fact_id for fact_id, _ in similar]\n        similarities_map = dict(similar)\n\n        fact_rows, content_map = _fetch_content_maps(\n            entity_fact_driver, candidate_ids=candidate_ids\n        )\n\n    # Cast to FactId types - in cloud path these are int indices,\n    # in DB path these are already FactId. Both are valid FactId values.\n    base_order, rank_score_map, lex_scores = _rank_candidates(\n        candidate_ids=cast(list[FactId], candidate_ids),\n        similarities_map=cast(dict[FactId, float], similarities_map),\n        query_text=query_text,\n        content_map=cast(dict[FactId, str], content_map),\n        lexical_scores_for_ids=lexical_scores_for_ids,\n        dense_lexical_weights=dense_lexical_weights,\n    )\n\n    ordered_ids = base_order[:limit]\n\n    facts_with_similarity = _build_fact_rows(\n        ordered_ids=ordered_ids,\n        fact_rows=cast(dict[FactId, dict], fact_rows),\n        content_map=cast(dict[FactId, str], content_map),\n        similarities_map=cast(dict[FactId, float], similarities_map),\n        rank_score_map=rank_score_map,\n    )\n\n    if fact_candidates is not None:\n        # Remap back to original cloud IDs.\n        remapped: list[FactSearchResult] = []\n        for row in facts_with_similarity:\n            rid = row.id\n            # In cloud path, rid is always int (internal index)\n            if isinstance(rid, int) and rid in idx_to_original_id:\n                remapped.append(\n                    FactSearchResult(\n                        id=idx_to_original_id[rid],\n                        content=row.content,\n                        similarity=row.similarity,\n                        rank_score=row.rank_score,\n                        date_created=row.date_created,\n                    )\n                )\n            else:\n                remapped.append(row)\n        facts_with_similarity = remapped\n\n    logger.debug(\n        \"Returning %d facts with similarity scores\", len(facts_with_similarity)\n    )\n\n    return facts_with_similarity\n"
  },
  {
    "path": "memori/search/_faiss.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nimport faiss\nimport numpy as np\n\nfrom memori.search._parsing import parse_embedding\nfrom memori.search._types import FactId\n\nlogger = logging.getLogger(__name__)\n\n\ndef _query_dim(query_embedding: list[float]) -> int:\n    return len(query_embedding)\n\n\ndef _parse_valid_embeddings(\n    embeddings: list[tuple[FactId, Any]], *, query_dim: int\n) -> tuple[list[np.ndarray], list[FactId]]:\n    embeddings_list: list[np.ndarray] = []\n    id_list: list[FactId] = []\n\n    for fact_id, raw in embeddings:\n        try:\n            parsed = parse_embedding(raw)\n        except Exception:\n            continue\n\n        if parsed.ndim != 1 or parsed.shape[0] != query_dim:\n            continue\n\n        embeddings_list.append(parsed)\n        id_list.append(fact_id)\n\n    return embeddings_list, id_list\n\n\ndef _stack_embeddings(embeddings_list: list[np.ndarray]) -> np.ndarray | None:\n    try:\n        return np.stack(embeddings_list, axis=0)\n    except ValueError:\n        return None\n\n\ndef _faiss_search(\n    *,\n    embeddings_array: np.ndarray,\n    query_embedding: list[float],\n    id_list: list[FactId],\n    limit: int,\n) -> list[tuple[FactId, float]]:\n    faiss.normalize_L2(embeddings_array)\n    query_array = np.asarray([query_embedding], dtype=np.float32)\n\n    if embeddings_array.shape[1] != query_array.shape[1]:\n        logger.debug(\n            \"Embedding dimension mismatch: db=%d, query=%d\",\n            embeddings_array.shape[1],\n            query_array.shape[1],\n        )\n        return []\n\n    faiss.normalize_L2(query_array)\n\n    index = faiss.IndexFlatIP(embeddings_array.shape[1])\n    index.add(embeddings_array)  # type: ignore[call-arg]\n\n    k = min(limit, len(embeddings_array))\n    similarities, indices = index.search(query_array, k)  # type: ignore[call-arg]\n\n    results: list[tuple[FactId, float]] = []\n    for result_idx, embedding_idx in enumerate(indices[0]):\n        if 0 <= embedding_idx < len(id_list):\n            results.append((id_list[embedding_idx], float(similarities[0][result_idx])))\n\n    return results\n\n\ndef find_similar_embeddings(\n    embeddings: list[tuple[FactId, Any]],\n    query_embedding: list[float],\n    limit: int = 5,\n) -> list[tuple[FactId, float]]:\n    \"\"\"Find most similar embeddings using FAISS cosine similarity.\"\"\"\n    if not embeddings:\n        logger.debug(\"find_similar_embeddings called with empty embeddings\")\n        return []\n\n    query_dim = _query_dim(query_embedding)\n    if query_dim == 0:\n        return []\n\n    embeddings_list, id_list = _parse_valid_embeddings(embeddings, query_dim=query_dim)\n\n    if not embeddings_list:\n        logger.debug(\"No valid embeddings after parsing\")\n        return []\n\n    logger.debug(\"Building FAISS index with %d embeddings\", len(embeddings_list))\n    embeddings_array = _stack_embeddings(embeddings_list)\n    if embeddings_array is None:\n        return []\n\n    results = _faiss_search(\n        embeddings_array=embeddings_array,\n        query_embedding=query_embedding,\n        id_list=id_list,\n        limit=limit,\n    )\n\n    if results:\n        scores = [round(score, 3) for _, score in results]\n        logger.debug(\n            \"FAISS similarity search complete - top %d matches: %s\",\n            len(results),\n            scores,\n        )\n\n    return results\n"
  },
  {
    "path": "memori/search/_lexical.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport math\nimport os\nimport re\nfrom collections import Counter\n\nfrom memori.search._types import FactId\n\nlogger = logging.getLogger(__name__)\n\n_TOKEN_RE = re.compile(r\"[a-z0-9]+\")\n_STOPWORDS = {\n    \"a\",\n    \"an\",\n    \"and\",\n    \"are\",\n    \"as\",\n    \"at\",\n    \"be\",\n    \"by\",\n    \"did\",\n    \"do\",\n    \"does\",\n    \"for\",\n    \"from\",\n    \"had\",\n    \"has\",\n    \"have\",\n    \"how\",\n    \"i\",\n    \"in\",\n    \"is\",\n    \"it\",\n    \"of\",\n    \"on\",\n    \"or\",\n    \"that\",\n    \"the\",\n    \"their\",\n    \"then\",\n    \"there\",\n    \"to\",\n    \"was\",\n    \"were\",\n    \"what\",\n    \"when\",\n    \"where\",\n    \"which\",\n    \"who\",\n    \"why\",\n    \"with\",\n    \"you\",\n    \"your\",\n}\n\n\ndef _tokenize(text: str) -> list[str]:\n    tokens = [t for t in _TOKEN_RE.findall((text or \"\").lower()) if t]\n    return [t for t in tokens if t not in _STOPWORDS]\n\n\ndef lexical_scores_for_ids(\n    *, query_text: str, ids: list[FactId], content_map: dict[FactId, str]\n) -> dict[FactId, float]:\n    \"\"\"\n    Compute a BM25 score in [0, 1] for each doc over the candidate pool.\n    \"\"\"\n    q_tokens = _tokenize(query_text)\n    if not q_tokens:\n        return dict.fromkeys(ids, 0.0)\n\n    docs_tf: dict[FactId, Counter[str]] = {}\n    doc_len: dict[FactId, int] = {}\n    for i in ids:\n        content = content_map.get(i, \"\")\n        toks = _tokenize(content)\n        docs_tf[i] = Counter(toks)\n        doc_len[i] = len(toks)\n\n    n_docs = len(ids)\n    avgdl = (sum(doc_len.values()) / float(n_docs)) if n_docs else 0.0\n\n    q_terms = set(q_tokens)\n    df: dict[str, int] = {}\n    for t in q_terms:\n        df[t] = sum(1 for i in ids if docs_tf.get(i, Counter()).get(t, 0) > 0)\n\n    k1 = 1.2\n    b = 0.75\n\n    def idf(t: str) -> float:\n        dft = float(df.get(t, 0))\n        return math.log(1.0 + ((n_docs - dft + 0.5) / (dft + 0.5)))\n\n    raw: dict[FactId, float] = {}\n    for i in ids:\n        tf = docs_tf.get(i, Counter())\n        dl = float(doc_len.get(i, 0))\n        denom_norm = (1.0 - b) + (b * (dl / avgdl)) if avgdl > 0 else 1.0\n        score = 0.0\n        for t in q_terms:\n            f = float(tf.get(t, 0))\n            if f <= 0.0:\n                continue\n            score += idf(t) * ((f * (k1 + 1.0)) / (f + (k1 * denom_norm)))\n        raw[i] = score\n\n    max_score = max(raw.values()) if raw else 0.0\n    if max_score <= 0.0:\n        return dict.fromkeys(ids, 0.0)\n\n    return {i: float(raw.get(i, 0.0) / max_score) for i in ids}\n\n\ndef dense_lexical_weights(*, query_text: str) -> tuple[float, float]:\n    \"\"\"\n    Return (w_cos, w_lex) for ranking.\n\n    We bias toward lexical matching for very short queries where exact terms\n    are usually high-signal.\n    \"\"\"\n    q_tokens = _tokenize(query_text)\n\n    try:\n        w_lex = float(os.environ.get(\"MEMORI_RECALL_LEX_WEIGHT\", \"0.15\") or \"0.15\")\n    except ValueError:\n        w_lex = 0.15\n    if len(q_tokens) <= 2:\n        try:\n            w_lex = float(\n                os.environ.get(\"MEMORI_RECALL_LEX_WEIGHT_SHORT\", \"0.30\") or \"0.30\"\n            )\n        except ValueError:\n            w_lex = 0.30\n    w_lex = max(0.05, min(0.40, w_lex))\n    return (1.0 - w_lex, w_lex)\n"
  },
  {
    "path": "memori/search/_parsing.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import Any\n\nimport numpy as np\n\n\ndef parse_embedding(raw: Any) -> np.ndarray:\n    \"\"\"Parse embedding from database format to numpy array.\n\n    Handles multiple storage formats:\n    - Binary (BYTEA/BLOB/BinData): Most common, used by all databases\n    - JSON string: Legacy format\n    - Native array: Fallback\n    \"\"\"\n    if isinstance(raw, bytes | memoryview):\n        return np.frombuffer(raw, dtype=\"<f4\")\n    if isinstance(raw, str):\n        return np.array(json.loads(raw), dtype=np.float32)\n\n    if hasattr(raw, \"__bytes__\"):\n        return np.frombuffer(bytes(raw), dtype=\"<f4\")\n    return np.asarray(raw, dtype=np.float32)\n"
  },
  {
    "path": "memori/search/_types.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from bson import ObjectId\n\n    FactId = int | ObjectId\nelse:\n    FactId = int | object  # object allows ObjectId at runtime\n\n\n@dataclass(frozen=True)\nclass FactCandidate:\n    id: FactId\n    content: str\n    score: float\n    date_created: str\n\n\n@dataclass(frozen=True)\nclass FactSearchResult:\n    id: FactId\n    content: str\n    similarity: float\n    rank_score: float\n    date_created: str\n\n    def to_dict(self) -> dict[str, object]:\n        return {\n            \"id\": self.id,\n            \"content\": self.content,\n            \"similarity\": self.similarity,\n            \"rank_score\": self.rank_score,\n            \"date_created\": self.date_created,\n        }\n"
  },
  {
    "path": "memori/storage/__init__.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport importlib\n\nfrom memori.storage._manager import Manager\n\n\ndef _import_optional_module(module_path: str) -> None:\n    try:\n        importlib.import_module(module_path)\n    except ImportError:\n        pass\n\n\n# Import adapters and drivers to trigger their self-registration decorators.\n# Order matters: more specific matchers (sqlalchemy, django) before generic ones (mongodb, dbapi)\nfor adapter in (\"sqlalchemy\", \"django\", \"mongodb\", \"dbapi\"):\n    _import_optional_module(f\"memori.storage.adapters.{adapter}\")\n\nfor driver in (\"mongodb\", \"mysql\", \"oceanbase\", \"oracle\", \"postgresql\", \"sqlite\"):\n    _import_optional_module(f\"memori.storage.drivers.{driver}\")\n\n__all__ = [\"Manager\"]\n"
  },
  {
    "path": "memori/storage/_base.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\n\nclass BaseStorageAdapter:\n    def __init__(self, conn):\n        if not callable(conn):\n            raise TypeError(\"conn must be a callable\")\n        self._release = None\n        self._cm = None\n\n        resource = conn()\n        if isinstance(resource, tuple) and len(resource) == 2 and callable(resource[1]):\n            self.conn = resource[0]\n            self._release = resource[1]\n            return\n\n        # Support factories that return a context manager, e.g.\n        # psycopg_pool.ConnectionPool.connection() which must be exited to\n        # return the connection to the pool.\n        if self._is_managed_resource(resource):\n            self._cm = resource\n            self.conn = resource.__enter__()\n\n            def _release():\n                try:\n                    self._cm.__exit__(None, None, None)\n                finally:\n                    self._cm = None\n\n            self._release = _release\n            return\n\n        self.conn = resource\n\n    def close(self):\n        if self.conn is not None:\n            if self._release is not None:\n                try:\n                    self._release()\n                finally:\n                    self._release = None\n                    self.conn = None\n                return\n\n            if hasattr(self.conn, \"close\"):\n                self.conn.close()\n            self.conn = None\n\n    @staticmethod\n    def _is_managed_resource(obj) -> bool:\n        # Only treat as a managed resource if it looks like a context manager\n        # and does NOT look like a DB connection/session itself.\n        if not (hasattr(obj, \"__enter__\") and hasattr(obj, \"__exit__\")):\n            return False\n\n        # DB-API connections commonly have cursor/commit/rollback.\n        if (\n            hasattr(obj, \"cursor\")\n            and hasattr(obj, \"commit\")\n            and hasattr(obj, \"rollback\")\n        ):\n            return False\n\n        # SQLAlchemy Session has get_bind.\n        if hasattr(obj, \"get_bind\"):\n            return False\n\n        # Django connection has vendor.\n        if hasattr(obj, \"vendor\"):\n            return False\n\n        # MongoDB clients/dbs have list_collection_names.\n        if hasattr(obj, \"list_collection_names\"):\n            return False\n\n        return True\n\n    def commit(self):\n        raise NotImplementedError\n\n    def execute(self, *args, **kwargs):\n        raise NotImplementedError\n\n    def flush(self):\n        raise NotImplementedError\n\n    def get_dialect(self):\n        raise NotImplementedError\n\n    def rollback(self):\n        raise NotImplementedError\n\n\nclass BaseConversation:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, session_id: int, timeout_minutes: int):\n        raise NotImplementedError\n\n    def update(self, id: int, summary: str):\n        raise NotImplementedError\n\n    def read(self, id: int) -> dict | None:\n        raise NotImplementedError\n\n\nclass BaseConversationMessage:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, conversation_id: int, role: str, type: str, content: str):\n        raise NotImplementedError\n\n\nclass BaseConversationMessages:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def read(self, conversation_id: int):\n        raise NotImplementedError\n\n\nclass BaseKnowledgeGraph:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, entity_id: int, semantic_triples: list):\n        raise NotImplementedError\n\n\nclass BaseEntity:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, external_id: str):\n        raise NotImplementedError\n\n\nclass BaseEntityFact:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, entity_id: int, facts: list, fact_embeddings: list | None = None):\n        raise NotImplementedError\n\n    def get_embeddings(self, entity_id: int, limit: int = 1000):\n        raise NotImplementedError\n\n    def get_facts_by_ids(self, fact_ids: list[int]):\n        raise NotImplementedError\n\n\nclass BaseProcess:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, external_id: str):\n        raise NotImplementedError\n\n\nclass BaseProcessAttribute:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, process_id: int, attributes: list):\n        raise NotImplementedError\n\n\nclass BaseSession:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, uuid: str, entity_id: int, process_id: int):\n        raise NotImplementedError\n\n\nclass BaseSchema:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n\nclass BaseSchemaVersion:\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conn = conn\n\n    def create(self, num: int):\n        raise NotImplementedError\n\n    def delete(self):\n        raise NotImplementedError\n\n    def read(self):\n        raise NotImplementedError\n"
  },
  {
    "path": "memori/storage/_builder.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori._cli import Cli\nfrom memori._config import Config\nfrom memori.storage._registry import Registry\n\n\nclass Builder:\n    def __init__(self, config: Config):\n        self.cli = Cli(config)\n        self.config = config\n        self.display_banner = True\n        self.registry = Registry()\n\n    def create_data_structures(self):\n        if self.display_banner:\n            self.cli.banner()\n\n        if self.config.storage is None or self.config.storage.adapter is None:\n            return self\n\n        dialect = self.config.storage.adapter.get_dialect()\n\n        try:\n            if self.config.storage is None or self.config.storage.driver is None:\n                raise RuntimeError(\"Driver not initialized\")\n\n            num = self.config.storage.driver.schema.version.read()\n            if num is None:\n                num = 0\n        except Exception:\n            if self._requires_rollback(dialect):\n                if (\n                    self.config.storage is not None\n                    and self.config.storage.adapter is not None\n                ):\n                    self.config.storage.adapter.rollback()\n\n            num = 0\n\n        self.cli.notice(f\"Currently at revision #{num}.\")\n\n        migrations = self._get_dialect_family(dialect)\n        if migrations is None:\n            raise NotImplementedError(\n                f\"No migration mapping found for dialect: {dialect}.\"\n            )\n\n        if num == max(migrations.keys()):\n            self.cli.notice(\"data structures are up-to-date\", 1)\n        else:\n            while True:\n                num += 1\n                if num not in migrations:\n                    break\n\n                self.cli.notice(f\"Building revision #{num}...\")\n\n                for migration in migrations[num]:\n                    self.cli.notice(migration[\"description\"], 1)\n                    if (\n                        self.config.storage is not None\n                        and self.config.storage.adapter is not None\n                    ):\n                        operation = migration.get(\"operations\") or migration.get(\n                            \"operation\"\n                        )\n                        self.config.storage.adapter.execute(operation)\n                        self.config.storage.adapter.commit()\n\n            if self.config.storage is None or self.config.storage.driver is None:\n                raise RuntimeError(\"Driver not initialized\")\n\n            self.config.storage.driver.schema.version.delete()\n            self.config.storage.driver.schema.version.create(num - 1)\n\n            if self.config.storage.adapter is not None:\n                self.config.storage.adapter.commit()\n\n        self.cli.notice(\"Build executed successfully!\")\n        self.cli.newline()\n\n        return self\n\n    def disable_banner(self):\n        self.display_banner = False\n        return self\n\n    def execute(self):\n        if self.config.storage is None or self.config.storage.adapter is None:\n            return self\n\n        dialect = self.config.storage.adapter.get_dialect()\n        supported_dialects = self._get_supported_dialects()\n\n        if dialect in supported_dialects:\n            self.create_data_structures()\n        else:\n            raise NotImplementedError(\n                f\"Unsupported dialect: {dialect}. \"\n                f\"Supported dialects: {supported_dialects}.\"\n            )\n\n        return self\n\n    def _get_supported_dialects(self):\n        return list(self.registry._drivers.keys())\n\n    def _get_dialect_family(self, dialect):\n        if dialect in self.registry._drivers:\n            driver_class = self.registry._drivers[dialect]\n            return getattr(driver_class, \"migrations\", None)\n\n        return None\n\n    def _requires_rollback(self, dialect):\n        if dialect in self.registry._drivers:\n            driver_class = self.registry._drivers[dialect]\n            return getattr(driver_class, \"requires_rollback_on_error\", False)\n\n        return False\n"
  },
  {
    "path": "memori/storage/_connection.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom collections.abc import Callable, Generator\nfrom contextlib import contextmanager\nfrom typing import Any\n\nfrom memori.storage._base import BaseStorageAdapter\nfrom memori.storage._registry import Registry\n\n\n@contextmanager\ndef connection_context(\n    conn_factory: Callable[[], Any] | None,\n) -> Generator[\n    tuple[Any, BaseStorageAdapter, Any] | tuple[None, None, None], None, None\n]:\n    if conn_factory is None:\n        yield None, None, None\n        return\n\n    conn = conn_factory()\n    adapter = Registry().adapter(lambda: conn)\n    driver = Registry().driver(adapter)\n\n    try:\n        yield conn, adapter, driver\n        adapter.commit()\n    except Exception:\n        try:\n            adapter.rollback()\n        except Exception:  # nosec B110\n            pass\n        raise\n    finally:\n        try:\n            adapter.close()\n        except Exception:  # nosec B110\n            pass\n"
  },
  {
    "path": "memori/storage/_manager.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori._config import Config\nfrom memori.storage._builder import Builder\nfrom memori.storage._connection import connection_context\nfrom memori.storage._registry import Registry\n\n\nclass Manager:\n    def __init__(self, config: Config) -> None:\n        self.adapter = None\n        self.config = config\n        self.conn_factory = None\n        self.driver = None\n\n    @property\n    def conn(self):\n        return connection_context(self.conn_factory)\n\n    def build(self) -> \"Manager\":\n        if self.conn_factory is None:\n            return self\n\n        Builder(self.config).execute()\n\n        return self\n\n    def start(self, conn) -> \"Manager\":\n        if conn is None:\n            return self\n\n        if callable(conn):\n            self.conn_factory = conn\n        else:\n            self.conn_factory = lambda: conn\n\n        self.adapter = Registry().adapter(conn)\n        self.driver = Registry().driver(self.adapter)\n\n        dialect = self.adapter.get_dialect()\n        self.config.storage_config.cockroachdb = dialect == \"cockroachdb\"\n\n        return self\n"
  },
  {
    "path": "memori/storage/_registry.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom memori._exceptions import UnsupportedDatabaseError\nfrom memori.storage._base import BaseStorageAdapter\n\n\nclass Registry:\n    _adapters: dict[Callable[[Any], bool], type[BaseStorageAdapter]] = {}\n    _drivers: dict[str, type] = {}\n\n    @classmethod\n    def register_adapter(cls, matcher: Callable[[Any], bool]):\n        def decorator(adapter_class: type[BaseStorageAdapter]):\n            cls._adapters[matcher] = adapter_class\n            return adapter_class\n\n        return decorator\n\n    @classmethod\n    def register_driver(cls, dialect: str):\n        def decorator(driver_class: type):\n            cls._drivers[dialect] = driver_class\n            return driver_class\n\n        return decorator\n\n    def adapter(self, conn: Any) -> BaseStorageAdapter:\n        conn_to_check = conn() if callable(conn) else conn\n\n        # Support factories that return (conn, release) tuples.\n        conn_for_match = (\n            conn_to_check[0] if isinstance(conn_to_check, tuple) else conn_to_check\n        )\n\n        # Support factories that return a context manager (e.g. psycopg_pool).\n        if BaseStorageAdapter._is_managed_resource(conn_for_match):\n            cm = conn_for_match\n            real_conn = cm.__enter__()\n\n            def _release(cm=cm):\n                return cm.__exit__(None, None, None)\n\n            conn_to_check = (real_conn, _release)\n            conn_for_match = real_conn\n\n        for matcher, adapter_class in self._adapters.items():\n            if matcher(conn_for_match):\n                return adapter_class(lambda: conn_to_check)\n\n        raise UnsupportedDatabaseError()\n\n    def driver(self, conn: BaseStorageAdapter):\n        dialect = conn.get_dialect()\n        if dialect not in self._drivers:\n            raise RuntimeError(f\"Unsupported database dialect: {dialect}\")\n        return self._drivers[dialect](conn)\n"
  },
  {
    "path": "memori/storage/adapters/dbapi/__init__.py",
    "content": "from memori.storage.adapters.dbapi._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/storage/adapters/dbapi/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.storage._base import BaseStorageAdapter\nfrom memori.storage._registry import Registry\n\n\nclass CursorWrapper:\n    def __init__(self, cursor):\n        self._cursor = cursor\n\n    def mappings(self):\n        return MappingResult(self._cursor)\n\n    def __getattr__(self, name):\n        return getattr(self._cursor, name)\n\n\nclass MappingResult:\n    def __init__(self, cursor):\n        self._cursor = cursor\n\n    def fetchone(self):\n        row = self._cursor.fetchone()\n        if row is None:\n            return None\n        columns = [col[0] for col in self._cursor.description]\n        return dict(zip(columns, row, strict=True))\n\n    def fetchall(self):\n        rows = self._cursor.fetchall()\n        columns = [col[0] for col in self._cursor.description]\n        return [dict(zip(columns, row, strict=True)) for row in rows]\n\n\ndef is_dbapi_connection(conn):\n    if not (\n        hasattr(conn, \"cursor\")\n        and hasattr(conn, \"commit\")\n        and hasattr(conn, \"rollback\")\n        and callable(getattr(conn, \"cursor\", None))\n        and callable(getattr(conn, \"commit\", None))\n        and callable(getattr(conn, \"rollback\", None))\n    ):\n        return False\n\n    if hasattr(conn, \"__class__\"):\n        module_name = conn.__class__.__module__\n        if module_name.startswith(\"django.db\"):\n            return False\n        class_name = conn.__class__.__name__\n        if class_name in (\"Session\", \"scoped_session\", \"AsyncSession\"):\n            return False\n        if hasattr(conn, \"get_bind\"):\n            return False\n\n    return True\n\n\n@Registry.register_adapter(is_dbapi_connection)\nclass Adapter(BaseStorageAdapter):\n    def commit(self):\n        self.conn.commit()\n        return self\n\n    def execute(self, operation, binds=()):\n        cursor = self.conn.cursor()\n        try:\n            cursor.execute(operation, binds)\n            return CursorWrapper(cursor)\n        except Exception:\n            cursor.close()\n            raise\n\n    def flush(self):\n        return self\n\n    def get_dialect(self):\n        module_name = type(self.conn).__module__\n        dialect_mapping = {\n            \"postgresql\": [\"psycopg\"],\n            \"mysql\": [\"mysql\", \"MySQLdb\", \"pymysql\"],\n            \"oceanbase\": [\"pyobvector\"],\n            \"sqlite\": [\"sqlite\"],\n            \"oracle\": [\"cx_Oracle\", \"oracledb\"],\n        }\n        for dialect, identifiers in dialect_mapping.items():\n            if any(identifier in module_name for identifier in identifiers):\n                return dialect\n        raise ValueError(\n            f\"Unable to determine dialect from connection module: {module_name}\"\n        )\n\n    def rollback(self):\n        self.conn.rollback()\n        return self\n"
  },
  {
    "path": "memori/storage/adapters/django/__init__.py",
    "content": "from memori.storage.adapters.django._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/storage/adapters/django/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.storage._base import BaseStorageAdapter\nfrom memori.storage._registry import Registry\n\n\nclass CursorWrapper:\n    def __init__(self, cursor):\n        self._cursor = cursor\n\n    def mappings(self):\n        return MappingResult(self._cursor)\n\n    def __getattr__(self, name):\n        return getattr(self._cursor, name)\n\n\nclass MappingResult:\n    def __init__(self, cursor):\n        self._cursor = cursor\n\n    def fetchone(self):\n        row = self._cursor.fetchone()\n        if row is None:\n            return None\n        columns = [col[0] for col in self._cursor.description]\n        return dict(zip(columns, row, strict=True))\n\n    def fetchall(self):\n        rows = self._cursor.fetchall()\n        columns = [col[0] for col in self._cursor.description]\n        return [dict(zip(columns, row, strict=True)) for row in rows]\n\n\ndef is_django_connection(conn):\n    if not hasattr(conn, \"__class__\"):\n        return False\n\n    module_name = conn.__class__.__module__\n    if not module_name.startswith(\"django.db\"):\n        return False\n\n    if not (hasattr(conn, \"cursor\") and callable(getattr(conn, \"cursor\", None))):\n        return False\n\n    return True\n\n\n@Registry.register_adapter(is_django_connection)\nclass Adapter(BaseStorageAdapter):\n    def commit(self):\n        self.conn.commit()\n        return self\n\n    def execute(self, operation, binds=()):\n        cursor = self.conn.cursor()\n        try:\n            cursor.execute(operation, binds)\n            return CursorWrapper(cursor)\n        except Exception:\n            cursor.close()\n            raise\n\n    def flush(self):\n        return self\n\n    def get_dialect(self):\n        vendor = self.conn.vendor\n        dialect_mapping = {\n            \"postgresql\": \"postgresql\",\n            \"mysql\": \"mysql\",\n            \"sqlite\": \"sqlite\",\n            \"oracle\": \"oracle\",\n        }\n        if vendor in dialect_mapping:\n            return dialect_mapping[vendor]\n        raise ValueError(f\"Unable to determine dialect from Django vendor: {vendor}\")\n\n    def rollback(self):\n        self.conn.rollback()\n        return self\n"
  },
  {
    "path": "memori/storage/adapters/mongodb/__init__.py",
    "content": "from memori.storage.adapters.mongodb._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/storage/adapters/mongodb/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom pymongo.synchronous.mongo_client import MongoClient\n\nfrom memori.storage._base import BaseStorageAdapter\nfrom memori.storage._registry import Registry\n\n\n@Registry.register_adapter(\n    lambda conn: hasattr(conn, \"database\") and hasattr(conn, \"list_collection_names\")\n)\nclass Adapter(BaseStorageAdapter):\n    \"\"\"MongoDB storage adapter for MongoDB database connections.\"\"\"\n\n    def execute(self, collection_name_or_ops, operation=None, *args, **kwargs):\n        \"\"\"Execute MongoDB operations.\n\n        Args:\n            collection_name_or_ops: Collection name, list of ops, or single op dict\n            operation: MongoDB operation (find_one, insert_one, etc.) - optional\n            *args: Positional arguments for the operation\n            **kwargs: Keyword arguments for the operation\n        \"\"\"\n        if isinstance(self.conn, MongoClient):\n            db = self.conn.get_default_database()\n        else:\n            db = self.conn\n\n        if db is None:\n            raise RuntimeError(\"MongoDB database connection is None\")\n\n        if operation is None:\n            if isinstance(collection_name_or_ops, list):\n                for op in collection_name_or_ops:\n                    self._execute_operation(db, op)\n            elif isinstance(collection_name_or_ops, dict):\n                self._execute_operation(db, collection_name_or_ops)\n            return None\n\n        collection = db[collection_name_or_ops]\n        return getattr(collection, operation)(*args, **kwargs)\n\n    def commit(self):\n        \"\"\"MongoDB doesn't require explicit commits for single operations.\"\"\"\n        pass\n\n    def flush(self):\n        \"\"\"MongoDB doesn't require explicit flushes for single operations.\"\"\"\n        pass\n\n    def rollback(self):\n        \"\"\"MongoDB doesn't require explicit rollbacks for single operations.\"\"\"\n        pass\n\n    def close(self):\n        \"\"\"MongoDB client connection should not be closed per-operation.\n\n        The MongoClient is designed to be long-lived and shared across threads.\n        Closing it would invalidate all connections from the client pool.\n        \"\"\"\n        pass\n\n    def get_dialect(self):\n        return \"mongodb\"\n\n    def _execute_operation(self, db, op):\n        \"\"\"Execute a single MongoDB operation from a dict.\n\n        Args:\n            db: MongoDB database instance\n            op: Dict with 'collection', 'method', 'args', and 'kwargs' keys\n        \"\"\"\n        collection = db[op[\"collection\"]]\n        method = getattr(collection, op[\"method\"])\n        args = op.get(\"args\", [])\n        kwargs = op.get(\"kwargs\", {})\n        method(*args, **kwargs)\n"
  },
  {
    "path": "memori/storage/adapters/sqlalchemy/__init__.py",
    "content": "from memori.storage.adapters.sqlalchemy._adapter import Adapter\n\n__all__ = [\"Adapter\"]\n"
  },
  {
    "path": "memori/storage/adapters/sqlalchemy/_adapter.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.storage._base import BaseStorageAdapter\nfrom memori.storage._registry import Registry\n\n\n@Registry.register_adapter(\n    lambda conn: type(conn).__module__ == \"sqlalchemy.orm.session\"\n)\nclass Adapter(BaseStorageAdapter):\n    def commit(self):\n        self.conn.commit()\n        return self\n\n    def execute(self, operation, binds=()):\n        return self.conn.connection().exec_driver_sql(operation, binds)\n\n    def flush(self):\n        self.conn.flush()\n        return self\n\n    def get_dialect(self):\n        dialect = self.conn.get_bind().dialect\n        module_name = dialect.__class__.__module__\n        if module_name.startswith(\"pyobvector.\"):\n            return \"oceanbase\"\n        return dialect.name\n\n    def rollback(self):\n        self.conn.rollback()\n        return self\n"
  },
  {
    "path": "memori/storage/cockroachdb/_cluster_manager.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom typing import Any\n\nimport requests\n\nfrom memori._cli import Cli\nfrom memori._config import Config\nfrom memori._exceptions import MissingPsycopgError\nfrom memori._network import Api\nfrom memori.storage._builder import Builder\nfrom memori.storage._manager import Manager\nfrom memori.storage.cockroachdb._display import Display\nfrom memori.storage.cockroachdb._files import Files\n\n\nclass ClusterManager:\n    def __init__(self, config: Config):\n        self.config = config\n        self.display = Display()\n        self.files = Files()\n\n    def claim(self, cli: Cli):\n        if not self.cluster_is_started():\n            cli.notice(self.display.cluster_was_not_started())\n            return self\n\n        cluster_id = self.files.read_id()\n        if cluster_id is not None:\n            claim = Api(Config()).get(f\"cockroachdb/cluster/claim/{cluster_id}\")\n\n        cli.notice(\n            \"You can claim the CockroachDB cluster by using this URL:\\n    \"\n            + claim[\"url\"]\n        )\n        cli.newline()\n\n        return self\n\n    def delete(self, cli: Cli):\n        if not self.cluster_is_started():\n            cli.notice(self.display.cluster_was_not_started())\n            return self\n\n        cluster_id = self.files.read_id()\n        if cluster_id is not None:\n            try:\n                Api(Config()).delete(f\"cockroachdb/cluster/{cluster_id}\")\n            except requests.exceptions.HTTPError as e:\n                if e.response is None or e.response.status_code != 404:\n                    raise\n\n        self.files.remove_id()\n\n        cli.notice(\"The CockroachDB cluster has been deleted.\")\n        cli.newline()\n\n        return self\n\n    def execute(self):\n        cli = Cli(self.config)\n\n        if sys.argv[2] != \"cluster\" or sys.argv[3] not in [\"start\", \"claim\", \"delete\"]:\n            self.usage()\n            cli.newline()\n            sys.exit(1)\n\n        if sys.argv[3] == \"claim\":\n            self.claim(cli)\n        elif sys.argv[3] == \"start\":\n            self.start(cli)\n        elif sys.argv[3] == \"delete\":\n            self.delete(cli)\n\n        return self\n\n    def start(self, cli: Cli):\n        if self.cluster_is_started():\n            cli.notice(self.display.cluster_already_started())\n            return self\n\n        cli.print(\n            \"Before we begin, we want you to know that security and privacy are\\n\"\n            + \"very important to us. Once we create this cluster for you we can\\n\"\n            + \"never access it again unless you provide us with your connection\\n\"\n            + \"string and we cannot help you recover your credentials if you lose\\n\"\n            + \"them.\\n\"\n        )\n\n        cli.print(\"This process may take a minute or longer, please be patient.\")\n\n        cli.newline()\n        proceed = input(\"Proceed [Y/n] \")\n        cli.newline()\n\n        if proceed is not None and proceed not in [\"y\", \"Y\", \"\"]:\n            sys.exit(0)\n\n        cli.notice(\"[Step 1] Starting new cluster: \", end=\"\")\n\n        started = Api(Config()).post(\"cockroachdb/cluster/start\")\n\n        cli.print(\"done\")\n\n        cli.notice(\"[Step 2] Waiting for cluster to come online: \", end=\"\")\n\n        finalized: dict[str, Any] | None = None\n        for _i in range(24):\n            finalized = Api(Config()).post(\n                f\"cockroachdb/cluster/finalize/{started['cluster']['uuid']}\",\n                json={\"cluster\": {\"id\": started[\"cluster\"][\"id\"]}},\n            )\n            if finalized[\"status\"] == 1:\n                break\n\n            time.sleep(5)\n\n        if finalized is None or finalized[\"status\"] != 1:\n            self.cluster_finalize_failed()\n            return\n\n        self.files.write_id(started[\"cluster\"][\"uuid\"])\n\n        cli.print(\"done\")\n\n        cli.notice(\"[Step 3] Creating the Memori schema:\\n\")\n\n        try:\n            import psycopg\n        except ImportError as e:\n            raise MissingPsycopgError(\"CockroachDB\") from e\n\n        self.config.storage = Manager(self.config).start(\n            lambda: psycopg.connect(finalized[\"connection\"][\"string\"])\n        )\n        Builder(self.config).disable_banner().execute()\n\n        cli.notice(\"--- YOUR COCKROACHDB CLUSTER IS READY! ---\\n\")\n\n        cli.notice(\"Please save your connection string:\")\n        cli.notice(finalized[\"connection\"][\"string\"], 1)\n        cli.newline()\n\n        cli.notice(\"To use your cluster, set the following environment variable:\")\n        cli.notice(\n            f\"MEMORI_COCKROACHDB_CONNECTION_STRING={finalized['connection']['string']}\",\n            1,\n        )\n        cli.newline()\n\n        cli.notice(\"You have to claim this database in 7 days or it will be deleted!\")\n        cli.notice(finalized[\"claim\"][\"url\"], 1)\n\n        cli.newline()\n\n        cli.notice(\"You're all set!\")\n        cli.newline()\n\n    def cluster_finalize_failed(self):\n        raise RuntimeError(\"the cluster failed to come online; please try again\")\n\n    def cluster_is_started(self):\n        return os.path.isfile(self.files.cluster_id())\n\n    def usage(self):\n        print(\"usage: python -m memori cockroachdb cluster <start | claim | delete>\")\n"
  },
  {
    "path": "memori/storage/cockroachdb/_display.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom memori.storage.cockroachdb._files import Files\n\n\nclass Display:\n    def __init__(self):\n        self.files = Files()\n\n    def cluster_already_started(self):\n        return \"\"\"You already have an active CockroachDB cluster running. To start\n  a new one, execute this command first:\n\n    python -m memori cockroachdb cluster delete\n\"\"\"\n\n    def cluster_was_not_started(self):\n        return \"\"\"You do not have an active CockroachDB cluster running. To start\n  a new one, execute this command first:\n\n    python -m memori cockroachdb cluster start\n\"\"\"\n"
  },
  {
    "path": "memori/storage/cockroachdb/_files.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nimport os\n\n\nclass Files:\n    def __init__(self):\n        self.filename_cluster_id = \"id\"\n\n    def cluster_dir(self):\n        return self.storage_dir() + \"/cluster\"\n\n    def cluster_id(self):\n        return \"/\".join([self.cluster_dir(), self.filename_cluster_id])\n\n    def makedirs(self):\n        os.makedirs(self.cluster_dir(), exist_ok=True)\n        return self\n\n    def read_id(self):\n        try:\n            with open(self.cluster_id()) as f:\n                return f.read().lstrip().rstrip()\n        except FileNotFoundError:\n            return None\n\n    def remove_id(self):\n        try:\n            os.unlink(self.cluster_id())\n        except FileNotFoundError:\n            pass\n\n        return self\n\n    def storage_dir(self):\n        home = os.environ.get(\"MEMORI_HOME\", None)\n        if home is None:\n            home = os.environ.get(\"HOME\", None)\n\n        if home is None:\n            raise RuntimeError(\n                \"neither MEMORI_HOME nor HOME environment variable is set\"\n            )\n\n        return f\"{home}/.memori\"\n\n    def write_id(self, id):\n        self.makedirs()\n\n        with open(self.cluster_id(), \"w\") as f:\n            f.write(id)\n\n        return self\n"
  },
  {
    "path": "memori/storage/drivers/mongodb/__init__.py",
    "content": "from memori.storage.drivers.mongodb._driver import Driver\n\n__all__ = [\"Driver\"]\n"
  },
  {
    "path": "memori/storage/drivers/mongodb/_driver.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom datetime import datetime, timezone\nfrom uuid import uuid4\n\nfrom memori.storage._base import (\n    BaseConversation,\n    BaseConversationMessage,\n    BaseConversationMessages,\n    BaseEntity,\n    BaseEntityFact,\n    BaseKnowledgeGraph,\n    BaseProcess,\n    BaseProcessAttribute,\n    BaseSchema,\n    BaseSchemaVersion,\n    BaseSession,\n    BaseStorageAdapter,\n)\nfrom memori.storage._registry import Registry\nfrom memori.storage.migrations._mongodb import migrations\n\n\nclass Conversation(BaseConversation):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.message = ConversationMessage(conn)\n        self.messages = ConversationMessages(conn)\n\n    def create(self, session_id, timeout_minutes: int):\n        existing = self.conn.execute(\n            \"memori_conversation\", \"find_one\", {\"session_id\": session_id}\n        )\n\n        if existing:\n            last_message = self.conn.execute(\n                \"memori_conversation_message\",\n                \"find_one\",\n                {\"conversation_id\": existing[\"_id\"]},\n                sort=[(\"date_created\", -1)],\n            )\n\n            last_activity = (\n                last_message[\"date_created\"]\n                if last_message\n                else existing[\"date_created\"]\n            )\n\n            now = datetime.now(timezone.utc)\n            minutes_elapsed = (now - last_activity).total_seconds() / 60\n\n            if minutes_elapsed <= timeout_minutes:\n                return existing.get(\"_id\")\n\n        conversation_uuid = str(uuid4())\n        conversation_doc = {\n            \"uuid\": conversation_uuid,\n            \"session_id\": session_id,\n            \"summary\": None,\n            \"date_created\": datetime.now(timezone.utc),\n            \"date_updated\": None,\n        }\n\n        result = self.conn.execute(\n            \"memori_conversation\", \"insert_one\", conversation_doc\n        )\n\n        return result.inserted_id\n\n    def update(self, id: int, summary: str):\n        if summary is None:\n            return self\n\n        self.conn.execute(\n            \"memori_conversation\",\n            \"update_one\",\n            {\"_id\": id},\n            {\"$set\": {\"summary\": summary}},\n        )\n\n        return self\n\n    def read(self, id: int) -> dict | None:\n        result = self.conn.execute(\n            \"memori_conversation\",\n            \"find_one\",\n            {\"_id\": id},\n        )\n\n        if result is None:\n            return None\n\n        # Convert MongoDB result to dict, excluding _id or converting it to id\n        conversation = dict(result)\n        if \"_id\" in conversation:\n            conversation[\"id\"] = conversation.pop(\"_id\")\n\n        return conversation\n\n    def read_id_by_session_id(self, session_id):\n        existing = self.conn.execute(\n            \"memori_conversation\", \"find_one\", {\"session_id\": session_id}\n        )\n        if not existing:\n            return None\n        return existing.get(\"_id\")\n\n\nclass ConversationMessage(BaseConversationMessage):\n    def create(self, conversation_id: int, role: str, type: str, content: str):\n        message_doc = {\n            \"uuid\": str(uuid4()),\n            \"conversation_id\": conversation_id,\n            \"role\": role,\n            \"type\": type,\n            \"content\": content,\n            \"date_created\": datetime.now(timezone.utc),\n            \"date_updated\": None,\n        }\n\n        self.conn.execute(\"memori_conversation_message\", \"insert_one\", message_doc)\n\n\nclass ConversationMessages(BaseConversationMessages):\n    def read(self, conversation_id: int):\n        results = self.conn.execute(\n            \"memori_conversation_message\",\n            \"find\",\n            {\"conversation_id\": conversation_id},\n            {\"role\": 1, \"content\": 1, \"_id\": 0},\n        )\n\n        messages = []\n        for result in results:\n            messages.append({\"content\": result[\"content\"], \"role\": result[\"role\"]})\n\n        return messages\n\n\nclass Entity(BaseEntity):\n    def create(self, external_id: str):\n        # Check if entity already exists\n        existing = self.conn.execute(\n            \"memori_entity\", \"find_one\", {\"external_id\": external_id}\n        )\n\n        if existing:\n            return existing.get(\"_id\")\n\n        # Create new entity\n        entity_doc = {\n            \"uuid\": str(uuid4()),\n            \"external_id\": external_id,\n            \"date_created\": datetime.now(timezone.utc),\n            \"date_updated\": None,\n        }\n\n        result = self.conn.execute(\"memori_entity\", \"insert_one\", entity_doc)\n\n        return result.inserted_id\n\n\nclass EntityFact(BaseEntityFact):\n    def create(self, entity_id: int, facts: list, fact_embeddings: list | None = None):\n        if facts is None or len(facts) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n        from memori.embeddings import format_embedding_for_db\n\n        for i, fact in enumerate(facts):\n            embedding = (\n                fact_embeddings[i]\n                if fact_embeddings and i < len(fact_embeddings)\n                else []\n            )\n            embedding_formatted = format_embedding_for_db(embedding, \"mongodb\")\n            uniq = generate_uniq([fact])\n\n            # Check if fact already exists\n            existing = self.conn.execute(\n                \"memori_entity_fact\",\n                \"find_one\",\n                {\"entity_id\": entity_id, \"uniq\": uniq},\n            )\n\n            if existing:\n                # Update existing fact\n                self.conn.execute(\n                    \"memori_entity_fact\",\n                    \"update_one\",\n                    {\"_id\": existing[\"_id\"]},\n                    {\n                        \"$inc\": {\"num_times\": 1},\n                        \"$set\": {\"date_last_time\": datetime.now(timezone.utc)},\n                    },\n                )\n            else:\n                # Insert new fact\n                fact_doc = {\n                    \"uuid\": str(uuid4()),\n                    \"entity_id\": entity_id,\n                    \"content\": fact,\n                    \"content_embedding\": embedding_formatted,\n                    \"num_times\": 1,\n                    \"date_last_time\": datetime.now(timezone.utc),\n                    \"uniq\": uniq,\n                    \"date_created\": datetime.now(timezone.utc),\n                    \"date_updated\": None,\n                }\n\n                self.conn.execute(\"memori_entity_fact\", \"insert_one\", fact_doc)\n\n        return self\n\n    def get_embeddings(self, entity_id: int, limit: int = 1000):\n        results = self.conn.execute(\n            \"memori_entity_fact\",\n            \"find\",\n            {\"entity_id\": entity_id},\n            {\"_id\": 1, \"content_embedding\": 1},\n        )\n\n        embeddings = []\n        if hasattr(results, \"limit\"):\n            results = results.sort(\n                [(\"date_last_time\", -1), (\"num_times\", -1), (\"_id\", -1)]\n            ).limit(limit)\n            iterable = results\n        else:\n            materialized = list(results)\n\n            def key(doc):\n                dt = doc.get(\"date_last_time\")\n                num = doc.get(\"num_times\")\n                _id = doc.get(\"_id\")\n                return (\n                    dt if dt is not None else 0,\n                    num if num is not None else 0,\n                    _id if _id is not None else 0,\n                )\n\n            iterable = sorted(materialized, key=key, reverse=True)[:limit]\n\n        for result in iterable:\n            embeddings.append(\n                {\"id\": result[\"_id\"], \"content_embedding\": result[\"content_embedding\"]}\n            )\n\n        return embeddings\n\n    def get_facts_by_ids(self, fact_ids: list[int]):\n        if not fact_ids:\n            return []\n\n        results = self.conn.execute(\n            \"memori_entity_fact\",\n            \"find\",\n            {\"_id\": {\"$in\": fact_ids}},\n            {\"_id\": 1, \"content\": 1, \"date_created\": 1},\n        )\n\n        facts = []\n        for result in results:\n            facts.append(\n                {\n                    \"id\": result[\"_id\"],\n                    \"content\": result[\"content\"],\n                    \"date_created\": result.get(\"date_created\"),\n                }\n            )\n\n        return facts\n\n\nclass KnowledgeGraph(BaseKnowledgeGraph):\n    def create(self, entity_id: int, semantic_triples: list):\n        if semantic_triples is None or len(semantic_triples) == 0:\n            return self\n\n        from datetime import datetime, timezone\n\n        from memori._utils import generate_uniq\n\n        for semantic_triple in semantic_triples:\n            # Insert or get subject\n            subject_uniq = generate_uniq(\n                [semantic_triple.subject_name, semantic_triple.subject_type]\n            )\n            existing_subject = self.conn.execute(\n                \"memori_subject\", \"find_one\", {\"uniq\": subject_uniq}\n            )\n\n            if existing_subject:\n                subject_id = existing_subject[\"_id\"]\n            else:\n                subject_doc = {\n                    \"uuid\": str(uuid4()),\n                    \"name\": semantic_triple.subject_name,\n                    \"type\": semantic_triple.subject_type,\n                    \"uniq\": subject_uniq,\n                    \"date_created\": datetime.now(timezone.utc),\n                    \"date_updated\": None,\n                }\n                result = self.conn.execute(\"memori_subject\", \"insert_one\", subject_doc)\n                subject_id = result.inserted_id\n\n            # Insert or get predicate\n            predicate_uniq = generate_uniq([semantic_triple.predicate])\n            existing_predicate = self.conn.execute(\n                \"memori_predicate\", \"find_one\", {\"uniq\": predicate_uniq}\n            )\n\n            if existing_predicate:\n                predicate_id = existing_predicate[\"_id\"]\n            else:\n                predicate_doc = {\n                    \"uuid\": str(uuid4()),\n                    \"content\": semantic_triple.predicate,\n                    \"uniq\": predicate_uniq,\n                    \"date_created\": datetime.now(timezone.utc),\n                    \"date_updated\": None,\n                }\n                result = self.conn.execute(\n                    \"memori_predicate\", \"insert_one\", predicate_doc\n                )\n                predicate_id = result.inserted_id\n\n            # Insert or get object\n            object_uniq = generate_uniq(\n                [semantic_triple.object_name, semantic_triple.object_type]\n            )\n            existing_object = self.conn.execute(\n                \"memori_object\", \"find_one\", {\"uniq\": object_uniq}\n            )\n\n            if existing_object:\n                object_id = existing_object[\"_id\"]\n            else:\n                object_doc = {\n                    \"uuid\": str(uuid4()),\n                    \"name\": semantic_triple.object_name,\n                    \"type\": semantic_triple.object_type,\n                    \"uniq\": object_uniq,\n                    \"date_created\": datetime.now(timezone.utc),\n                    \"date_updated\": None,\n                }\n                result = self.conn.execute(\"memori_object\", \"insert_one\", object_doc)\n                object_id = result.inserted_id\n\n            # Insert or update knowledge graph entry\n            if (\n                entity_id is not None\n                and subject_id is not None\n                and predicate_id is not None\n                and object_id is not None\n            ):\n                existing_kg = self.conn.execute(\n                    \"memori_knowledge_graph\",\n                    \"find_one\",\n                    {\n                        \"entity_id\": entity_id,\n                        \"subject_id\": subject_id,\n                        \"predicate_id\": predicate_id,\n                        \"object_id\": object_id,\n                    },\n                )\n\n                if existing_kg:\n                    self.conn.execute(\n                        \"memori_knowledge_graph\",\n                        \"update_one\",\n                        {\"_id\": existing_kg[\"_id\"]},\n                        {\n                            \"$inc\": {\"num_times\": 1},\n                            \"$set\": {\"date_last_time\": datetime.now(timezone.utc)},\n                        },\n                    )\n                else:\n                    kg_doc = {\n                        \"uuid\": str(uuid4()),\n                        \"entity_id\": entity_id,\n                        \"subject_id\": subject_id,\n                        \"predicate_id\": predicate_id,\n                        \"object_id\": object_id,\n                        \"num_times\": 1,\n                        \"date_last_time\": datetime.now(timezone.utc),\n                        \"date_created\": datetime.now(timezone.utc),\n                        \"date_updated\": None,\n                    }\n                    self.conn.execute(\"memori_knowledge_graph\", \"insert_one\", kg_doc)\n\n        return self\n\n\nclass Process(BaseProcess):\n    def create(self, external_id: str):\n        # Check if process already exists\n        existing = self.conn.execute(\n            \"memori_process\", \"find_one\", {\"external_id\": external_id}\n        )\n\n        if existing:\n            return existing.get(\"_id\")\n\n        # Create new process\n        process_doc = {\n            \"uuid\": str(uuid4()),\n            \"external_id\": external_id,\n            \"date_created\": datetime.now(timezone.utc),\n            \"date_updated\": None,\n        }\n\n        result = self.conn.execute(\"memori_process\", \"insert_one\", process_doc)\n\n        return result.inserted_id\n\n\nclass ProcessAttribute(BaseProcessAttribute):\n    def create(self, process_id: int, attributes: list):\n        if attributes is None or len(attributes) == 0:\n            return self\n\n        from datetime import datetime, timezone\n\n        from memori._utils import generate_uniq\n\n        for attribute in attributes:\n            uniq = generate_uniq([attribute])\n            existing = self.conn.execute(\n                \"memori_process_attribute\",\n                \"find_one\",\n                {\"process_id\": process_id, \"uniq\": uniq},\n            )\n\n            if existing:\n                self.conn.execute(\n                    \"memori_process_attribute\",\n                    \"update_one\",\n                    {\"_id\": existing[\"_id\"]},\n                    {\n                        \"$inc\": {\"num_times\": 1},\n                        \"$set\": {\"date_last_time\": datetime.now(timezone.utc)},\n                    },\n                )\n            else:\n                attribute_doc = {\n                    \"uuid\": str(uuid4()),\n                    \"process_id\": process_id,\n                    \"content\": attribute,\n                    \"num_times\": 1,\n                    \"date_last_time\": datetime.now(timezone.utc),\n                    \"uniq\": uniq,\n                    \"date_created\": datetime.now(timezone.utc),\n                    \"date_updated\": None,\n                }\n                self.conn.execute(\n                    \"memori_process_attribute\", \"insert_one\", attribute_doc\n                )\n\n        return self\n\n\nclass Session(BaseSession):\n    def create(self, uuid: str, entity_id: int, process_id: int):\n        # Check if session already exists\n        existing = self.conn.execute(\"memori_session\", \"find_one\", {\"uuid\": str(uuid)})\n\n        if existing:\n            return existing.get(\"_id\")\n\n        # Create new session\n        session_doc = {\n            \"uuid\": str(uuid),\n            \"entity_id\": entity_id,\n            \"process_id\": process_id,\n            \"date_created\": datetime.now(timezone.utc),\n            \"date_updated\": None,\n        }\n\n        result = self.conn.execute(\"memori_session\", \"insert_one\", session_doc)\n\n        return result.inserted_id\n\n    def read(self, uuid: str):\n        existing = self.conn.execute(\"memori_session\", \"find_one\", {\"uuid\": str(uuid)})\n        if not existing:\n            return None\n        return existing.get(\"_id\")\n\n\nclass Schema(BaseSchema):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.version = SchemaVersion(conn)\n\n\nclass SchemaVersion(BaseSchemaVersion):\n    def create(self, num: int):\n        schema_doc = {\"num\": num}\n\n        self.conn.execute(\"memori_schema_version\", \"insert_one\", schema_doc)\n\n    def delete(self):\n        self.conn.execute(\"memori_schema_version\", \"delete_many\", {})\n\n    def read(self):\n        result = self.conn.execute(\n            \"memori_schema_version\", \"find_one\", {}, {\"num\": 1, \"_id\": 0}\n        )\n\n        if not result:\n            return None\n\n        return result.get(\"num\")\n\n\n@Registry.register_driver(\"mongodb\")\nclass Driver:\n    \"\"\"MongoDB storage driver.\n\n    Attributes:\n        migrations: Database schema migrations for MongoDB.\n        requires_rollback_on_error: MongoDB does not abort transactions on query\n            errors by default, so no rollback is needed to continue executing queries.\n    \"\"\"\n\n    migrations = migrations\n    requires_rollback_on_error = False\n\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conversation = Conversation(conn)\n        self.entity = Entity(conn)\n        self.entity_fact = EntityFact(conn)\n        self.knowledge_graph = KnowledgeGraph(conn)\n        self.process = Process(conn)\n        self.process_attribute = ProcessAttribute(conn)\n        self.schema = Schema(conn)\n        self.session = Session(conn)\n"
  },
  {
    "path": "memori/storage/drivers/mysql/__init__.py",
    "content": "from memori.storage.drivers.mysql._driver import Driver\n\n__all__ = [\"Driver\"]\n"
  },
  {
    "path": "memori/storage/drivers/mysql/_driver.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom uuid import uuid4\n\nfrom memori._utils import generate_uniq\nfrom memori.storage._base import (\n    BaseConversation,\n    BaseConversationMessage,\n    BaseConversationMessages,\n    BaseEntity,\n    BaseEntityFact,\n    BaseKnowledgeGraph,\n    BaseProcess,\n    BaseProcessAttribute,\n    BaseSchema,\n    BaseSchemaVersion,\n    BaseSession,\n    BaseStorageAdapter,\n)\nfrom memori.storage._registry import Registry\nfrom memori.storage.migrations._mysql import migrations\n\n\nclass Conversation(BaseConversation):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.message = ConversationMessage(conn)\n        self.messages = ConversationMessages(conn)\n\n    def create(self, session_id, timeout_minutes: int):\n        existing = (\n            self.conn.execute(\n                \"\"\"\n                SELECT c.id,\n                       COALESCE(MAX(m.date_created), c.date_created) as last_activity\n                  FROM memori_conversation c\n                  LEFT JOIN memori_conversation_message m ON m.conversation_id = c.id\n                 WHERE c.session_id = %s\n                 GROUP BY c.id, c.date_created\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n\n        if existing:\n            result = self.conn.execute(\n                \"\"\"\n                SELECT TIMESTAMPDIFF(MINUTE, %s, CURRENT_TIMESTAMP) as minutes_since_activity\n                \"\"\",\n                (existing[\"last_activity\"],),\n            ).fetchone()\n\n            if result[0] <= timeout_minutes:\n                return existing[\"id\"]\n\n        uuid = uuid4()\n        self.conn.execute(\n            \"\"\"\n            INSERT IGNORE INTO memori_conversation(\n                uuid,\n                session_id\n            ) VALUES (\n                %s,\n                %s\n            )\n            \"\"\",\n            (uuid, session_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_conversation\n                 WHERE session_id = %s\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n    def update(self, id: int, summary: str):\n        if summary is None:\n            return self\n\n        self.conn.execute(\n            \"\"\"\n            UPDATE memori_conversation\n               SET summary = %s\n             WHERE id = %s\n            \"\"\",\n            (\n                summary,\n                id,\n            ),\n        )\n        self.conn.commit()\n\n        return self\n\n    def read(self, id: int) -> dict | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id, uuid, session_id, summary, date_created, date_updated\n                  FROM memori_conversation\n                 WHERE id = %s\n                \"\"\",\n                (id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n\n        if result is None:\n            return None\n\n        return dict(result)\n\n    def read_id_by_session_id(self, session_id) -> int | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_conversation\n                 WHERE session_id = %s\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n        if result is None:\n            return None\n        return result.get(\"id\", None)\n\n\nclass ConversationMessage(BaseConversationMessage):\n    def create(self, conversation_id: int, role: str, type: str, content: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_conversation_message(\n                uuid,\n                conversation_id,\n                role,\n                type,\n                content\n            ) VALUES (\n                %s,\n                %s,\n                %s,\n                %s,\n                %s\n            )\n            \"\"\",\n            (\n                uuid4(),\n                conversation_id,\n                role,\n                type,\n                content,\n            ),\n        )\n\n\nclass ConversationMessages(BaseConversationMessages):\n    def read(self, conversation_id: int):\n        results = (\n            self.conn.execute(\n                \"\"\"\n                SELECT role,\n                       content\n                  FROM memori_conversation_message\n                 WHERE conversation_id = %s\n                \"\"\",\n                (conversation_id,),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n        messages = []\n        for result in results:\n            messages.append({\"content\": result[\"content\"], \"role\": result[\"role\"]})\n\n        return messages\n\n\nclass KnowledgeGraph(BaseKnowledgeGraph):\n    def create(self, entity_id: int, semantic_triples: list):\n        if semantic_triples is None or len(semantic_triples) == 0:\n            return self\n\n        for semantic_triple in semantic_triples:\n            uniq = generate_uniq(\n                [semantic_triple.subject_name, semantic_triple.subject_type]\n            )\n\n            self.conn.execute(\n                \"\"\"\n                INSERT IGNORE INTO memori_subject(\n                    uuid,\n                    name,\n                    type,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    %s\n                )\n                \"\"\",\n                (\n                    uuid4(),\n                    semantic_triple.subject_name,\n                    semantic_triple.subject_type,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            subject_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_subject\n                     WHERE uniq = %s\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            uniq = generate_uniq([semantic_triple.predicate])\n\n            self.conn.execute(\n                \"\"\"\n                INSERT IGNORE INTO memori_predicate(\n                    uuid,\n                    content,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s\n                )\n                \"\"\",\n                (\n                    uuid4(),\n                    semantic_triple.predicate,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            predicate_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_predicate\n                     WHERE uniq = %s\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            uniq = generate_uniq(\n                [semantic_triple.object_name, semantic_triple.object_type]\n            )\n\n            self.conn.execute(\n                \"\"\"\n                INSERT IGNORE INTO memori_object(\n                    uuid,\n                    name,\n                    type,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    %s\n                )\n                \"\"\",\n                (\n                    uuid4(),\n                    semantic_triple.object_name,\n                    semantic_triple.object_type,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            object_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_object\n                     WHERE uniq = %s\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            if (\n                entity_id is not None\n                and subject_id is not None\n                and predicate_id is not None\n                and object_id is not None\n            ):\n                self.conn.execute(\n                    \"\"\"\n                    INSERT INTO memori_knowledge_graph(\n                        uuid,\n                        entity_id,\n                        subject_id,\n                        predicate_id,\n                        object_id,\n                        num_times,\n                        date_last_time\n                    ) VALUES (\n                        %s,\n                        %s,\n                        %s,\n                        %s,\n                        %s,\n                        1,\n                        current_timestamp()\n                    )\n                    ON DUPLICATE KEY UPDATE\n                        num_times = num_times + 1,\n                        date_last_time = current_timestamp()\n                    \"\"\",\n                    (uuid4(), entity_id, subject_id, predicate_id, object_id),\n                )\n                self.conn.commit()\n\n        return self\n\n\nclass Entity(BaseEntity):\n    def create(self, external_id: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT IGNORE INTO memori_entity(\n                uuid,\n                external_id\n            ) VALUES (\n                %s,\n                %s\n            )\n            \"\"\",\n            (uuid4(), external_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_entity\n                 WHERE external_id = %s\n                \"\"\",\n                (external_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n\nclass EntityFact(BaseEntityFact):\n    def create(self, entity_id: int, facts: list, fact_embeddings: list | None = None):\n        if facts is None or len(facts) == 0:\n            return self\n\n        from memori.embeddings import format_embedding_for_db\n\n        for i, fact in enumerate(facts):\n            embedding = (\n                fact_embeddings[i]\n                if fact_embeddings and i < len(fact_embeddings)\n                else []\n            )\n            embedding_formatted = format_embedding_for_db(embedding, \"mysql\")\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_entity_fact(\n                    uuid,\n                    entity_id,\n                    content,\n                    content_embedding,\n                    num_times,\n                    date_last_time,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    %s,\n                    %s,\n                    current_timestamp(),\n                    %s\n                )\n                ON DUPLICATE KEY UPDATE\n                    num_times = num_times + 1,\n                    date_last_time = current_timestamp()\n                \"\"\",\n                (\n                    uuid4(),\n                    entity_id,\n                    fact,\n                    embedding_formatted,\n                    1,\n                    generate_uniq(fact),\n                ),\n            )\n\n        self.conn.commit()\n\n        return self\n\n    def get_embeddings(self, entity_id: int, limit: int = 1000):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id,\n                       content_embedding\n                  FROM memori_entity_fact\n                 WHERE entity_id = %s\n                 ORDER BY date_last_time DESC,\n                          num_times DESC,\n                          id DESC\n                 LIMIT %s\n                \"\"\",\n                (entity_id, limit),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n    def get_facts_by_ids(self, fact_ids: list[int]):\n        if not fact_ids:\n            return []\n        placeholders = \",\".join([\"%s\"] * len(fact_ids))\n\n        query = f\"\"\"\n                SELECT id,\n                       content,\n                       date_created\n                  FROM memori_entity_fact\n                 WHERE id IN ({placeholders})\n                \"\"\"  # nosec B608: Safe - only interpolating placeholder count, actual values parameterized\n        return self.conn.execute(query, tuple(fact_ids)).mappings().fetchall()\n\n\nclass Process(BaseProcess):\n    def create(self, external_id: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT IGNORE INTO memori_process(\n                uuid,\n                external_id\n            ) VALUES (\n                %s,\n                %s\n            )\n            \"\"\",\n            (\n                uuid4(),\n                external_id,\n            ),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_process\n                 WHERE external_id = %s\n                \"\"\",\n                (external_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n\nclass ProcessAttribute(BaseProcessAttribute):\n    def create(self, process_id: int, attributes: list):\n        if attributes is None or len(attributes) == 0:\n            return self\n\n        for attribute in attributes:\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_process_attribute(\n                    uuid,\n                    process_id,\n                    content,\n                    num_times,\n                    date_last_time,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    %s,\n                    current_timestamp(),\n                    %s\n                )\n                ON DUPLICATE KEY UPDATE\n                    num_times = num_times + 1,\n                    date_last_time = current_timestamp()\n                \"\"\",\n                (\n                    uuid4(),\n                    process_id,\n                    attribute,\n                    1,\n                    generate_uniq(attribute),\n                ),\n            )\n\n        self.conn.commit()\n\n        return self\n\n\nclass Session(BaseSession):\n    def create(self, uuid: str, entity_id: int, process_id: int):\n        self.conn.execute(\n            \"\"\"\n            INSERT IGNORE INTO memori_session(\n                uuid,\n                entity_id,\n                process_id\n            ) VALUES (\n                %s,\n                %s,\n                %s\n            )\n            \"\"\",\n            (\n                uuid,\n                entity_id,\n                process_id,\n            ),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_session\n                 WHERE uuid = %s\n                \"\"\",\n                (uuid,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n    def read(self, uuid: str) -> int | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_session\n                 WHERE uuid = %s\n                \"\"\",\n                (str(uuid),),\n            )\n            .mappings()\n            .fetchone()\n        )\n        if result is None:\n            return None\n        return result.get(\"id\", None)\n\n\nclass Schema(BaseSchema):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.version = SchemaVersion(conn)\n\n\nclass SchemaVersion(BaseSchemaVersion):\n    def create(self, num: int):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_schema_version(\n                num\n            ) VALUES (\n                %s\n            )\n            \"\"\",\n            (num,),\n        )\n\n    def delete(self):\n        self.conn.execute(\n            \"\"\"\n            DELETE FROM memori_schema_version\n            \"\"\"\n        )\n\n    def read(self):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT num\n                  FROM memori_schema_version\n                \"\"\"\n            )\n            .mappings()\n            .fetchone()\n            .get(\"num\", None)\n        )\n\n\n@Registry.register_driver(\"mysql\")\n@Registry.register_driver(\"mariadb\")\nclass Driver:\n    \"\"\"MySQL storage driver (also supports MariaDB).\n\n    Attributes:\n        migrations: Database schema migrations for MySQL.\n        requires_rollback_on_error: MySQL does not abort transactions on query\n            errors, so no rollback is needed to continue executing queries.\n    \"\"\"\n\n    migrations = migrations\n    requires_rollback_on_error = False\n\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conversation = Conversation(conn)\n        self.entity = Entity(conn)\n        self.entity_fact = EntityFact(conn)\n        self.knowledge_graph = KnowledgeGraph(conn)\n        self.process = Process(conn)\n        self.process_attribute = ProcessAttribute(conn)\n        self.schema = Schema(conn)\n        self.session = Session(conn)\n"
  },
  {
    "path": "memori/storage/drivers/oceanbase/__init__.py",
    "content": "from memori.storage.drivers.oceanbase._driver import Driver\n\n__all__ = [\"Driver\"]\n"
  },
  {
    "path": "memori/storage/drivers/oceanbase/_driver.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom uuid import uuid4\n\nfrom memori._utils import generate_uniq\nfrom memori.storage._registry import Registry\nfrom memori.storage.drivers.mysql._driver import Driver as MysqlDriver\nfrom memori.storage.drivers.mysql._driver import EntityFact as MysqlEntityFact\nfrom memori.storage.migrations._oceanbase import migrations\n\n\nclass EntityFact(MysqlEntityFact):\n    def create(self, entity_id: int, facts: list, fact_embeddings: list | None = None):\n        if facts is None or len(facts) == 0:\n            return self\n\n        from memori.embeddings import format_embedding_for_db\n\n        dialect = self.conn.get_dialect()\n\n        for i, fact in enumerate(facts):\n            embedding = (\n                fact_embeddings[i]\n                if fact_embeddings and i < len(fact_embeddings)\n                else []\n            )\n            embedding_formatted = format_embedding_for_db(embedding, dialect)\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_entity_fact(\n                    uuid,\n                    entity_id,\n                    content,\n                    content_embedding,\n                    num_times,\n                    date_last_time,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    %s,\n                    %s,\n                    current_timestamp(),\n                    %s\n                )\n                ON DUPLICATE KEY UPDATE\n                    num_times = num_times + 1,\n                    date_last_time = current_timestamp()\n                \"\"\",\n                (\n                    uuid4(),\n                    entity_id,\n                    fact,\n                    embedding_formatted,\n                    1,\n                    generate_uniq(fact),\n                ),\n            )\n\n        self.conn.commit()\n\n        return self\n\n\n@Registry.register_driver(\"oceanbase\")\nclass Driver(MysqlDriver):\n    \"\"\"OceanBase storage driver (MySQL-compatible).\"\"\"\n\n    migrations = migrations\n    requires_rollback_on_error = True\n\n    def __init__(self, conn):\n        super().__init__(conn)\n        self.entity_fact = EntityFact(conn)\n"
  },
  {
    "path": "memori/storage/drivers/oracle/__init__.py",
    "content": "from memori.storage.drivers.oracle._driver import Driver\n\n__all__ = [\"Driver\"]\n"
  },
  {
    "path": "memori/storage/drivers/oracle/_driver.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom uuid import uuid4\n\nfrom memori.storage._base import (\n    BaseConversation,\n    BaseConversationMessage,\n    BaseConversationMessages,\n    BaseEntity,\n    BaseEntityFact,\n    BaseKnowledgeGraph,\n    BaseProcess,\n    BaseProcessAttribute,\n    BaseSchema,\n    BaseSchemaVersion,\n    BaseSession,\n    BaseStorageAdapter,\n)\nfrom memori.storage._registry import Registry\nfrom memori.storage.migrations._oracle import migrations\n\n\nclass Conversation(BaseConversation):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.message = ConversationMessage(conn)\n        self.messages = ConversationMessages(conn)\n\n    def create(self, session_id, timeout_minutes: int):\n        existing = (\n            self.conn.execute(\n                \"\"\"\n                SELECT c.id,\n                       COALESCE(MAX(m.date_created), c.date_created) as last_activity\n                  FROM memori_conversation c\n                  LEFT JOIN memori_conversation_message m ON m.conversation_id = c.id\n                 WHERE c.session_id = :1\n                 GROUP BY c.id, c.date_created\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n\n        if existing:\n            result = self.conn.execute(\n                \"\"\"\n                SELECT ROUND((CAST(SYSTIMESTAMP AS DATE) - CAST(:1 AS DATE)) * 24 * 60) as minutes_since_activity\n                  FROM DUAL\n                \"\"\",\n                (existing[\"last_activity\"],),\n            ).fetchone()\n\n            if result and result[0] is not None and result[0] <= timeout_minutes:\n                return existing[\"id\"]\n\n        uuid = str(uuid4())\n        self.conn.execute(\n            \"\"\"\n            MERGE INTO memori_conversation dst\n            USING (SELECT :1 AS uuid, :2 AS session_id FROM DUAL) src\n            ON (dst.session_id = src.session_id)\n            WHEN NOT MATCHED THEN\n                INSERT (uuid, session_id)\n                VALUES (src.uuid, src.session_id)\n            \"\"\",\n            (\n                uuid,\n                session_id,\n            ),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_conversation\n                 WHERE session_id = :1\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n    def update(self, id: int, summary: str):\n        if summary is None:\n            return self\n\n        self.conn.execute(\n            \"\"\"\n            UPDATE memori_conversation\n               SET summary = :1\n             WHERE id = :2\n            \"\"\",\n            (\n                summary,\n                id,\n            ),\n        )\n        self.conn.commit()\n\n        return self\n\n    def read(self, id: int) -> dict | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id, uuid, session_id, summary, date_created, date_updated\n                  FROM memori_conversation\n                 WHERE id = :1\n                \"\"\",\n                (id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n\n        if result is None:\n            return None\n\n        return dict(result)\n\n    def read_id_by_session_id(self, session_id) -> int | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_conversation\n                 WHERE session_id = :1\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n        if result is None:\n            return None\n        return result.get(\"id\", None)\n\n\nclass ConversationMessage(BaseConversationMessage):\n    def create(self, conversation_id: int, role: str, type: str, content: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_conversation_message(\n                uuid,\n                conversation_id,\n                role,\n                type,\n                content\n            ) VALUES (\n                :1,\n                :2,\n                :3,\n                :4,\n                :5\n            )\n            \"\"\",\n            (\n                str(uuid4()),\n                conversation_id,\n                role,\n                type,\n                content,\n            ),\n        )\n\n\nclass ConversationMessages(BaseConversationMessages):\n    def read(self, conversation_id: int):\n        results = (\n            self.conn.execute(\n                \"\"\"\n                SELECT role,\n                       content\n                  FROM memori_conversation_message\n                 WHERE conversation_id = :1\n                 ORDER BY id\n                \"\"\",\n                (conversation_id,),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n        messages = []\n        for result in results:\n            messages.append({\"content\": result[\"content\"], \"role\": result[\"role\"]})\n\n        return messages\n\n\nclass Entity(BaseEntity):\n    def create(self, external_id: str):\n        self.conn.execute(\n            \"\"\"\n            MERGE INTO memori_entity dst\n            USING (SELECT :1 AS uuid, :2 AS external_id FROM DUAL) src\n            ON (dst.external_id = src.external_id)\n            WHEN NOT MATCHED THEN\n                INSERT (uuid, external_id)\n                VALUES (src.uuid, src.external_id)\n            \"\"\",\n            (str(uuid4()), external_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_entity\n                 WHERE external_id = :1\n                \"\"\",\n                (external_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n\nclass EntityFact(BaseEntityFact):\n    def create(self, entity_id: int, facts: list, fact_embeddings: list | None = None):\n        if facts is None or len(facts) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n        from memori.embeddings import format_embedding_for_db\n\n        dialect = self.conn.get_dialect()\n\n        for i, fact in enumerate(facts):\n            embedding = (\n                fact_embeddings[i]\n                if fact_embeddings and i < len(fact_embeddings)\n                else []\n            )\n            embedding_formatted = format_embedding_for_db(embedding, dialect)\n            uniq = generate_uniq([fact])\n\n            self.conn.execute(\n                \"\"\"\n                MERGE INTO memori_entity_fact dst\n                USING (SELECT :1 AS uuid, :2 AS entity_id, :3 AS content,\n                              :4 AS content_embedding, :5 AS uniq FROM DUAL) src\n                ON (dst.entity_id = src.entity_id AND dst.uniq = src.uniq)\n                WHEN MATCHED THEN\n                    UPDATE SET num_times = dst.num_times + 1,\n                               date_last_time = SYSTIMESTAMP\n                WHEN NOT MATCHED THEN\n                    INSERT (uuid, entity_id, content, content_embedding,\n                            num_times, date_last_time, uniq)\n                    VALUES (src.uuid, src.entity_id, src.content, src.content_embedding,\n                            1, SYSTIMESTAMP, src.uniq)\n                \"\"\",\n                (\n                    str(uuid4()),\n                    entity_id,\n                    fact,\n                    embedding_formatted,\n                    uniq,\n                ),\n            )\n\n        self.conn.commit()\n        return self\n\n    def get_embeddings(self, entity_id: int, limit: int = 1000):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id,\n                       content_embedding\n                  FROM (\n                    SELECT id,\n                           content_embedding\n                      FROM memori_entity_fact\n                     WHERE entity_id = :1\n                     ORDER BY date_last_time DESC,\n                              num_times DESC,\n                              id DESC\n                  )\n                 WHERE ROWNUM <= :2\n                \"\"\",\n                (entity_id, limit),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n    def get_facts_by_ids(self, fact_ids: list[int]):\n        if not fact_ids:\n            return []\n\n        # Oracle doesn't support ANY, so we need to use IN with placeholders\n        placeholders = \",\".join([f\":{i + 1}\" for i in range(len(fact_ids))])\n        query = f\"\"\"\n            SELECT id,\n                   content,\n                   date_created\n              FROM memori_entity_fact\n             WHERE id IN ({placeholders})\n        \"\"\"\n\n        return self.conn.execute(query, tuple(fact_ids)).mappings().fetchall()\n\n\nclass KnowledgeGraph(BaseKnowledgeGraph):\n    def create(self, entity_id: int, semantic_triples: list):\n        if semantic_triples is None or len(semantic_triples) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n\n        for semantic_triple in semantic_triples:\n            uniq = generate_uniq(\n                [semantic_triple.subject_name, semantic_triple.subject_type]\n            )\n\n            self.conn.execute(\n                \"\"\"\n                MERGE INTO memori_subject dst\n                USING (SELECT :1 AS uuid, :2 AS name, :3 AS type, :4 AS uniq FROM DUAL) src\n                ON (dst.uniq = src.uniq)\n                WHEN NOT MATCHED THEN\n                    INSERT (uuid, name, type, uniq)\n                    VALUES (src.uuid, src.name, src.type, src.uniq)\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.subject_name,\n                    semantic_triple.subject_type,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            subject_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_subject\n                     WHERE uniq = :1\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            uniq = generate_uniq([semantic_triple.predicate])\n\n            self.conn.execute(\n                \"\"\"\n                MERGE INTO memori_predicate dst\n                USING (SELECT :1 AS uuid, :2 AS content, :3 AS uniq FROM DUAL) src\n                ON (dst.uniq = src.uniq)\n                WHEN NOT MATCHED THEN\n                    INSERT (uuid, content, uniq)\n                    VALUES (src.uuid, src.content, src.uniq)\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.predicate,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            predicate_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_predicate\n                     WHERE uniq = :1\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            uniq = generate_uniq(\n                [semantic_triple.object_name, semantic_triple.object_type]\n            )\n\n            self.conn.execute(\n                \"\"\"\n                MERGE INTO memori_object dst\n                USING (SELECT :1 AS uuid, :2 AS name, :3 AS type, :4 AS uniq FROM DUAL) src\n                ON (dst.uniq = src.uniq)\n                WHEN NOT MATCHED THEN\n                    INSERT (uuid, name, type, uniq)\n                    VALUES (src.uuid, src.name, src.type, src.uniq)\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.object_name,\n                    semantic_triple.object_type,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            object_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_object\n                     WHERE uniq = :1\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            if (\n                entity_id is not None\n                and subject_id is not None\n                and predicate_id is not None\n                and object_id is not None\n            ):\n                self.conn.execute(\n                    \"\"\"\n                    MERGE INTO memori_knowledge_graph dst\n                    USING (SELECT :1 AS uuid, :2 AS entity_id, :3 AS subject_id,\n                                  :4 AS predicate_id, :5 AS object_id FROM DUAL) src\n                    ON (dst.entity_id = src.entity_id AND dst.subject_id = src.subject_id\n                        AND dst.predicate_id = src.predicate_id AND dst.object_id = src.object_id)\n                    WHEN MATCHED THEN\n                        UPDATE SET num_times = dst.num_times + 1,\n                                   date_last_time = SYSTIMESTAMP\n                    WHEN NOT MATCHED THEN\n                        INSERT (uuid, entity_id, subject_id, predicate_id, object_id,\n                                num_times, date_last_time)\n                        VALUES (src.uuid, src.entity_id, src.subject_id, src.predicate_id,\n                                src.object_id, 1, SYSTIMESTAMP)\n                    \"\"\",\n                    (str(uuid4()), entity_id, subject_id, predicate_id, object_id),\n                )\n                self.conn.commit()\n\n        return self\n\n\nclass Process(BaseProcess):\n    def create(self, external_id: str):\n        self.conn.execute(\n            \"\"\"\n            MERGE INTO memori_process dst\n            USING (SELECT :1 AS uuid, :2 AS external_id FROM DUAL) src\n            ON (dst.external_id = src.external_id)\n            WHEN NOT MATCHED THEN\n                INSERT (uuid, external_id)\n                VALUES (src.uuid, src.external_id)\n            \"\"\",\n            (str(uuid4()), external_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_process\n                 WHERE external_id = :1\n                \"\"\",\n                (external_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n\nclass ProcessAttribute(BaseProcessAttribute):\n    def create(self, process_id: int, attributes: list):\n        if attributes is None or len(attributes) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n\n        for attribute in attributes:\n            uniq = generate_uniq([attribute])\n\n            self.conn.execute(\n                \"\"\"\n                MERGE INTO memori_process_attribute dst\n                USING (SELECT :1 AS uuid, :2 AS process_id, :3 AS content, :4 AS uniq FROM DUAL) src\n                ON (dst.process_id = src.process_id AND dst.uniq = src.uniq)\n                WHEN MATCHED THEN\n                    UPDATE SET num_times = dst.num_times + 1,\n                               date_last_time = SYSTIMESTAMP\n                WHEN NOT MATCHED THEN\n                    INSERT (uuid, process_id, content, num_times, date_last_time, uniq)\n                    VALUES (src.uuid, src.process_id, src.content, 1, SYSTIMESTAMP, src.uniq)\n                \"\"\",\n                (\n                    str(uuid4()),\n                    process_id,\n                    attribute,\n                    uniq,\n                ),\n            )\n\n        self.conn.commit()\n        return self\n\n\nclass Session(BaseSession):\n    def create(self, uuid: str, entity_id: int, process_id: int):\n        self.conn.execute(\n            \"\"\"\n            MERGE INTO memori_session dst\n            USING (SELECT :1 AS uuid, :2 AS entity_id, :3 AS process_id FROM DUAL) src\n            ON (dst.uuid = src.uuid)\n            WHEN NOT MATCHED THEN\n                INSERT (uuid, entity_id, process_id)\n                VALUES (src.uuid, src.entity_id, src.process_id)\n            \"\"\",\n            (str(uuid), entity_id, process_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_session\n                 WHERE uuid = :1\n                \"\"\",\n                (str(uuid),),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n    def read(self, uuid: str) -> int | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_session\n                 WHERE uuid = :1\n                \"\"\",\n                (str(uuid),),\n            )\n            .mappings()\n            .fetchone()\n        )\n        if result is None:\n            return None\n        return result.get(\"id\", None)\n\n\nclass Schema(BaseSchema):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.version = SchemaVersion(conn)\n\n\nclass SchemaVersion(BaseSchemaVersion):\n    def create(self, num: int):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_schema_version(\n                num\n            ) VALUES (\n                :1\n            )\n            \"\"\",\n            (num,),\n        )\n\n    def delete(self):\n        self.conn.execute(\n            \"\"\"\n            DELETE FROM memori_schema_version\n            \"\"\"\n        )\n\n    def read(self):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT num\n                  FROM memori_schema_version\n                \"\"\"\n            )\n            .mappings()\n            .fetchone()\n            .get(\"num\", None)\n        )\n\n\n@Registry.register_driver(\"oracle\")\nclass Driver:\n    \"\"\"Oracle storage driver.\n\n    Attributes:\n        migrations: Database schema migrations for Oracle.\n        requires_rollback_on_error: Oracle aborts transactions when a query\n            fails and requires an explicit ROLLBACK before executing new queries.\n    \"\"\"\n\n    migrations = migrations\n    requires_rollback_on_error = True\n\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conversation = Conversation(conn)\n        self.entity = Entity(conn)\n        self.entity_fact = EntityFact(conn)\n        self.knowledge_graph = KnowledgeGraph(conn)\n        self.process = Process(conn)\n        self.process_attribute = ProcessAttribute(conn)\n        self.schema = Schema(conn)\n        self.session = Session(conn)\n"
  },
  {
    "path": "memori/storage/drivers/postgresql/__init__.py",
    "content": "from memori.storage.drivers.postgresql._driver import Driver\n\n__all__ = [\"Driver\"]\n"
  },
  {
    "path": "memori/storage/drivers/postgresql/_driver.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom uuid import uuid4\n\nfrom memori.storage._base import (\n    BaseConversation,\n    BaseConversationMessage,\n    BaseConversationMessages,\n    BaseEntity,\n    BaseEntityFact,\n    BaseKnowledgeGraph,\n    BaseProcess,\n    BaseProcessAttribute,\n    BaseSchema,\n    BaseSchemaVersion,\n    BaseSession,\n    BaseStorageAdapter,\n)\nfrom memori.storage._registry import Registry\nfrom memori.storage.migrations._postgresql import migrations\n\n\nclass Conversation(BaseConversation):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.message = ConversationMessage(conn)\n        self.messages = ConversationMessages(conn)\n\n    def create(self, session_id, timeout_minutes: int):\n        existing = (\n            self.conn.execute(\n                \"\"\"\n                SELECT c.id,\n                       COALESCE(MAX(m.date_created), c.date_created) as last_activity\n                  FROM memori_conversation c\n                  LEFT JOIN memori_conversation_message m ON m.conversation_id = c.id\n                 WHERE c.session_id = %s\n                 GROUP BY c.id, c.date_created\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n\n        if existing:\n            result = self.conn.execute(\n                \"\"\"\n                SELECT EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - %s::timestamp)) / 60 as minutes_since_activity\n                \"\"\",\n                (existing[\"last_activity\"],),\n            ).fetchone()\n\n            if result[0] <= timeout_minutes:\n                return existing[\"id\"]\n\n        uuid = str(uuid4())\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_conversation(\n                uuid,\n                session_id\n            ) VALUES (\n                %s,\n                %s\n            )\n            ON CONFLICT DO NOTHING\n            \"\"\",\n            (uuid, session_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_conversation\n                 WHERE session_id = %s\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n    def update(self, id: int, summary: str):\n        if summary is None:\n            return self\n\n        self.conn.execute(\n            \"\"\"\n            UPDATE memori_conversation\n               SET summary = %s\n             WHERE id = %s\n            \"\"\",\n            (\n                summary,\n                id,\n            ),\n        )\n        self.conn.commit()\n\n        return self\n\n    def read(self, id: int) -> dict | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id, uuid, session_id, summary, date_created, date_updated\n                  FROM memori_conversation\n                 WHERE id = %s\n                \"\"\",\n                (id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n\n        if result is None:\n            return None\n\n        return dict(result)\n\n    def read_id_by_session_id(self, session_id) -> int | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_conversation\n                 WHERE session_id = %s\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n        if result is None:\n            return None\n        return result.get(\"id\", None)\n\n\nclass ConversationMessage(BaseConversationMessage):\n    def create(self, conversation_id: int, role: str, type: str, content: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_conversation_message(\n                uuid,\n                conversation_id,\n                role,\n                type,\n                content\n            ) VALUES (\n                %s,\n                %s,\n                %s,\n                %s,\n                %s\n            )\n            \"\"\",\n            (\n                str(uuid4()),\n                conversation_id,\n                role,\n                type,\n                content,\n            ),\n        )\n\n\nclass ConversationMessages(BaseConversationMessages):\n    def read(self, conversation_id: int):\n        results = (\n            self.conn.execute(\n                \"\"\"\n                SELECT role,\n                       content\n                  FROM memori_conversation_message\n                 WHERE conversation_id = %s\n                \"\"\",\n                (conversation_id,),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n        messages = []\n        for result in results:\n            messages.append({\"content\": result[\"content\"], \"role\": result[\"role\"]})\n\n        return messages\n\n\nclass Entity(BaseEntity):\n    def create(self, external_id: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_entity(\n                uuid,\n                external_id\n            ) VALUES (\n                %s,\n                %s\n            )\n            ON CONFLICT DO NOTHING\n            \"\"\",\n            (str(uuid4()), external_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_entity\n                 WHERE external_id = %s\n                \"\"\",\n                (external_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n\nclass EntityFact(BaseEntityFact):\n    def create(self, entity_id: int, facts: list, fact_embeddings: list | None = None):\n        if facts is None or len(facts) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n        from memori.embeddings import format_embedding_for_db\n\n        dialect = self.conn.get_dialect()\n\n        for i, fact in enumerate(facts):\n            embedding = (\n                fact_embeddings[i]\n                if fact_embeddings and i < len(fact_embeddings)\n                else []\n            )\n            embedding_formatted = format_embedding_for_db(embedding, dialect)\n            uniq = generate_uniq([fact])\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_entity_fact(\n                    uuid,\n                    entity_id,\n                    content,\n                    content_embedding,\n                    num_times,\n                    date_last_time,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    %s,\n                    1,\n                    CURRENT_TIMESTAMP,\n                    %s\n                )\n                ON CONFLICT (entity_id, uniq) DO UPDATE SET\n                    num_times = memori_entity_fact.num_times + 1,\n                    date_last_time = CURRENT_TIMESTAMP\n                \"\"\",\n                (\n                    str(uuid4()),\n                    entity_id,\n                    fact,\n                    embedding_formatted,\n                    uniq,\n                ),\n            )\n\n        return self\n\n    def get_embeddings(self, entity_id: int, limit: int = 1000):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id,\n                       content_embedding\n                  FROM memori_entity_fact\n                 WHERE entity_id = %s\n                 ORDER BY date_last_time DESC,\n                          num_times DESC,\n                          id DESC\n                 LIMIT %s\n                \"\"\",\n                (entity_id, limit),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n    def get_facts_by_ids(self, fact_ids: list[int]):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id,\n                       content,\n                       date_created\n                  FROM memori_entity_fact\n                 WHERE id = ANY(%s)\n                \"\"\",\n                (fact_ids,),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n\nclass KnowledgeGraph(BaseKnowledgeGraph):\n    def create(self, entity_id: int, semantic_triples: list):\n        if semantic_triples is None or len(semantic_triples) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n\n        for semantic_triple in semantic_triples:\n            uniq = generate_uniq(\n                [semantic_triple.subject_name, semantic_triple.subject_type]\n            )\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_subject(\n                    uuid,\n                    name,\n                    type,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    %s\n                )\n                ON CONFLICT DO NOTHING\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.subject_name,\n                    semantic_triple.subject_type,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            subject_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_subject\n                     WHERE uniq = %s\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            uniq = generate_uniq([semantic_triple.predicate])\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_predicate(\n                    uuid,\n                    content,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s\n                )\n                ON CONFLICT DO NOTHING\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.predicate,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            predicate_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_predicate\n                     WHERE uniq = %s\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            uniq = generate_uniq(\n                [semantic_triple.object_name, semantic_triple.object_type]\n            )\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_object(\n                    uuid,\n                    name,\n                    type,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    %s\n                )\n                ON CONFLICT DO NOTHING\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.object_name,\n                    semantic_triple.object_type,\n                    uniq,\n                ),\n            )\n            self.conn.commit()\n\n            object_id = (\n                self.conn.execute(\n                    \"\"\"\n                    SELECT id\n                      FROM memori_object\n                     WHERE uniq = %s\n                    \"\"\",\n                    (uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            if (\n                entity_id is not None\n                and subject_id is not None\n                and predicate_id is not None\n                and object_id is not None\n            ):\n                self.conn.execute(\n                    \"\"\"\n                    INSERT INTO memori_knowledge_graph(\n                        uuid,\n                        entity_id,\n                        subject_id,\n                        predicate_id,\n                        object_id,\n                        num_times,\n                        date_last_time\n                    ) VALUES (\n                        %s,\n                        %s,\n                        %s,\n                        %s,\n                        %s,\n                        1,\n                        CURRENT_TIMESTAMP\n                    )\n                    ON CONFLICT (entity_id, subject_id, predicate_id, object_id) DO UPDATE SET\n                        num_times = memori_knowledge_graph.num_times + 1,\n                        date_last_time = CURRENT_TIMESTAMP\n                    \"\"\",\n                    (str(uuid4()), entity_id, subject_id, predicate_id, object_id),\n                )\n\n        return self\n\n\nclass Process(BaseProcess):\n    def create(self, external_id: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_process(\n                uuid,\n                external_id\n            ) VALUES (\n                %s,\n                %s\n            )\n            ON CONFLICT DO NOTHING\n            \"\"\",\n            (str(uuid4()), external_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_process\n                 WHERE external_id = %s\n                \"\"\",\n                (external_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n\nclass ProcessAttribute(BaseProcessAttribute):\n    def create(self, process_id: int, attributes: list):\n        if attributes is None or len(attributes) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n\n        for attribute in attributes:\n            uniq = generate_uniq([attribute])\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_process_attribute(\n                    uuid,\n                    process_id,\n                    content,\n                    num_times,\n                    date_last_time,\n                    uniq\n                ) VALUES (\n                    %s,\n                    %s,\n                    %s,\n                    1,\n                    CURRENT_TIMESTAMP,\n                    %s\n                )\n                ON CONFLICT (process_id, uniq) DO UPDATE SET\n                    num_times = memori_process_attribute.num_times + 1,\n                    date_last_time = CURRENT_TIMESTAMP\n                \"\"\",\n                (\n                    str(uuid4()),\n                    process_id,\n                    attribute,\n                    uniq,\n                ),\n            )\n\n        return self\n\n\nclass Session(BaseSession):\n    def create(self, uuid: str, entity_id: int, process_id: int):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_session(\n                uuid,\n                entity_id,\n                process_id\n            ) VALUES (\n                %s,\n                %s,\n                %s\n            )\n            ON CONFLICT DO NOTHING\n            \"\"\",\n            (str(uuid), entity_id, process_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_session\n                 WHERE uuid = %s\n                \"\"\",\n                (str(uuid),),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n    def read(self, uuid: str) -> int | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_session\n                 WHERE uuid = %s\n                \"\"\",\n                (str(uuid),),\n            )\n            .mappings()\n            .fetchone()\n        )\n        if result is None:\n            return None\n        return result.get(\"id\", None)\n\n\nclass Schema(BaseSchema):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.version = SchemaVersion(conn)\n\n\nclass SchemaVersion(BaseSchemaVersion):\n    def create(self, num: int):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_schema_version(\n                num\n            ) VALUES (\n                %s\n            )\n            \"\"\",\n            (num,),\n        )\n\n    def delete(self):\n        self.conn.execute(\n            \"\"\"\n            DELETE FROM memori_schema_version\n            \"\"\"\n        )\n\n    def read(self):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT num\n                  FROM memori_schema_version\n                \"\"\"\n            )\n            .mappings()\n            .fetchone()\n            .get(\"num\", None)\n        )\n\n\n@Registry.register_driver(\"postgresql\")\n@Registry.register_driver(\"cockroachdb\")\nclass Driver:\n    \"\"\"PostgreSQL storage driver (also supports CockroachDB).\n\n    Attributes:\n        migrations: Database schema migrations for PostgreSQL-compatible databases.\n        requires_rollback_on_error: PostgreSQL aborts transactions when a query\n            fails and requires an explicit ROLLBACK before executing new queries.\n    \"\"\"\n\n    migrations = migrations\n    requires_rollback_on_error = True\n\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conversation = Conversation(conn)\n        self.entity = Entity(conn)\n        self.entity_fact = EntityFact(conn)\n        self.knowledge_graph = KnowledgeGraph(conn)\n        self.process = Process(conn)\n        self.process_attribute = ProcessAttribute(conn)\n        self.schema = Schema(conn)\n        self.session = Session(conn)\n"
  },
  {
    "path": "memori/storage/drivers/sqlite/__init__.py",
    "content": "from memori.storage.drivers.sqlite._driver import Driver\n\n__all__ = [\"Driver\"]\n"
  },
  {
    "path": "memori/storage/drivers/sqlite/_driver.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nfrom uuid import uuid4\n\nfrom memori.storage._base import (\n    BaseConversation,\n    BaseConversationMessage,\n    BaseConversationMessages,\n    BaseEntity,\n    BaseEntityFact,\n    BaseKnowledgeGraph,\n    BaseProcess,\n    BaseProcessAttribute,\n    BaseSchema,\n    BaseSchemaVersion,\n    BaseSession,\n    BaseStorageAdapter,\n)\nfrom memori.storage._registry import Registry\nfrom memori.storage.migrations._sqlite import migrations\n\n\nclass Conversation(BaseConversation):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.message = ConversationMessage(conn)\n        self.messages = ConversationMessages(conn)\n\n    def create(self, session_id, timeout_minutes: int):\n        existing = (\n            self.conn.execute(\n                \"\"\"\n                SELECT c.id,\n                       COALESCE(MAX(m.date_created), c.date_created) as last_activity\n                  FROM memori_conversation c\n                  LEFT JOIN memori_conversation_message m ON m.conversation_id = c.id\n                 WHERE c.session_id = ?\n                 GROUP BY c.id, c.date_created\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n\n        if existing:\n            result = self.conn.execute(\n                \"\"\"\n                SELECT (julianday('now') - julianday(?)) * 24 * 60 as minutes_since_activity\n                \"\"\",\n                (existing[\"last_activity\"],),\n            ).fetchone()\n\n            if result[0] <= timeout_minutes:\n                return existing[\"id\"]\n\n        uuid = str(uuid4())\n        self.conn.execute(\n            \"\"\"\n            INSERT OR IGNORE INTO memori_conversation(\n                uuid,\n                session_id\n            ) VALUES (\n                ?,\n                ?\n            )\n            \"\"\",\n            (uuid, session_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_conversation\n                 WHERE session_id = ?\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n    def update(self, id: int, summary: str):\n        if summary is None:\n            return self\n\n        self.conn.execute(\n            \"\"\"\n            UPDATE memori_conversation\n               SET summary = ?\n             WHERE id = ?\n            \"\"\",\n            (\n                summary,\n                id,\n            ),\n        )\n        self.conn.commit()\n\n        return self\n\n    def read(self, id: int) -> dict | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id, uuid, session_id, summary, date_created, date_updated\n                  FROM memori_conversation\n                 WHERE id = ?\n                \"\"\",\n                (id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n\n        if result is None:\n            return None\n\n        return dict(result)\n\n    def read_id_by_session_id(self, session_id) -> int | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_conversation\n                 WHERE session_id = ?\n                \"\"\",\n                (session_id,),\n            )\n            .mappings()\n            .fetchone()\n        )\n        if result is None:\n            return None\n        return result.get(\"id\", None)\n\n\nclass ConversationMessage(BaseConversationMessage):\n    def create(self, conversation_id: int, role: str, type: str, content: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_conversation_message(\n                uuid,\n                conversation_id,\n                role,\n                type,\n                content\n            ) VALUES (\n                ?,\n                ?,\n                ?,\n                ?,\n                ?\n            )\n            \"\"\",\n            (\n                str(uuid4()),\n                conversation_id,\n                role,\n                type,\n                content,\n            ),\n        )\n\n\nclass ConversationMessages(BaseConversationMessages):\n    def read(self, conversation_id: int):\n        results = (\n            self.conn.execute(\n                \"\"\"\n                SELECT role,\n                       content\n                  FROM memori_conversation_message\n                 WHERE conversation_id = ?\n                 ORDER BY id\n                \"\"\",\n                (conversation_id,),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n        messages = []\n        for result in results:\n            messages.append({\"content\": result[\"content\"], \"role\": result[\"role\"]})\n\n        return messages\n\n\nclass Entity(BaseEntity):\n    def create(self, external_id: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT OR IGNORE INTO memori_entity(\n                uuid,\n                external_id\n            ) VALUES (\n                ?,\n                ?\n            )\n            \"\"\",\n            (str(uuid4()), external_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_entity\n                 WHERE external_id = ?\n                \"\"\",\n                (external_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n\nclass EntityFact(BaseEntityFact):\n    def create(self, entity_id: int, facts: list, fact_embeddings: list | None = None):\n        if facts is None or len(facts) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n        from memori.embeddings import format_embedding_for_db\n\n        for i, fact in enumerate(facts):\n            embedding = (\n                fact_embeddings[i]\n                if fact_embeddings and i < len(fact_embeddings)\n                else []\n            )\n            embedding_formatted = format_embedding_for_db(embedding, \"sqlite\")\n            uniq = generate_uniq([fact])\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_entity_fact(\n                    uuid,\n                    entity_id,\n                    content,\n                    content_embedding,\n                    num_times,\n                    date_last_time,\n                    uniq\n                ) VALUES (\n                    ?,\n                    ?,\n                    ?,\n                    ?,\n                    ?,\n                    datetime('now'),\n                    ?\n                )\n                ON CONFLICT(entity_id, uniq) DO UPDATE SET\n                    num_times = num_times + 1,\n                    date_last_time = datetime('now')\n                \"\"\",\n                (\n                    str(uuid4()),\n                    entity_id,\n                    fact,\n                    embedding_formatted,\n                    1,\n                    uniq,\n                ),\n            )\n\n        self.conn.commit()\n\n        return self\n\n    def get_embeddings(self, entity_id: int, limit: int = 1000):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id,\n                       content_embedding\n                  FROM memori_entity_fact\n                 WHERE entity_id = ?\n                 ORDER BY date_last_time DESC,\n                          num_times DESC,\n                          id DESC\n                 LIMIT ?\n                \"\"\",\n                (entity_id, limit),\n            )\n            .mappings()\n            .fetchall()\n        )\n\n    def get_facts_by_ids(self, fact_ids: list[int]):\n        if not fact_ids:\n            return []\n        placeholders = \",\".join([\"?\"] * len(fact_ids))\n\n        query = f\"\"\"\n                SELECT id,\n                       content,\n                       date_created\n                  FROM memori_entity_fact\n                 WHERE id IN ({placeholders})\n                \"\"\"  # nosec B608: Safe - only interpolating placeholder count, actual values parameterized\n        return self.conn.execute(query, tuple(fact_ids)).mappings().fetchall()\n\n\nclass KnowledgeGraph(BaseKnowledgeGraph):\n    def create(self, entity_id: int, semantic_triples: list):\n        if semantic_triples is None or len(semantic_triples) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n\n        for semantic_triple in semantic_triples:\n            # Insert or get subject\n            subject_uniq = generate_uniq(\n                [semantic_triple.subject_name, semantic_triple.subject_type]\n            )\n\n            self.conn.execute(\n                \"\"\"\n                INSERT OR IGNORE INTO memori_subject(\n                    uuid,\n                    name,\n                    type,\n                    uniq\n                ) VALUES (?, ?, ?, ?)\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.subject_name,\n                    semantic_triple.subject_type,\n                    subject_uniq,\n                ),\n            )\n            self.conn.commit()\n\n            subject_id = (\n                self.conn.execute(\n                    \"SELECT id FROM memori_subject WHERE uniq = ?\",\n                    (subject_uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            # Insert or get predicate\n            predicate_uniq = generate_uniq([semantic_triple.predicate])\n\n            self.conn.execute(\n                \"\"\"\n                INSERT OR IGNORE INTO memori_predicate(\n                    uuid,\n                    content,\n                    uniq\n                ) VALUES (?, ?, ?)\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.predicate,\n                    predicate_uniq,\n                ),\n            )\n            self.conn.commit()\n\n            predicate_id = (\n                self.conn.execute(\n                    \"SELECT id FROM memori_predicate WHERE uniq = ?\",\n                    (predicate_uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            # Insert or get object\n            object_uniq = generate_uniq(\n                [semantic_triple.object_name, semantic_triple.object_type]\n            )\n\n            self.conn.execute(\n                \"\"\"\n                INSERT OR IGNORE INTO memori_object(\n                    uuid,\n                    name,\n                    type,\n                    uniq\n                ) VALUES (?, ?, ?, ?)\n                \"\"\",\n                (\n                    str(uuid4()),\n                    semantic_triple.object_name,\n                    semantic_triple.object_type,\n                    object_uniq,\n                ),\n            )\n            self.conn.commit()\n\n            object_id = (\n                self.conn.execute(\n                    \"SELECT id FROM memori_object WHERE uniq = ?\",\n                    (object_uniq,),\n                )\n                .mappings()\n                .fetchone()\n                .get(\"id\", None)\n            )\n\n            # Insert or update knowledge graph entry\n            if (\n                entity_id is not None\n                and subject_id is not None\n                and predicate_id is not None\n                and object_id is not None\n            ):\n                self.conn.execute(\n                    \"\"\"\n                    INSERT INTO memori_knowledge_graph(\n                        uuid,\n                        entity_id,\n                        subject_id,\n                        predicate_id,\n                        object_id,\n                        num_times,\n                        date_last_time\n                    ) VALUES (?, ?, ?, ?, ?, 1, datetime('now'))\n                    ON CONFLICT(entity_id, subject_id, predicate_id, object_id) DO UPDATE SET\n                        num_times = num_times + 1,\n                        date_last_time = datetime('now')\n                    \"\"\",\n                    (str(uuid4()), entity_id, subject_id, predicate_id, object_id),\n                )\n                self.conn.commit()\n\n        return self\n\n\nclass Process(BaseProcess):\n    def create(self, external_id: str):\n        self.conn.execute(\n            \"\"\"\n            INSERT OR IGNORE INTO memori_process(\n                uuid,\n                external_id\n            ) VALUES (\n                ?,\n                ?\n            )\n            \"\"\",\n            (str(uuid4()), external_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_process\n                 WHERE external_id = ?\n                \"\"\",\n                (external_id,),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n\nclass ProcessAttribute(BaseProcessAttribute):\n    def create(self, process_id: int, attributes: list):\n        if attributes is None or len(attributes) == 0:\n            return self\n\n        from memori._utils import generate_uniq\n\n        for attribute in attributes:\n            uniq = generate_uniq([attribute])\n\n            self.conn.execute(\n                \"\"\"\n                INSERT INTO memori_process_attribute(\n                    uuid,\n                    process_id,\n                    content,\n                    num_times,\n                    date_last_time,\n                    uniq\n                ) VALUES (?, ?, ?, 1, datetime('now'), ?)\n                ON CONFLICT(process_id, uniq) DO UPDATE SET\n                    num_times = num_times + 1,\n                    date_last_time = datetime('now')\n                \"\"\",\n                (str(uuid4()), process_id, attribute, uniq),\n            )\n\n        self.conn.commit()\n        return self\n\n\nclass Session(BaseSession):\n    def create(self, uuid: str, entity_id: int, process_id: int):\n        self.conn.execute(\n            \"\"\"\n            INSERT OR IGNORE INTO memori_session(\n                uuid,\n                entity_id,\n                process_id\n            ) VALUES (\n                ?,\n                ?,\n                ?\n            )\n            \"\"\",\n            (str(uuid), entity_id, process_id),\n        )\n        self.conn.commit()\n\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_session\n                 WHERE uuid = ?\n                \"\"\",\n                (str(uuid),),\n            )\n            .mappings()\n            .fetchone()\n            .get(\"id\", None)\n        )\n\n    def read(self, uuid: str) -> int | None:\n        result = (\n            self.conn.execute(\n                \"\"\"\n                SELECT id\n                  FROM memori_session\n                 WHERE uuid = ?\n                \"\"\",\n                (str(uuid),),\n            )\n            .mappings()\n            .fetchone()\n        )\n        if result is None:\n            return None\n        return result.get(\"id\", None)\n\n\nclass Schema(BaseSchema):\n    def __init__(self, conn: BaseStorageAdapter):\n        super().__init__(conn)\n        self.version = SchemaVersion(conn)\n\n\nclass SchemaVersion(BaseSchemaVersion):\n    def create(self, num: int):\n        self.conn.execute(\n            \"\"\"\n            INSERT INTO memori_schema_version(\n                num\n            ) VALUES (\n                ?\n            )\n            \"\"\",\n            (num,),\n        )\n\n    def delete(self):\n        self.conn.execute(\n            \"\"\"\n            DELETE FROM memori_schema_version\n            \"\"\"\n        )\n\n    def read(self):\n        return (\n            self.conn.execute(\n                \"\"\"\n                SELECT num\n                  FROM memori_schema_version\n                \"\"\"\n            )\n            .mappings()\n            .fetchone()\n            .get(\"num\", None)\n        )\n\n\n@Registry.register_driver(\"sqlite\")\nclass Driver:\n    \"\"\"SQLite storage driver.\n\n    Attributes:\n        migrations: Database schema migrations for SQLite.\n        requires_rollback_on_error: SQLite does not abort transactions on query\n            errors, so no rollback is needed to continue executing queries.\n    \"\"\"\n\n    migrations = migrations\n    requires_rollback_on_error = False\n\n    def __init__(self, conn: BaseStorageAdapter):\n        self.conversation = Conversation(conn)\n        self.entity = Entity(conn)\n        self.entity_fact = EntityFact(conn)\n        self.knowledge_graph = KnowledgeGraph(conn)\n        self.process = Process(conn)\n        self.process_attribute = ProcessAttribute(conn)\n        self.schema = Schema(conn)\n        self.session = Session(conn)\n"
  },
  {
    "path": "memori/storage/migrations/_mongodb.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nmigrations = {\n    1: [\n        {\n            \"description\": \"create collection memori_schema_version\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_schema_version\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"num\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_entity\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_entity\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"external_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_entity\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_process\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_process\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"external_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_process\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_session\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_session\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_session\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"entity_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True, \"sparse\": True},\n                },\n                {\n                    \"collection\": \"memori_session\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"process_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True, \"sparse\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_conversation\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_conversation\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"session_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_conversation\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_conversation_message\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_conversation_message\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_conversation_message\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"conversation_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_entity_fact\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_entity_fact\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_entity_fact\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"entity_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_entity_fact\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"entity_id\", 1), (\"uniq\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_entity_fact\",\n                    \"method\": \"create_index\",\n                    \"args\": [\n                        [(\"entity_id\", 1), (\"num_times\", -1), (\"date_last_time\", -1)]\n                    ],\n                    \"kwargs\": {\"name\": \"idx_memori_entity_fact_entity_id_freq\"},\n                },\n                # NOTE: We intentionally do not create a second index with the same\n                # key pattern as the unique (entity_id, _id) index above. MongoDB\n                # will raise IndexOptionsConflict if an index already exists with\n                # different options (e.g. unique vs non-unique).\n            ],\n        },\n        {\n            \"description\": \"create collection memori_process_attribute\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_process_attribute\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_process_attribute\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"process_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_process_attribute\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"process_id\", 1), (\"uniq\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_subject\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_subject\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_subject\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uniq\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_predicate\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_predicate\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_predicate\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uniq\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_object\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_object\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_object\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uniq\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n        {\n            \"description\": \"create collection memori_knowledge_graph\",\n            \"operations\": [\n                {\n                    \"collection\": \"memori_knowledge_graph\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"uuid\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_knowledge_graph\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"entity_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_knowledge_graph\",\n                    \"method\": \"create_index\",\n                    \"args\": [\n                        [\n                            (\"entity_id\", 1),\n                            (\"subject_id\", 1),\n                            (\"predicate_id\", 1),\n                            (\"object_id\", 1),\n                        ]\n                    ],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_knowledge_graph\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"subject_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_knowledge_graph\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"predicate_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n                {\n                    \"collection\": \"memori_knowledge_graph\",\n                    \"method\": \"create_index\",\n                    \"args\": [[(\"object_id\", 1), (\"_id\", 1)]],\n                    \"kwargs\": {\"unique\": True},\n                },\n            ],\n        },\n    ]\n}\n"
  },
  {
    "path": "memori/storage/migrations/_mysql.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nmigrations = {\n    1: [\n        {\n            \"description\": \"create table memori_schema_version\",\n            \"operation\": \"\"\"\n                create table if not exists memori_schema_version(\n                    num bigint not null primary key\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity\",\n            \"operation\": \"\"\"\n                create table if not exists memori_entity(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    external_id varchar(100) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (external_id),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process\",\n            \"operation\": \"\"\"\n                create table if not exists memori_process(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    external_id varchar(100) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (external_id),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_session\",\n            \"operation\": \"\"\"\n                create table if not exists memori_session(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    entity_id bigint default null,\n                    process_id bigint default null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (entity_id, id),\n                    unique key (process_id, id),\n                    unique key (uuid),\n                    --\n                    constraint fk_memori_sess_entity\n                   foreign key (entity_id)\n                    references memori_entity (id)\n                     on delete cascade,\n                    constraint fk_memori_sess_process\n                   foreign key (process_id)\n                    references memori_process (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation\",\n            \"operation\": \"\"\"\n                create table if not exists memori_conversation(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    session_id bigint not null,\n                    summary text default null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (session_id),\n                    unique key (uuid),\n                    --\n                    constraint fk_memori_conv_session\n                   foreign key (session_id)\n                    references memori_session (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation_message\",\n            \"operation\": \"\"\"\n                create table if not exists memori_conversation_message(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    conversation_id bigint not null,\n                    role varchar(255) not null,\n                    type varchar(255) default null,\n                    content text not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (conversation_id, id),\n                    unique key (uuid),\n                    --\n                    constraint fk_memori_conv_msg_conv\n                   foreign key (conversation_id)\n                    references memori_conversation (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity_fact\",\n            \"operation\": \"\"\"\n                create table if not exists memori_entity_fact(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    entity_id bigint not null,\n                    content text not null,\n                    content_embedding blob not null,\n                    num_times bigint not null,\n                    date_last_time datetime not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (entity_id, id),\n                    unique key (entity_id, uniq),\n                    unique key (uuid),\n                    --\n                    key idx_memori_entity_fact_entity_id_freq (entity_id, num_times desc, date_last_time desc),\n                    --\n                    constraint fk_memori_ent_summ_entity\n                   foreign key (entity_id)\n                    references memori_entity (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process_attribute\",\n            \"operation\": \"\"\"\n                create table if not exists memori_process_attribute(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    process_id bigint not null,\n                    content text not null,\n                    num_times bigint not null,\n                    date_last_time datetime not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    primary key (id),\n                    unique key (process_id, id),\n                    unique key (process_id, uniq),\n                    unique key (uuid),\n                    constraint fk_memori_proc_attribute\n                   foreign key (process_id)\n                    references memori_process (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_subject\",\n            \"operation\": \"\"\"\n                create table if not exists memori_subject(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    name varchar(255) not null,\n                    type varchar(255) not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (uniq),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_predicate\",\n            \"operation\": \"\"\"\n                create table if not exists memori_predicate(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    content text not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (uniq),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_object\",\n            \"operation\": \"\"\"\n                create table if not exists memori_object(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    name varchar(255) not null,\n                    type varchar(255) not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (uniq),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_knowledge_graph\",\n            \"operation\": \"\"\"\n                create table if not exists memori_knowledge_graph(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    entity_id bigint not null,\n                    subject_id bigint not null,\n                    predicate_id bigint not null,\n                    object_id bigint not null,\n                    num_times bigint not null,\n                    date_last_time datetime not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (entity_id, id),\n                    unique key (entity_id, subject_id, predicate_id, object_id),\n                    unique key (object_id, id),\n                    unique key (predicate_id, id),\n                    unique key (subject_id, id),\n                    unique key (uuid),\n                    --\n                    constraint fk_memori_know_graph_entity\n                       foreign key (entity_id)\n                    references memori_entity (id)\n                     on delete cascade,\n                    --\n                    constraint fk_memori_know_graph_object\n                       foreign key (object_id)\n                    references memori_object (id)\n                     on delete cascade,\n                    --\n                    constraint fk_memori_know_graph_predicate\n                       foreign key (predicate_id)\n                    references memori_predicate (id)\n                     on delete cascade,\n                    --\n                    constraint fk_memori_know_graph_subject\n                       foreign key (subject_id)\n                    references memori_subject (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n    ]\n}\n"
  },
  {
    "path": "memori/storage/migrations/_oceanbase.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nmigrations = {\n    1: [\n        {\n            \"description\": \"create table memori_schema_version\",\n            \"operation\": \"\"\"\n                create table if not exists memori_schema_version(\n                    num bigint not null primary key\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity\",\n            \"operation\": \"\"\"\n                create table if not exists memori_entity(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    external_id varchar(100) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (external_id),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process\",\n            \"operation\": \"\"\"\n                create table if not exists memori_process(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    external_id varchar(100) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (external_id),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_session\",\n            \"operation\": \"\"\"\n                create table if not exists memori_session(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    entity_id bigint default null,\n                    process_id bigint default null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (entity_id, id),\n                    unique key (process_id, id),\n                    unique key (uuid),\n                    --\n                    constraint fk_memori_sess_entity\n                   foreign key (entity_id)\n                    references memori_entity (id)\n                     on delete cascade,\n                    constraint fk_memori_sess_process\n                   foreign key (process_id)\n                    references memori_process (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation\",\n            \"operation\": \"\"\"\n                create table if not exists memori_conversation(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    session_id bigint not null,\n                    summary text default null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (session_id),\n                    unique key (uuid),\n                    --\n                    constraint fk_memori_conv_session\n                   foreign key (session_id)\n                    references memori_session (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation_message\",\n            \"operation\": \"\"\"\n                create table if not exists memori_conversation_message(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    conversation_id bigint not null,\n                    role varchar(255) not null,\n                    type varchar(255) default null,\n                    content text not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (conversation_id, id),\n                    unique key (uuid),\n                    --\n                    constraint fk_memori_conv_msg_conv\n                   foreign key (conversation_id)\n                    references memori_conversation (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity_fact\",\n            \"operation\": \"\"\"\n                create table if not exists memori_entity_fact(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    entity_id bigint not null,\n                    content text not null,\n                    content_embedding blob not null,\n                    num_times bigint not null,\n                    date_last_time datetime not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (entity_id, id),\n                    unique key (entity_id, uniq),\n                    unique key (uuid),\n                    --\n                    key idx_memori_entity_fact_entity_id_freq (entity_id, num_times desc, date_last_time desc),\n                    --\n                    constraint fk_memori_ent_summ_entity\n                   foreign key (entity_id)\n                    references memori_entity (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process_attribute\",\n            \"operation\": \"\"\"\n                create table if not exists memori_process_attribute(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    process_id bigint not null,\n                    content text not null,\n                    num_times bigint not null,\n                    date_last_time datetime not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    primary key (id),\n                    unique key (process_id, id),\n                    unique key (process_id, uniq),\n                    unique key (uuid),\n                    constraint fk_memori_proc_attribute\n                   foreign key (process_id)\n                    references memori_process (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_subject\",\n            \"operation\": \"\"\"\n                create table if not exists memori_subject(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    name varchar(255) not null,\n                    type varchar(255) not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (uniq),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_predicate\",\n            \"operation\": \"\"\"\n                create table if not exists memori_predicate(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    content text not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (uniq),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_object\",\n            \"operation\": \"\"\"\n                create table if not exists memori_object(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    name varchar(255) not null,\n                    type varchar(255) not null,\n                    uniq char(64) not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (uniq),\n                    unique key (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_knowledge_graph\",\n            \"operation\": \"\"\"\n                create table if not exists memori_knowledge_graph(\n                    id bigint not null auto_increment,\n                    uuid varchar(36) not null,\n                    entity_id bigint not null,\n                    subject_id bigint not null,\n                    predicate_id bigint not null,\n                    object_id bigint not null,\n                    num_times bigint not null,\n                    date_last_time datetime not null,\n                    date_created datetime not null default current_timestamp,\n                    date_updated datetime default null on update current_timestamp,\n                    --\n                    primary key (id),\n                    unique key (entity_id, id),\n                    unique key (entity_id, subject_id, predicate_id, object_id),\n                    unique key (object_id, id),\n                    unique key (predicate_id, id),\n                    unique key (subject_id, id),\n                    unique key (uuid),\n                    --\n                    constraint fk_memori_know_graph_entity\n                       foreign key (entity_id)\n                    references memori_entity (id)\n                     on delete cascade,\n                    --\n                    constraint fk_memori_know_graph_object\n                       foreign key (object_id)\n                    references memori_object (id)\n                     on delete cascade,\n                    --\n                    constraint fk_memori_know_graph_predicate\n                       foreign key (predicate_id)\n                    references memori_predicate (id)\n                     on delete cascade,\n                    --\n                    constraint fk_memori_know_graph_subject\n                       foreign key (subject_id)\n                    references memori_subject (id)\n                     on delete cascade\n                )\n            \"\"\",\n        },\n    ]\n}\n"
  },
  {
    "path": "memori/storage/migrations/_oracle.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nmigrations = {\n    1: [\n        {\n            \"description\": \"create table memori_schema_version\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_schema_version(\n                            num NUMBER(19) NOT NULL PRIMARY KEY\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_entity(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            external_id VARCHAR2(100) NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_entity_external_id UNIQUE (external_id),\n                            CONSTRAINT uk_memori_entity_uuid UNIQUE (uuid)\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_process(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            external_id VARCHAR2(100) NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_process_external_id UNIQUE (external_id),\n                            CONSTRAINT uk_memori_process_uuid UNIQUE (uuid)\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_session\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_session(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            entity_id NUMBER(19) DEFAULT NULL,\n                            process_id NUMBER(19) DEFAULT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_session_entity_id UNIQUE (entity_id, id),\n                            CONSTRAINT uk_memori_session_process_id UNIQUE (process_id, id),\n                            CONSTRAINT uk_memori_session_uuid UNIQUE (uuid),\n                            CONSTRAINT fk_memori_sess_entity\n                               FOREIGN KEY (entity_id)\n                                REFERENCES memori_entity (id)\n                                 ON DELETE CASCADE,\n                            CONSTRAINT fk_memori_sess_process\n                               FOREIGN KEY (process_id)\n                                REFERENCES memori_process (id)\n                                 ON DELETE CASCADE\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_conversation(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            session_id NUMBER(19) NOT NULL,\n                            summary CLOB DEFAULT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_conversation_session_id UNIQUE (session_id),\n                            CONSTRAINT uk_memori_conversation_uuid UNIQUE (uuid),\n                            CONSTRAINT fk_memori_conv_session\n                               FOREIGN KEY (session_id)\n                                REFERENCES memori_session (id)\n                                 ON DELETE CASCADE\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation_message\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_conversation_message(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            conversation_id NUMBER(19) NOT NULL,\n                            role VARCHAR2(255) NOT NULL,\n                            type VARCHAR2(255) DEFAULT NULL,\n                            content CLOB NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_conversation_message_conversation_id UNIQUE (conversation_id, id),\n                            CONSTRAINT uk_memori_conversation_message_uuid UNIQUE (uuid),\n                            CONSTRAINT fk_memori_conv_msg_conv\n                               FOREIGN KEY (conversation_id)\n                                REFERENCES memori_conversation (id)\n                                 ON DELETE CASCADE\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity_fact\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_entity_fact(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            entity_id NUMBER(19) NOT NULL,\n                            content CLOB NOT NULL,\n                            content_embedding BLOB NOT NULL,\n                            num_times NUMBER(19) NOT NULL,\n                            date_last_time TIMESTAMP NOT NULL,\n                            uniq CHAR(64) NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_entity_fact_entity_id UNIQUE (entity_id, id),\n                            CONSTRAINT uk_memori_entity_fact_entity_id_uniq UNIQUE (entity_id, uniq),\n                            CONSTRAINT uk_memori_entity_fact_uuid UNIQUE (uuid),\n                            CONSTRAINT fk_memori_ent_fact_entity\n                               FOREIGN KEY (entity_id)\n                                REFERENCES memori_entity (id)\n                                 ON DELETE CASCADE\n                        )\n                    ';\n                    EXECUTE IMMEDIATE '\n                        CREATE INDEX idx_memori_entity_fact_entity_id_freq\n                        ON memori_entity_fact (entity_id, num_times DESC, date_last_time DESC)\n                    ';\n                    EXECUTE IMMEDIATE '\n                        CREATE INDEX idx_memori_entity_fact_embedding_search\n                        ON memori_entity_fact (entity_id, id)\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 OR SQLCODE = -1408 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process_attribute\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_process_attribute(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            process_id NUMBER(19) NOT NULL,\n                            content CLOB NOT NULL,\n                            num_times NUMBER(19) NOT NULL,\n                            date_last_time TIMESTAMP NOT NULL,\n                            uniq CHAR(64) NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_process_attribute_process_id UNIQUE (process_id, id),\n                            CONSTRAINT uk_memori_process_attribute_process_id_uniq UNIQUE (process_id, uniq),\n                            CONSTRAINT uk_memori_process_attribute_uuid UNIQUE (uuid),\n                            CONSTRAINT fk_memori_proc_attribute\n                               FOREIGN KEY (process_id)\n                                REFERENCES memori_process (id)\n                                 ON DELETE CASCADE\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_subject\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_subject(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            name VARCHAR2(255) NOT NULL,\n                            type VARCHAR2(255) NOT NULL,\n                            uniq CHAR(64) NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_subject_uniq UNIQUE (uniq),\n                            CONSTRAINT uk_memori_subject_uuid UNIQUE (uuid)\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_predicate\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_predicate(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            content CLOB NOT NULL,\n                            uniq CHAR(64) NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_predicate_uniq UNIQUE (uniq),\n                            CONSTRAINT uk_memori_predicate_uuid UNIQUE (uuid)\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_object\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_object(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            name VARCHAR2(255) NOT NULL,\n                            type VARCHAR2(255) NOT NULL,\n                            uniq CHAR(64) NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_object_uniq UNIQUE (uniq),\n                            CONSTRAINT uk_memori_object_uuid UNIQUE (uuid)\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_knowledge_graph\",\n            \"operation\": \"\"\"\n                BEGIN\n                    EXECUTE IMMEDIATE '\n                        CREATE TABLE memori_knowledge_graph(\n                            id NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n                            uuid VARCHAR2(36) NOT NULL,\n                            entity_id NUMBER(19) NOT NULL,\n                            subject_id NUMBER(19) NOT NULL,\n                            predicate_id NUMBER(19) NOT NULL,\n                            object_id NUMBER(19) NOT NULL,\n                            num_times NUMBER(19) NOT NULL,\n                            date_last_time TIMESTAMP NOT NULL,\n                            date_created TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n                            date_updated TIMESTAMP DEFAULT NULL,\n                            CONSTRAINT uk_memori_knowledge_graph_entity_id UNIQUE (entity_id, id),\n                            CONSTRAINT uk_memori_knowledge_graph_entity_subject_predicate_object UNIQUE (entity_id, subject_id, predicate_id, object_id),\n                            CONSTRAINT uk_memori_knowledge_graph_object_id UNIQUE (object_id, id),\n                            CONSTRAINT uk_memori_knowledge_graph_predicate_id UNIQUE (predicate_id, id),\n                            CONSTRAINT uk_memori_knowledge_graph_subject_id UNIQUE (subject_id, id),\n                            CONSTRAINT uk_memori_knowledge_graph_uuid UNIQUE (uuid),\n                            CONSTRAINT fk_memori_know_graph_entity\n                               FOREIGN KEY (entity_id)\n                                REFERENCES memori_entity (id)\n                                 ON DELETE CASCADE,\n                            CONSTRAINT fk_memori_know_graph_object\n                               FOREIGN KEY (object_id)\n                                REFERENCES memori_object (id)\n                                 ON DELETE CASCADE,\n                            CONSTRAINT fk_memori_know_graph_predicate\n                               FOREIGN KEY (predicate_id)\n                                REFERENCES memori_predicate (id)\n                                 ON DELETE CASCADE,\n                            CONSTRAINT fk_memori_know_graph_subject\n                               FOREIGN KEY (subject_id)\n                                REFERENCES memori_subject (id)\n                                 ON DELETE CASCADE\n                        )\n                    ';\n                EXCEPTION\n                    WHEN OTHERS THEN\n                        IF SQLCODE = -955 THEN NULL;\n                        ELSE RAISE;\n                        END IF;\n                END;\n            \"\"\",\n        },\n    ]\n}\n"
  },
  {
    "path": "memori/storage/migrations/_postgresql.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nmigrations = {\n    1: [\n        {\n            \"description\": \"create table memori_schema_version\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_schema_version(\n                    num BIGINT NOT NULL PRIMARY KEY\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_entity(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    external_id VARCHAR(100) NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_entity_external_id UNIQUE (external_id),\n                    CONSTRAINT uk_memori_entity_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_process(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    external_id VARCHAR(100) NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_process_external_id UNIQUE (external_id),\n                    CONSTRAINT uk_memori_process_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_session\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_session(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    entity_id BIGINT DEFAULT NULL,\n                    process_id BIGINT DEFAULT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_session_entity_id UNIQUE (entity_id, id),\n                    CONSTRAINT uk_memori_session_process_id UNIQUE (process_id, id),\n                    CONSTRAINT uk_memori_session_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_sess_entity\n                       FOREIGN KEY (entity_id)\n                        REFERENCES memori_entity (id)\n                         ON DELETE CASCADE,\n                    CONSTRAINT fk_memori_sess_process\n                       FOREIGN KEY (process_id)\n                        REFERENCES memori_process (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_conversation(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    session_id BIGINT NOT NULL,\n                    summary TEXT DEFAULT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_conversation_session_id UNIQUE (session_id),\n                    CONSTRAINT uk_memori_conversation_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_conv_session\n                       FOREIGN KEY (session_id)\n                        REFERENCES memori_session (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation_message\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_conversation_message(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    conversation_id BIGINT NOT NULL,\n                    role VARCHAR(255) NOT NULL,\n                    type VARCHAR(255) DEFAULT NULL,\n                    content TEXT NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_conversation_message_conversation_id UNIQUE (conversation_id, id),\n                    CONSTRAINT uk_memori_conversation_message_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_conv_msg_conv\n                       FOREIGN KEY (conversation_id)\n                        REFERENCES memori_conversation (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity_fact\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_entity_fact(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    entity_id BIGINT NOT NULL,\n                    content TEXT NOT NULL,\n                    content_embedding BYTEA NOT NULL,\n                    num_times BIGINT NOT NULL,\n                    date_last_time TIMESTAMP NOT NULL,\n                    uniq CHAR(64) NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_entity_fact_entity_id UNIQUE (entity_id, id),\n                    CONSTRAINT uk_memori_entity_fact_entity_id_uniq UNIQUE (entity_id, uniq),\n                    CONSTRAINT uk_memori_entity_fact_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_ent_fact_entity\n                       FOREIGN KEY (entity_id)\n                        REFERENCES memori_entity (id)\n                         ON DELETE CASCADE\n                );\n                CREATE INDEX IF NOT EXISTS idx_memori_entity_fact_entity_id_freq\n                ON memori_entity_fact (entity_id, num_times DESC, date_last_time DESC);\n                CREATE INDEX IF NOT EXISTS idx_memori_entity_fact_embedding_search\n                ON memori_entity_fact (entity_id, id)\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process_attribute\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_process_attribute(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    process_id BIGINT NOT NULL,\n                    content TEXT NOT NULL,\n                    num_times BIGINT NOT NULL,\n                    date_last_time TIMESTAMP NOT NULL,\n                    uniq CHAR(64) NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_process_attribute_process_id UNIQUE (process_id, id),\n                    CONSTRAINT uk_memori_process_attribute_process_id_uniq UNIQUE (process_id, uniq),\n                    CONSTRAINT uk_memori_process_attribute_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_proc_attribute\n                       FOREIGN KEY (process_id)\n                        REFERENCES memori_process (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_subject\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_subject(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    name VARCHAR(255) NOT NULL,\n                    type VARCHAR(255) NOT NULL,\n                    uniq CHAR(64) NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_subject_uniq UNIQUE (uniq),\n                    CONSTRAINT uk_memori_subject_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_predicate\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_predicate(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    content TEXT NOT NULL,\n                    uniq CHAR(64) NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_predicate_uniq UNIQUE (uniq),\n                    CONSTRAINT uk_memori_predicate_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_object\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_object(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    name VARCHAR(255) NOT NULL,\n                    type VARCHAR(255) NOT NULL,\n                    uniq CHAR(64) NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_object_uniq UNIQUE (uniq),\n                    CONSTRAINT uk_memori_object_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_knowledge_graph\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_knowledge_graph(\n                    id BIGSERIAL PRIMARY KEY,\n                    uuid VARCHAR(36) NOT NULL,\n                    entity_id BIGINT NOT NULL,\n                    subject_id BIGINT NOT NULL,\n                    predicate_id BIGINT NOT NULL,\n                    object_id BIGINT NOT NULL,\n                    num_times BIGINT NOT NULL,\n                    date_last_time TIMESTAMP NOT NULL,\n                    date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n                    date_updated TIMESTAMP DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_knowledge_graph_entity_id UNIQUE (entity_id, id),\n                    CONSTRAINT uk_memori_knowledge_graph_entity_subject_predicate_object UNIQUE (entity_id, subject_id, predicate_id, object_id),\n                    CONSTRAINT uk_memori_knowledge_graph_object_id UNIQUE (object_id, id),\n                    CONSTRAINT uk_memori_knowledge_graph_predicate_id UNIQUE (predicate_id, id),\n                    CONSTRAINT uk_memori_knowledge_graph_subject_id UNIQUE (subject_id, id),\n                    CONSTRAINT uk_memori_knowledge_graph_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_know_graph_entity\n                       FOREIGN KEY (entity_id)\n                        REFERENCES memori_entity (id)\n                         ON DELETE CASCADE,\n                    CONSTRAINT fk_memori_know_graph_object\n                       FOREIGN KEY (object_id)\n                        REFERENCES memori_object (id)\n                         ON DELETE CASCADE,\n                    CONSTRAINT fk_memori_know_graph_predicate\n                       FOREIGN KEY (predicate_id)\n                        REFERENCES memori_predicate (id)\n                         ON DELETE CASCADE,\n                    CONSTRAINT fk_memori_know_graph_subject\n                       FOREIGN KEY (subject_id)\n                        REFERENCES memori_subject (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n    ]\n}\n"
  },
  {
    "path": "memori/storage/migrations/_sqlite.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n\"\"\"\n\nmigrations = {\n    1: [\n        {\n            \"description\": \"create table memori_schema_version\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_schema_version(\n                    num INTEGER NOT NULL PRIMARY KEY\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_entity(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    external_id TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_entity_external_id UNIQUE (external_id),\n                    CONSTRAINT uk_memori_entity_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_process(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    external_id TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_process_external_id UNIQUE (external_id),\n                    CONSTRAINT uk_memori_process_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_session\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_session(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    entity_id INTEGER DEFAULT NULL,\n                    process_id INTEGER DEFAULT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_session_entity_id UNIQUE (entity_id, id),\n                    CONSTRAINT uk_memori_session_process_id UNIQUE (process_id, id),\n                    CONSTRAINT uk_memori_session_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_sess_entity\n                       FOREIGN KEY (entity_id)\n                        REFERENCES memori_entity (id)\n                         ON DELETE CASCADE,\n                    CONSTRAINT fk_memori_sess_process\n                       FOREIGN KEY (process_id)\n                        REFERENCES memori_process (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_conversation(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    session_id INTEGER NOT NULL,\n                    summary TEXT DEFAULT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_conversation_session_id UNIQUE (session_id),\n                    CONSTRAINT uk_memori_conversation_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_conv_session\n                       FOREIGN KEY (session_id)\n                        REFERENCES memori_session (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_conversation_message\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_conversation_message(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    conversation_id INTEGER NOT NULL,\n                    role TEXT NOT NULL,\n                    type TEXT DEFAULT NULL,\n                    content TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_conversation_message_conversation_id UNIQUE (conversation_id, id),\n                    CONSTRAINT uk_memori_conversation_message_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_conv_msg_conv\n                       FOREIGN KEY (conversation_id)\n                        REFERENCES memori_conversation (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_entity_fact\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_entity_fact(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    entity_id INTEGER NOT NULL,\n                    content TEXT NOT NULL,\n                    content_embedding BLOB NOT NULL,\n                    num_times INTEGER NOT NULL,\n                    date_last_time TEXT NOT NULL,\n                    uniq TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_entity_fact_entity_id UNIQUE (entity_id, id),\n                    CONSTRAINT uk_memori_entity_fact_entity_id_uniq UNIQUE (entity_id, uniq),\n                    CONSTRAINT uk_memori_entity_fact_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_ent_fact_entity\n                       FOREIGN KEY (entity_id)\n                        REFERENCES memori_entity (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create index on memori_entity_fact for frequency queries\",\n            \"operation\": \"\"\"\n                CREATE INDEX IF NOT EXISTS idx_memori_entity_fact_entity_id_freq\n                ON memori_entity_fact (entity_id, num_times DESC, date_last_time DESC)\n            \"\"\",\n        },\n        {\n            \"description\": \"create index on memori_entity_fact for embedding search\",\n            \"operation\": \"\"\"\n                CREATE INDEX IF NOT EXISTS idx_memori_entity_fact_embedding_search\n                ON memori_entity_fact (entity_id, id)\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_process_attribute\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_process_attribute(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    process_id INTEGER NOT NULL,\n                    content TEXT NOT NULL,\n                    num_times INTEGER NOT NULL,\n                    date_last_time TEXT NOT NULL,\n                    uniq TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_process_attribute_process_id UNIQUE (process_id, id),\n                    CONSTRAINT uk_memori_process_attribute_process_id_uniq UNIQUE (process_id, uniq),\n                    CONSTRAINT uk_memori_process_attribute_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_proc_attribute\n                       FOREIGN KEY (process_id)\n                        REFERENCES memori_process (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_subject\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_subject(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    name TEXT NOT NULL,\n                    type TEXT NOT NULL,\n                    uniq TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_subject_uniq UNIQUE (uniq),\n                    CONSTRAINT uk_memori_subject_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_predicate\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_predicate(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    content TEXT NOT NULL,\n                    uniq TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_predicate_uniq UNIQUE (uniq),\n                    CONSTRAINT uk_memori_predicate_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_object\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_object(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    name TEXT NOT NULL,\n                    type TEXT NOT NULL,\n                    uniq TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_object_uniq UNIQUE (uniq),\n                    CONSTRAINT uk_memori_object_uuid UNIQUE (uuid)\n                )\n            \"\"\",\n        },\n        {\n            \"description\": \"create table memori_knowledge_graph\",\n            \"operation\": \"\"\"\n                CREATE TABLE IF NOT EXISTS memori_knowledge_graph(\n                    id INTEGER PRIMARY KEY AUTOINCREMENT,\n                    uuid TEXT NOT NULL,\n                    entity_id INTEGER NOT NULL,\n                    subject_id INTEGER NOT NULL,\n                    predicate_id INTEGER NOT NULL,\n                    object_id INTEGER NOT NULL,\n                    num_times INTEGER NOT NULL,\n                    date_last_time TEXT NOT NULL,\n                    date_created TEXT NOT NULL DEFAULT (datetime('now')),\n                    date_updated TEXT DEFAULT NULL,\n                    --\n                    CONSTRAINT uk_memori_knowledge_graph_entity_id UNIQUE (entity_id, id),\n                    CONSTRAINT uk_memori_knowledge_graph_entity_subject_predicate_object UNIQUE (entity_id, subject_id, predicate_id, object_id),\n                    CONSTRAINT uk_memori_knowledge_graph_object_id UNIQUE (object_id, id),\n                    CONSTRAINT uk_memori_knowledge_graph_predicate_id UNIQUE (predicate_id, id),\n                    CONSTRAINT uk_memori_knowledge_graph_subject_id UNIQUE (subject_id, id),\n                    CONSTRAINT uk_memori_knowledge_graph_uuid UNIQUE (uuid),\n                    --\n                    CONSTRAINT fk_memori_know_graph_entity\n                       FOREIGN KEY (entity_id)\n                        REFERENCES memori_entity (id)\n                         ON DELETE CASCADE,\n                    CONSTRAINT fk_memori_know_graph_object\n                       FOREIGN KEY (object_id)\n                        REFERENCES memori_object (id)\n                         ON DELETE CASCADE,\n                    CONSTRAINT fk_memori_know_graph_predicate\n                       FOREIGN KEY (predicate_id)\n                        REFERENCES memori_predicate (id)\n                         ON DELETE CASCADE,\n                    CONSTRAINT fk_memori_know_graph_subject\n                       FOREIGN KEY (subject_id)\n                        REFERENCES memori_subject (id)\n                         ON DELETE CASCADE\n                )\n            \"\"\",\n        },\n    ]\n}\n"
  },
  {
    "path": "memori-ts/.npmignore",
    "content": "# Exclude development configurations\ntsconfig.json\ntsconfig.build.json\nvitest.config.ts\neslint.config.js\n.editorconfig\n.prettierrc\n\n# Exclude source code and tests (users only need the compiled dist)\nsrc/\ntests/\nexamples/\n\n# Exclude environment and logs\n.env\n*.log\n.DS_Store"
  },
  {
    "path": "memori-ts/.prettierrc.json",
    "content": "{\n  \"semi\": true,\n  \"trailingComma\": \"es5\",\n  \"singleQuote\": true,\n  \"printWidth\": 100,\n  \"tabWidth\": 2,\n  \"arrowParens\": \"always\"\n}\n"
  },
  {
    "path": "memori-ts/README.md",
    "content": "[![Memori Labs](https://s3.us-east-1.amazonaws.com/images.memorilabs.ai/banner.png)](https://memorilabs.ai/)\n\n<p align=\"center\">\n  <strong>The memory fabric for enterprise AI</strong>\n</p>\n\n<p align=\"center\">\n  <i>Memori plugs into the software and infrastructure you already use. It is LLM and framework agnostic and seamlessly integrates into the architecture you've already designed.</i>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@memorilabs/memori\">\n    <img src=\"https://img.shields.io/npm/v/@memorilabs/memori.svg\" alt=\"NPM version\">\n  </a>\n  <a href=\"https://www.npmjs.com/package/@memorilabs/memori\">\n    <img src=\"https://img.shields.io/npm/dm/@memorilabs/memori.svg\" alt=\"NPM Downloads\">\n  </a>\n  <a href=\"https://opensource.org/license/apache-2-0\">\n    <img src=\"https://img.shields.io/badge/license-Apache%202.0-blue\" alt=\"License\">\n  </a>\n  <a href=\"https://discord.gg/abD4eGym6v\">\n    <img src=\"https://img.shields.io/discord/1042405378304004156?logo=discord\" alt=\"Discord\">\n  </a>\n</p>\n\n---\n\n## Getting Started\n\nInstall the Memori SDK and your preferred LLM client using your package manager of choice:\n\n```bash\nnpm install @memorilabs/memori\n```\n\n_(Note: Memori currently supports `openai` and `@anthropic-ai/sdk` as peer dependencies)._\n\n## Quickstart Example\n\n```typescript\nimport 'dotenv/config';\nimport { OpenAI } from 'openai';\nimport { Memori } from '@memorilabs/memori';\n\n// Environment check\nconst OPENAI_API_KEY = process.env.OPENAI_API_KEY;\nif (!OPENAI_API_KEY) {\n  console.error('Error: OPENAI_API_KEY must be set in .env');\n  process.exit(1);\n}\n\n// 1. Initialize the LLM Client\nconst client = new OpenAI({ apiKey: OPENAI_API_KEY });\n\n// 2. Initialize Memori and Register the Client\nconst memori = new Memori().llm\n  .register(client)\n  .attribution('typescript-sdk-test-user', 'test-process-1');\n\nasync function main() {\n  console.log('--- Step 1: Teaching the AI ---');\n  const factPrompt = 'My favorite color is blue and I live in Paris.';\n  console.log(`User: ${factPrompt}`);\n\n  // This call automatically triggers Persistence and Augmentation in the background.\n  const response1 = await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: factPrompt }],\n  });\n\n  console.log(`AI:   ${response1.choices[0].message.content}`);\n\n  console.log('\\n(Waiting 5 seconds for backend processing...)\\n');\n  await new Promise((resolve) => setTimeout(resolve, 5000));\n\n  console.log('--- Step 2: Testing Recall ---');\n  const questionPrompt = 'What is my favorite color?';\n  console.log(`User: ${questionPrompt}`);\n\n  // This call automatically triggers Recall, injecting the Paris/Blue facts into the prompt.\n  const response2 = await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: questionPrompt }],\n  });\n\n  console.log(`AI:   ${response2.choices[0].message.content}`);\n}\n\nmain().catch(console.error);\n```\n\n## Key Features\n\n- **Zero-Latency Memory:** Background processing ensures your LLM calls are never slowed down.\n- **Advanced Augmentation:** Automatically extracts and structures facts, preferences, and relationships.\n- **Cloud-Hosted:** Fully managed infrastructure via the Memori Cloud API.\n- **LLM Agnostic:** Native support for the official OpenAI and Anthropic SDKs via interceptors.\n- **Automatic Prompt Injection:** Seamlessly fetches relevant memories and injects them into the system context.\n\n## Attribution\n\nTo get the most out of Memori, you want to attribute your LLM interactions to an entity (think person, place or thing; like a user) and a process (think your agent, LLM interaction or program).\n\nIf you do not provide any attribution, Memori cannot make memories for you.\n\n```typescript\nmemori.attribution('user-123', 'my-app');\n```\n\n## Session Management\n\nMemori uses sessions to group your LLM interactions together. For example, if you have an agent that executes multiple steps you want those to be recorded in a single session.\n\nBy default, Memori handles setting the session for you but you can start a new session or override the session by executing the following:\n\n```typescript\nmemori.resetSession();\n```\n\nor\n\n```typescript\nconst sessionId = memori.session.id;\n\n// ... Later ...\n\nmemori.setSession(sessionId);\n```\n\n## Supported LLMs\n\n- Anthropic Claude (`@anthropic-ai/sdk`)\n- OpenAI (`openai`)\n- Gemini (`@google/genai`)\n\n## Memori Advanced Augmentation\n\nMemories are tracked at several different levels:\n\n- **entity**: think person, place, or thing; like a user\n- **process**: think your agent, LLM interaction or program\n- **session**: the current interactions between the entity, process and the LLM\n\n[Memori's Advanced Augmentation](https://github.com/MemoriLabs/Memori/blob/main/docs/advanced-augmentation.md) enhances memories at each of these levels with:\n\n- attributes\n- events\n- facts\n- people\n- preferences\n- relationships\n- rules\n- skills\n\nMemori knows who your user is, what tasks your agent handles and creates unparalleled context between the two. Augmentation occurs asynchronously in the background incurring no latency.\n\nBy default, Memori Advanced Augmentation is available without an account but is rate limited. When you need increased limits, [sign up for Memori Advanced Augmentation](https://app.memorilabs.ai/signup).\n\nMemori Advanced Augmentation is always free for developers!\n\nOnce you've obtained an API key, simply set the following environment variable:\n\n```bash\nexport MEMORI_API_KEY=[api_key]\n```\n\n## Managing Your Quota\nAny any time, you can check your quota using the Memori CLI:\n```bash\nmemori quota\n```\n\nOr by checking your account by logging in at [https://memorilabs.ai/](https://memorilabs.ai/). If you have reached your IP address quota, sign up and get an API key for increased limits.\n\nIf your API key exceeds its quota limits we will email you and let you know.\n\n## Contributing\n\nWe welcome contributions from the community! Please see our [Contributing Guidelines](https://github.com/MemoriLabs/Memori/blob/main/CONTRIBUTING.md) for details on:\n\n- Setting up your development environment\n- Code style and standards\n- Submitting pull requests\n- Reporting issues\n\n---\n\n## Support\n\n- **Documentation**: [https://memorilabs.ai/docs](https://memorilabs.ai/docs)\n- **Discord**: [https://discord.gg/abD4eGym6v](https://discord.gg/abD4eGym6v)\n- **Issues**: [GitHub Issues](https://github.com/MemoriLabs/Memori/issues)\n\n---\n\n## License\n\nApache 2.0 - see [LICENSE](https://github.com/MemoriLabs/Memori/blob/main/LICENSE)\n"
  },
  {
    "path": "memori-ts/eslint.config.js",
    "content": "import eslint from '@eslint/js';\nimport tseslint from 'typescript-eslint';\nimport prettier from 'eslint-config-prettier';\nimport tsdoc from 'eslint-plugin-tsdoc';\n\nexport default tseslint.config(\n  eslint.configs.recommended,\n  ...tseslint.configs.strictTypeChecked,\n  prettier,\n  {\n    languageOptions: {\n      parserOptions: {\n        projectService: true,\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n    plugins: {\n      tsdoc: tsdoc,\n    },\n    rules: {\n      '@typescript-eslint/no-unused-vars': [\n        'error',\n        { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },\n      ],\n      '@typescript-eslint/no-explicit-any': 'warn',\n      '@typescript-eslint/no-extraneous-class': 'off',\n      '@typescript-eslint/restrict-template-expressions': 'off',\n      'tsdoc/syntax': 'warn',\n    },\n  },\n  {\n    files: ['tests/**/*.ts'],\n    rules: {\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/no-unsafe-assignment': 'off',\n      '@typescript-eslint/no-unsafe-member-access': 'off',\n      '@typescript-eslint/no-unsafe-argument': 'off',\n      '@typescript-eslint/no-unsafe-return': 'off',\n      '@typescript-eslint/no-unsafe-call': 'off',\n      '@typescript-eslint/require-await': 'off',\n      '@typescript-eslint/no-non-null-assertion': 'off',\n      '@typescript-eslint/unbound-method': 'off',\n    },\n  },\n  {\n    ignores: ['dist/**', 'node_modules/**', '*.config.js', 'coverage'],\n  }\n);\n"
  },
  {
    "path": "memori-ts/examples/cloud/simple.ts",
    "content": "/**\n * Quickstart: Memori + OpenAI + Cloud\n *\n * Demonstrates how Memori adds memory across conversations.\n */\n\nimport 'dotenv/config';\nimport { OpenAI } from 'openai';\nimport { Memori } from '../../src/index.js';\n\n// Setup OpenAI\nconst client = new OpenAI({\n  apiKey: process.env.OPENAI_API_KEY || '<your_api_key_here>',\n});\n\n// Setup Memori - that's it!\nconst mem = new Memori().llm.register(client);\nmem.attribution('user-123', 'my-app');\n\nasync function main() {\n  // First conversation - establish facts\n  console.log('You: My favorite color is blue and I live in Paris');\n  const response1 = await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: 'My favorite color is blue and I live in Paris' }],\n  });\n  console.log(`AI: ${response1.choices[0]?.message?.content}\\n`);\n\n  // Give the cloud API a brief moment to index the new memory\n  await new Promise((resolve) => setTimeout(resolve, 2000));\n\n  // Second conversation - Memori recalls context automatically\n  console.log(\"You: What's my favorite color?\");\n  const response2 = await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: \"What's my favorite color?\" }],\n  });\n  console.log(`AI: ${response2.choices[0]?.message?.content}\\n`);\n\n  // Third conversation - context is maintained\n  console.log('You: What city do I live in?');\n  const response3 = await client.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [{ role: 'user', content: 'What city do I live in?' }],\n  });\n  console.log(`AI: ${response3.choices[0]?.message?.content}\\n`);\n\n  // Advanced Augmentation runs asynchronously to efficiently\n  // create memories. For this example, a short lived command\n  // line program, we need to wait for it to finish.\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "memori-ts/package-lock.json",
    "content": "{\n  \"name\": \"@memorilabs/memori\",\n  \"version\": \"0.0.5\",\n  \"lockfileVersion\": 3,\n  \"requires\": true,\n  \"packages\": {\n    \"\": {\n      \"name\": \"@memorilabs/memori\",\n      \"version\": \"0.0.5\",\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@memorilabs/axon\": \"^0.1.2\"\n      },\n      \"bin\": {\n        \"memori\": \"dist/bin/cli.js\"\n      },\n      \"devDependencies\": {\n        \"@anthropic-ai/sdk\": \"*\",\n        \"@eslint/js\": \"^9.0.0\",\n        \"@types/node\": \"^20.0.0\",\n        \"@vitest/coverage-v8\": \"^2.1.9\",\n        \"@vitest/ui\": \"^2.1.9\",\n        \"dotenv\": \"^16.4.5\",\n        \"eslint\": \"^9.0.0\",\n        \"eslint-config-prettier\": \"^10.1.8\",\n        \"eslint-plugin-tsdoc\": \"^0.5.1\",\n        \"lint-staged\": \"^15.0.0\",\n        \"openai\": \"*\",\n        \"prettier\": \"^3.0.0\",\n        \"typescript\": \"^5.0.0\",\n        \"typescript-eslint\": \"^8.0.0\",\n        \"vitest\": \"^2.1.9\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"@anthropic-ai/sdk\": \"*\",\n        \"openai\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@anthropic-ai/sdk\": {\n          \"optional\": true\n        },\n        \"openai\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@ampproject/remapping\": {\n      \"version\": \"2.3.0\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@jridgewell/gen-mapping\": \"^0.3.5\",\n        \"@jridgewell/trace-mapping\": \"^0.3.24\"\n      },\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/@anthropic-ai/sdk\": {\n      \"version\": \"0.78.0\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"json-schema-to-ts\": \"^3.1.1\"\n      },\n      \"bin\": {\n        \"anthropic-ai-sdk\": \"bin/cli\"\n      },\n      \"peerDependencies\": {\n        \"zod\": \"^3.25.0 || ^4.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"zod\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@babel/helper-string-parser\": {\n      \"version\": \"7.27.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/helper-validator-identifier\": {\n      \"version\": \"7.28.5\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/parser\": {\n      \"version\": \"7.29.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/types\": \"^7.29.0\"\n      },\n      \"bin\": {\n        \"parser\": \"bin/babel-parser.js\"\n      },\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/@babel/runtime\": {\n      \"version\": \"7.28.6\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@babel/types\": {\n      \"version\": \"7.29.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/helper-string-parser\": \"^7.27.1\",\n        \"@babel/helper-validator-identifier\": \"^7.28.5\"\n      },\n      \"engines\": {\n        \"node\": \">=6.9.0\"\n      }\n    },\n    \"node_modules/@bcoe/v8-coverage\": {\n      \"version\": \"0.2.3\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@esbuild/aix-ppc64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz\",\n      \"integrity\": \"sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"aix\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/android-arm\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz\",\n      \"integrity\": \"sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/android-arm64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz\",\n      \"integrity\": \"sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/android-x64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz\",\n      \"integrity\": \"sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/darwin-arm64\": {\n      \"version\": \"0.21.5\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/darwin-x64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz\",\n      \"integrity\": \"sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/freebsd-arm64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz\",\n      \"integrity\": \"sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/freebsd-x64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz\",\n      \"integrity\": \"sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-arm\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz\",\n      \"integrity\": \"sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-arm64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz\",\n      \"integrity\": \"sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-ia32\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz\",\n      \"integrity\": \"sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-loong64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz\",\n      \"integrity\": \"sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==\",\n      \"cpu\": [\n        \"loong64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-mips64el\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz\",\n      \"integrity\": \"sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==\",\n      \"cpu\": [\n        \"mips64el\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-ppc64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz\",\n      \"integrity\": \"sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-riscv64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz\",\n      \"integrity\": \"sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-s390x\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz\",\n      \"integrity\": \"sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/linux-x64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz\",\n      \"integrity\": \"sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/netbsd-x64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz\",\n      \"integrity\": \"sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"netbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/openbsd-x64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz\",\n      \"integrity\": \"sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openbsd\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/sunos-x64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz\",\n      \"integrity\": \"sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"sunos\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/win32-arm64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz\",\n      \"integrity\": \"sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/win32-ia32\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz\",\n      \"integrity\": \"sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@esbuild/win32-x64\": {\n      \"version\": \"0.21.5\",\n      \"resolved\": \"https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz\",\n      \"integrity\": \"sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ],\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@eslint-community/eslint-utils\": {\n      \"version\": \"4.9.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"eslint-visitor-keys\": \"^3.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^12.22.0 || ^14.17.0 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^6.0.0 || ^7.0.0 || >=8.0.0\"\n      }\n    },\n    \"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys\": {\n      \"version\": \"3.4.3\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^12.22.0 || ^14.17.0 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@eslint-community/regexpp\": {\n      \"version\": \"4.12.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^12.0.0 || ^14.0.0 || >=16.0.0\"\n      }\n    },\n    \"node_modules/@eslint/config-array\": {\n      \"version\": \"0.21.1\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/object-schema\": \"^2.1.7\",\n        \"debug\": \"^4.3.1\",\n        \"minimatch\": \"^3.1.2\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/config-helpers\": {\n      \"version\": \"0.4.2\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/core\": \"^0.17.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/core\": {\n      \"version\": \"0.17.0\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@types/json-schema\": \"^7.0.15\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/eslintrc\": {\n      \"version\": \"3.3.4\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ajv\": \"^6.14.0\",\n        \"debug\": \"^4.3.2\",\n        \"espree\": \"^10.0.1\",\n        \"globals\": \"^14.0.0\",\n        \"ignore\": \"^5.2.0\",\n        \"import-fresh\": \"^3.2.1\",\n        \"js-yaml\": \"^4.1.1\",\n        \"minimatch\": \"^3.1.3\",\n        \"strip-json-comments\": \"^3.1.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@eslint/js\": {\n      \"version\": \"9.39.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://eslint.org/donate\"\n      }\n    },\n    \"node_modules/@eslint/object-schema\": {\n      \"version\": \"2.1.7\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@eslint/plugin-kit\": {\n      \"version\": \"0.4.1\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@eslint/core\": \"^0.17.0\",\n        \"levn\": \"^0.4.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      }\n    },\n    \"node_modules/@humanfs/core\": {\n      \"version\": \"0.19.1\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.18.0\"\n      }\n    },\n    \"node_modules/@humanfs/node\": {\n      \"version\": \"0.16.7\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"dependencies\": {\n        \"@humanfs/core\": \"^0.19.1\",\n        \"@humanwhocodes/retry\": \"^0.4.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18.18.0\"\n      }\n    },\n    \"node_modules/@humanwhocodes/module-importer\": {\n      \"version\": \"1.0.1\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=12.22\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/nzakas\"\n      }\n    },\n    \"node_modules/@humanwhocodes/retry\": {\n      \"version\": \"0.4.3\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.18\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/nzakas\"\n      }\n    },\n    \"node_modules/@isaacs/cliui\": {\n      \"version\": \"8.0.2\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"string-width\": \"^5.1.2\",\n        \"string-width-cjs\": \"npm:string-width@^4.2.0\",\n        \"strip-ansi\": \"^7.0.1\",\n        \"strip-ansi-cjs\": \"npm:strip-ansi@^6.0.1\",\n        \"wrap-ansi\": \"^8.1.0\",\n        \"wrap-ansi-cjs\": \"npm:wrap-ansi@^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/@isaacs/cliui/node_modules/ansi-styles\": {\n      \"version\": \"6.2.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/@isaacs/cliui/node_modules/emoji-regex\": {\n      \"version\": \"9.2.2\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@isaacs/cliui/node_modules/string-width\": {\n      \"version\": \"5.1.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"eastasianwidth\": \"^0.2.0\",\n        \"emoji-regex\": \"^9.2.2\",\n        \"strip-ansi\": \"^7.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/@isaacs/cliui/node_modules/wrap-ansi\": {\n      \"version\": \"8.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^6.1.0\",\n        \"string-width\": \"^5.0.1\",\n        \"strip-ansi\": \"^7.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/@istanbuljs/schema\": {\n      \"version\": \"0.1.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/@jridgewell/gen-mapping\": {\n      \"version\": \"0.3.13\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/sourcemap-codec\": \"^1.5.0\",\n        \"@jridgewell/trace-mapping\": \"^0.3.24\"\n      }\n    },\n    \"node_modules/@jridgewell/resolve-uri\": {\n      \"version\": \"3.1.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6.0.0\"\n      }\n    },\n    \"node_modules/@jridgewell/sourcemap-codec\": {\n      \"version\": \"1.5.5\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@jridgewell/trace-mapping\": {\n      \"version\": \"0.3.31\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/resolve-uri\": \"^3.1.0\",\n        \"@jridgewell/sourcemap-codec\": \"^1.4.14\"\n      }\n    },\n    \"node_modules/@memorilabs/axon\": {\n      \"version\": \"0.1.2\",\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      },\n      \"peerDependencies\": {\n        \"@anthropic-ai/sdk\": \"*\",\n        \"@google/genai\": \"*\",\n        \"openai\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@anthropic-ai/sdk\": {\n          \"optional\": true\n        },\n        \"@google/genai\": {\n          \"optional\": true\n        },\n        \"openai\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@microsoft/tsdoc\": {\n      \"version\": \"0.16.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@microsoft/tsdoc-config\": {\n      \"version\": \"0.18.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@microsoft/tsdoc\": \"0.16.0\",\n        \"ajv\": \"~8.18.0\",\n        \"jju\": \"~1.4.0\",\n        \"resolve\": \"~1.22.2\"\n      }\n    },\n    \"node_modules/@microsoft/tsdoc-config/node_modules/ajv\": {\n      \"version\": \"8.18.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"fast-uri\": \"^3.0.1\",\n        \"json-schema-traverse\": \"^1.0.0\",\n        \"require-from-string\": \"^2.0.2\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/epoberezkin\"\n      }\n    },\n    \"node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse\": {\n      \"version\": \"1.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@pkgjs/parseargs\": {\n      \"version\": \"0.11.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"engines\": {\n        \"node\": \">=14\"\n      }\n    },\n    \"node_modules/@polka/url\": {\n      \"version\": \"1.0.0-next.29\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@rollup/rollup-android-arm-eabi\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz\",\n      \"integrity\": \"sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-android-arm64\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz\",\n      \"integrity\": \"sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"android\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-darwin-arm64\": {\n      \"version\": \"4.59.0\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-darwin-x64\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz\",\n      \"integrity\": \"sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-freebsd-arm64\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz\",\n      \"integrity\": \"sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-freebsd-x64\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz\",\n      \"integrity\": \"sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"freebsd\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-arm-gnueabihf\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz\",\n      \"integrity\": \"sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-arm-musleabihf\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz\",\n      \"integrity\": \"sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==\",\n      \"cpu\": [\n        \"arm\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-arm64-gnu\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz\",\n      \"integrity\": \"sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-arm64-musl\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz\",\n      \"integrity\": \"sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-loong64-gnu\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz\",\n      \"integrity\": \"sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==\",\n      \"cpu\": [\n        \"loong64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-loong64-musl\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz\",\n      \"integrity\": \"sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==\",\n      \"cpu\": [\n        \"loong64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-ppc64-gnu\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz\",\n      \"integrity\": \"sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-ppc64-musl\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz\",\n      \"integrity\": \"sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==\",\n      \"cpu\": [\n        \"ppc64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-riscv64-gnu\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz\",\n      \"integrity\": \"sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-riscv64-musl\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz\",\n      \"integrity\": \"sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==\",\n      \"cpu\": [\n        \"riscv64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-s390x-gnu\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz\",\n      \"integrity\": \"sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==\",\n      \"cpu\": [\n        \"s390x\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-x64-gnu\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz\",\n      \"integrity\": \"sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-linux-x64-musl\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz\",\n      \"integrity\": \"sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"linux\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-openbsd-x64\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz\",\n      \"integrity\": \"sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openbsd\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-openharmony-arm64\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz\",\n      \"integrity\": \"sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"openharmony\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-win32-arm64-msvc\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz\",\n      \"integrity\": \"sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==\",\n      \"cpu\": [\n        \"arm64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-win32-ia32-msvc\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz\",\n      \"integrity\": \"sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==\",\n      \"cpu\": [\n        \"ia32\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-win32-x64-gnu\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz\",\n      \"integrity\": \"sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ]\n    },\n    \"node_modules/@rollup/rollup-win32-x64-msvc\": {\n      \"version\": \"4.59.0\",\n      \"resolved\": \"https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz\",\n      \"integrity\": \"sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==\",\n      \"cpu\": [\n        \"x64\"\n      ],\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"win32\"\n      ]\n    },\n    \"node_modules/@types/estree\": {\n      \"version\": \"1.0.8\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/json-schema\": {\n      \"version\": \"7.0.15\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/@types/node\": {\n      \"version\": \"20.19.34\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"undici-types\": \"~6.21.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/regexpp\": \"^4.12.2\",\n        \"@typescript-eslint/scope-manager\": \"8.56.1\",\n        \"@typescript-eslint/type-utils\": \"8.56.1\",\n        \"@typescript-eslint/utils\": \"8.56.1\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.1\",\n        \"ignore\": \"^7.0.5\",\n        \"natural-compare\": \"^1.4.0\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"@typescript-eslint/parser\": \"^8.56.1\",\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore\": {\n      \"version\": \"7.0.5\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/@typescript-eslint/parser\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/scope-manager\": \"8.56.1\",\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.1\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.1\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/project-service\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/tsconfig-utils\": \"^8.56.1\",\n        \"@typescript-eslint/types\": \"^8.56.1\",\n        \"debug\": \"^4.4.3\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/scope-manager\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/tsconfig-utils\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/type-utils\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.1\",\n        \"@typescript-eslint/utils\": \"8.56.1\",\n        \"debug\": \"^4.4.3\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/types\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/project-service\": \"8.56.1\",\n        \"@typescript-eslint/tsconfig-utils\": \"8.56.1\",\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"@typescript-eslint/visitor-keys\": \"8.56.1\",\n        \"debug\": \"^4.4.3\",\n        \"minimatch\": \"^10.2.2\",\n        \"semver\": \"^7.7.3\",\n        \"tinyglobby\": \"^0.2.15\",\n        \"ts-api-utils\": \"^2.4.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion\": {\n      \"version\": \"5.0.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/@typescript-eslint/utils\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.9.1\",\n        \"@typescript-eslint/scope-manager\": \"8.56.1\",\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/@typescript-eslint/visitor-keys\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/types\": \"8.56.1\",\n        \"eslint-visitor-keys\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      }\n    },\n    \"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys\": {\n      \"version\": \"5.0.1\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^20.19.0 || ^22.13.0 || >=24\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/@vitest/coverage-v8\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@ampproject/remapping\": \"^2.3.0\",\n        \"@bcoe/v8-coverage\": \"^0.2.3\",\n        \"debug\": \"^4.3.7\",\n        \"istanbul-lib-coverage\": \"^3.2.2\",\n        \"istanbul-lib-report\": \"^3.0.1\",\n        \"istanbul-lib-source-maps\": \"^5.0.6\",\n        \"istanbul-reports\": \"^3.1.7\",\n        \"magic-string\": \"^0.30.12\",\n        \"magicast\": \"^0.3.5\",\n        \"std-env\": \"^3.8.0\",\n        \"test-exclude\": \"^7.0.1\",\n        \"tinyrainbow\": \"^1.2.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      },\n      \"peerDependencies\": {\n        \"@vitest/browser\": \"2.1.9\",\n        \"vitest\": \"2.1.9\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@vitest/browser\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@vitest/expect\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/spy\": \"2.1.9\",\n        \"@vitest/utils\": \"2.1.9\",\n        \"chai\": \"^5.1.2\",\n        \"tinyrainbow\": \"^1.2.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/mocker\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/spy\": \"2.1.9\",\n        \"estree-walker\": \"^3.0.3\",\n        \"magic-string\": \"^0.30.12\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      },\n      \"peerDependencies\": {\n        \"msw\": \"^2.4.9\",\n        \"vite\": \"^5.0.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"msw\": {\n          \"optional\": true\n        },\n        \"vite\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/@vitest/pretty-format\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tinyrainbow\": \"^1.2.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/runner\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/utils\": \"2.1.9\",\n        \"pathe\": \"^1.1.2\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/snapshot\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/pretty-format\": \"2.1.9\",\n        \"magic-string\": \"^0.30.12\",\n        \"pathe\": \"^1.1.2\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/spy\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"tinyspy\": \"^3.0.2\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/@vitest/ui\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/utils\": \"2.1.9\",\n        \"fflate\": \"^0.8.2\",\n        \"flatted\": \"^3.3.1\",\n        \"pathe\": \"^1.1.2\",\n        \"sirv\": \"^3.0.0\",\n        \"tinyglobby\": \"^0.2.10\",\n        \"tinyrainbow\": \"^1.2.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      },\n      \"peerDependencies\": {\n        \"vitest\": \"2.1.9\"\n      }\n    },\n    \"node_modules/@vitest/utils\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/pretty-format\": \"2.1.9\",\n        \"loupe\": \"^3.1.2\",\n        \"tinyrainbow\": \"^1.2.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/acorn\": {\n      \"version\": \"8.16.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"acorn\": \"bin/acorn\"\n      },\n      \"engines\": {\n        \"node\": \">=0.4.0\"\n      }\n    },\n    \"node_modules/acorn-jsx\": {\n      \"version\": \"5.3.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"peerDependencies\": {\n        \"acorn\": \"^6.0.0 || ^7.0.0 || ^8.0.0\"\n      }\n    },\n    \"node_modules/ajv\": {\n      \"version\": \"6.14.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fast-deep-equal\": \"^3.1.1\",\n        \"fast-json-stable-stringify\": \"^2.0.0\",\n        \"json-schema-traverse\": \"^0.4.1\",\n        \"uri-js\": \"^4.2.2\"\n      },\n      \"funding\": {\n        \"type\": \"github\",\n        \"url\": \"https://github.com/sponsors/epoberezkin\"\n      }\n    },\n    \"node_modules/ansi-escapes\": {\n      \"version\": \"7.3.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"environment\": \"^1.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/ansi-regex\": {\n      \"version\": \"6.2.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-regex?sponsor=1\"\n      }\n    },\n    \"node_modules/ansi-styles\": {\n      \"version\": \"4.3.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"color-convert\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/argparse\": {\n      \"version\": \"2.0.1\",\n      \"dev\": true,\n      \"license\": \"Python-2.0\"\n    },\n    \"node_modules/assertion-error\": {\n      \"version\": \"2.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      }\n    },\n    \"node_modules/balanced-match\": {\n      \"version\": \"1.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/brace-expansion\": {\n      \"version\": \"1.1.12\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^1.0.0\",\n        \"concat-map\": \"0.0.1\"\n      }\n    },\n    \"node_modules/braces\": {\n      \"version\": \"3.0.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fill-range\": \"^7.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/cac\": {\n      \"version\": \"6.7.14\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/callsites\": {\n      \"version\": \"3.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/chai\": {\n      \"version\": \"5.3.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"assertion-error\": \"^2.0.1\",\n        \"check-error\": \"^2.1.1\",\n        \"deep-eql\": \"^5.0.1\",\n        \"loupe\": \"^3.1.0\",\n        \"pathval\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/chalk\": {\n      \"version\": \"4.1.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^4.1.0\",\n        \"supports-color\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/check-error\": {\n      \"version\": \"2.1.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 16\"\n      }\n    },\n    \"node_modules/cli-cursor\": {\n      \"version\": \"5.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"restore-cursor\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/cli-truncate\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"slice-ansi\": \"^5.0.0\",\n        \"string-width\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/color-convert\": {\n      \"version\": \"2.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"color-name\": \"~1.1.4\"\n      },\n      \"engines\": {\n        \"node\": \">=7.0.0\"\n      }\n    },\n    \"node_modules/color-name\": {\n      \"version\": \"1.1.4\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/colorette\": {\n      \"version\": \"2.0.20\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/commander\": {\n      \"version\": \"13.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/concat-map\": {\n      \"version\": \"0.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/cross-spawn\": {\n      \"version\": \"7.0.6\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"path-key\": \"^3.1.0\",\n        \"shebang-command\": \"^2.0.0\",\n        \"which\": \"^2.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/debug\": {\n      \"version\": \"4.4.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ms\": \"^2.1.3\"\n      },\n      \"engines\": {\n        \"node\": \">=6.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"supports-color\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/deep-eql\": {\n      \"version\": \"5.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/deep-is\": {\n      \"version\": \"0.1.4\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/dotenv\": {\n      \"version\": \"16.6.1\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://dotenvx.com\"\n      }\n    },\n    \"node_modules/eastasianwidth\": {\n      \"version\": \"0.2.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/emoji-regex\": {\n      \"version\": \"10.6.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/environment\": {\n      \"version\": \"1.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/es-module-lexer\": {\n      \"version\": \"1.7.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/esbuild\": {\n      \"version\": \"0.21.5\",\n      \"dev\": true,\n      \"hasInstallScript\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"esbuild\": \"bin/esbuild\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"optionalDependencies\": {\n        \"@esbuild/aix-ppc64\": \"0.21.5\",\n        \"@esbuild/android-arm\": \"0.21.5\",\n        \"@esbuild/android-arm64\": \"0.21.5\",\n        \"@esbuild/android-x64\": \"0.21.5\",\n        \"@esbuild/darwin-arm64\": \"0.21.5\",\n        \"@esbuild/darwin-x64\": \"0.21.5\",\n        \"@esbuild/freebsd-arm64\": \"0.21.5\",\n        \"@esbuild/freebsd-x64\": \"0.21.5\",\n        \"@esbuild/linux-arm\": \"0.21.5\",\n        \"@esbuild/linux-arm64\": \"0.21.5\",\n        \"@esbuild/linux-ia32\": \"0.21.5\",\n        \"@esbuild/linux-loong64\": \"0.21.5\",\n        \"@esbuild/linux-mips64el\": \"0.21.5\",\n        \"@esbuild/linux-ppc64\": \"0.21.5\",\n        \"@esbuild/linux-riscv64\": \"0.21.5\",\n        \"@esbuild/linux-s390x\": \"0.21.5\",\n        \"@esbuild/linux-x64\": \"0.21.5\",\n        \"@esbuild/netbsd-x64\": \"0.21.5\",\n        \"@esbuild/openbsd-x64\": \"0.21.5\",\n        \"@esbuild/sunos-x64\": \"0.21.5\",\n        \"@esbuild/win32-arm64\": \"0.21.5\",\n        \"@esbuild/win32-ia32\": \"0.21.5\",\n        \"@esbuild/win32-x64\": \"0.21.5\"\n      }\n    },\n    \"node_modules/escape-string-regexp\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/eslint\": {\n      \"version\": \"9.39.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@eslint-community/eslint-utils\": \"^4.8.0\",\n        \"@eslint-community/regexpp\": \"^4.12.1\",\n        \"@eslint/config-array\": \"^0.21.1\",\n        \"@eslint/config-helpers\": \"^0.4.2\",\n        \"@eslint/core\": \"^0.17.0\",\n        \"@eslint/eslintrc\": \"^3.3.1\",\n        \"@eslint/js\": \"9.39.3\",\n        \"@eslint/plugin-kit\": \"^0.4.1\",\n        \"@humanfs/node\": \"^0.16.6\",\n        \"@humanwhocodes/module-importer\": \"^1.0.1\",\n        \"@humanwhocodes/retry\": \"^0.4.2\",\n        \"@types/estree\": \"^1.0.6\",\n        \"ajv\": \"^6.12.4\",\n        \"chalk\": \"^4.0.0\",\n        \"cross-spawn\": \"^7.0.6\",\n        \"debug\": \"^4.3.2\",\n        \"escape-string-regexp\": \"^4.0.0\",\n        \"eslint-scope\": \"^8.4.0\",\n        \"eslint-visitor-keys\": \"^4.2.1\",\n        \"espree\": \"^10.4.0\",\n        \"esquery\": \"^1.5.0\",\n        \"esutils\": \"^2.0.2\",\n        \"fast-deep-equal\": \"^3.1.3\",\n        \"file-entry-cache\": \"^8.0.0\",\n        \"find-up\": \"^5.0.0\",\n        \"glob-parent\": \"^6.0.2\",\n        \"ignore\": \"^5.2.0\",\n        \"imurmurhash\": \"^0.1.4\",\n        \"is-glob\": \"^4.0.0\",\n        \"json-stable-stringify-without-jsonify\": \"^1.0.1\",\n        \"lodash.merge\": \"^4.6.2\",\n        \"minimatch\": \"^3.1.2\",\n        \"natural-compare\": \"^1.4.0\",\n        \"optionator\": \"^0.9.3\"\n      },\n      \"bin\": {\n        \"eslint\": \"bin/eslint.js\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://eslint.org/donate\"\n      },\n      \"peerDependencies\": {\n        \"jiti\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"jiti\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/eslint-config-prettier\": {\n      \"version\": \"10.1.8\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"eslint-config-prettier\": \"bin/cli.js\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint-config-prettier\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \">=7.0.0\"\n      }\n    },\n    \"node_modules/eslint-plugin-tsdoc\": {\n      \"version\": \"0.5.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@microsoft/tsdoc\": \"0.16.0\",\n        \"@microsoft/tsdoc-config\": \"0.18.1\",\n        \"@typescript-eslint/utils\": \"~8.56.0\"\n      }\n    },\n    \"node_modules/eslint-scope\": {\n      \"version\": \"8.4.0\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"esrecurse\": \"^4.3.0\",\n        \"estraverse\": \"^5.2.0\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/eslint-visitor-keys\": {\n      \"version\": \"4.2.1\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/espree\": {\n      \"version\": \"10.4.0\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"acorn\": \"^8.15.0\",\n        \"acorn-jsx\": \"^5.3.2\",\n        \"eslint-visitor-keys\": \"^4.2.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/eslint\"\n      }\n    },\n    \"node_modules/esquery\": {\n      \"version\": \"1.7.0\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"estraverse\": \"^5.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/esrecurse\": {\n      \"version\": \"4.3.0\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"estraverse\": \"^5.2.0\"\n      },\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/estraverse\": {\n      \"version\": \"5.3.0\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=4.0\"\n      }\n    },\n    \"node_modules/estree-walker\": {\n      \"version\": \"3.0.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/estree\": \"^1.0.0\"\n      }\n    },\n    \"node_modules/esutils\": {\n      \"version\": \"2.0.3\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/eventemitter3\": {\n      \"version\": \"5.0.4\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/execa\": {\n      \"version\": \"8.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"cross-spawn\": \"^7.0.3\",\n        \"get-stream\": \"^8.0.1\",\n        \"human-signals\": \"^5.0.0\",\n        \"is-stream\": \"^3.0.0\",\n        \"merge-stream\": \"^2.0.0\",\n        \"npm-run-path\": \"^5.1.0\",\n        \"onetime\": \"^6.0.0\",\n        \"signal-exit\": \"^4.1.0\",\n        \"strip-final-newline\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16.17\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sindresorhus/execa?sponsor=1\"\n      }\n    },\n    \"node_modules/expect-type\": {\n      \"version\": \"1.3.0\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      }\n    },\n    \"node_modules/fast-deep-equal\": {\n      \"version\": \"3.1.3\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-json-stable-stringify\": {\n      \"version\": \"2.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-levenshtein\": {\n      \"version\": \"2.0.6\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/fast-uri\": {\n      \"version\": \"3.1.0\",\n      \"dev\": true,\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/fastify\"\n        },\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/fastify\"\n        }\n      ],\n      \"license\": \"BSD-3-Clause\"\n    },\n    \"node_modules/fflate\": {\n      \"version\": \"0.8.2\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/file-entry-cache\": {\n      \"version\": \"8.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"flat-cache\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16.0.0\"\n      }\n    },\n    \"node_modules/fill-range\": {\n      \"version\": \"7.1.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"to-regex-range\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/find-up\": {\n      \"version\": \"5.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"locate-path\": \"^6.0.0\",\n        \"path-exists\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/flat-cache\": {\n      \"version\": \"4.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"flatted\": \"^3.2.9\",\n        \"keyv\": \"^4.5.4\"\n      },\n      \"engines\": {\n        \"node\": \">=16\"\n      }\n    },\n    \"node_modules/flatted\": {\n      \"version\": \"3.3.3\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/foreground-child\": {\n      \"version\": \"3.3.1\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"cross-spawn\": \"^7.0.6\",\n        \"signal-exit\": \"^4.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/fsevents\": {\n      \"version\": \"2.3.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"optional\": true,\n      \"os\": [\n        \"darwin\"\n      ],\n      \"engines\": {\n        \"node\": \"^8.16.0 || ^10.6.0 || >=11.0.0\"\n      }\n    },\n    \"node_modules/function-bind\": {\n      \"version\": \"1.1.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/get-east-asian-width\": {\n      \"version\": \"1.5.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/get-stream\": {\n      \"version\": \"8.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=16\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/glob\": {\n      \"version\": \"10.5.0\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"foreground-child\": \"^3.1.0\",\n        \"jackspeak\": \"^3.1.2\",\n        \"minimatch\": \"^9.0.4\",\n        \"minipass\": \"^7.1.2\",\n        \"package-json-from-dist\": \"^1.0.0\",\n        \"path-scurry\": \"^1.11.1\"\n      },\n      \"bin\": {\n        \"glob\": \"dist/esm/bin.mjs\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/glob-parent\": {\n      \"version\": \"6.0.2\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"is-glob\": \"^4.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">=10.13.0\"\n      }\n    },\n    \"node_modules/glob/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/glob/node_modules/brace-expansion\": {\n      \"version\": \"5.0.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/glob/node_modules/minimatch\": {\n      \"version\": \"9.0.8\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=16 || 14 >=14.17\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/globals\": {\n      \"version\": \"14.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/has-flag\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/hasown\": {\n      \"version\": \"2.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"function-bind\": \"^1.1.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      }\n    },\n    \"node_modules/html-escaper\": {\n      \"version\": \"2.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/human-signals\": {\n      \"version\": \"5.0.0\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"engines\": {\n        \"node\": \">=16.17.0\"\n      }\n    },\n    \"node_modules/ignore\": {\n      \"version\": \"5.3.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 4\"\n      }\n    },\n    \"node_modules/import-fresh\": {\n      \"version\": \"3.3.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"parent-module\": \"^1.0.0\",\n        \"resolve-from\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/imurmurhash\": {\n      \"version\": \"0.1.4\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.8.19\"\n      }\n    },\n    \"node_modules/is-core-module\": {\n      \"version\": \"2.16.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"hasown\": \"^2.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/is-extglob\": {\n      \"version\": \"2.1.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is-fullwidth-code-point\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/is-glob\": {\n      \"version\": \"4.0.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-extglob\": \"^2.1.1\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/is-number\": {\n      \"version\": \"7.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.12.0\"\n      }\n    },\n    \"node_modules/is-stream\": {\n      \"version\": \"3.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^12.20.0 || ^14.13.1 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/isexe\": {\n      \"version\": \"2.0.0\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/istanbul-lib-coverage\": {\n      \"version\": \"3.2.2\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/istanbul-lib-report\": {\n      \"version\": \"3.0.1\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"istanbul-lib-coverage\": \"^3.0.0\",\n        \"make-dir\": \"^4.0.0\",\n        \"supports-color\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/istanbul-lib-source-maps\": {\n      \"version\": \"5.0.6\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"@jridgewell/trace-mapping\": \"^0.3.23\",\n        \"debug\": \"^4.1.1\",\n        \"istanbul-lib-coverage\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/istanbul-reports\": {\n      \"version\": \"3.2.0\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"dependencies\": {\n        \"html-escaper\": \"^2.0.0\",\n        \"istanbul-lib-report\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/jackspeak\": {\n      \"version\": \"3.4.3\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"@isaacs/cliui\": \"^8.0.2\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      },\n      \"optionalDependencies\": {\n        \"@pkgjs/parseargs\": \"^0.11.0\"\n      }\n    },\n    \"node_modules/jju\": {\n      \"version\": \"1.4.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/js-yaml\": {\n      \"version\": \"4.1.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"argparse\": \"^2.0.1\"\n      },\n      \"bin\": {\n        \"js-yaml\": \"bin/js-yaml.js\"\n      }\n    },\n    \"node_modules/json-buffer\": {\n      \"version\": \"3.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/json-schema-to-ts\": {\n      \"version\": \"3.1.1\",\n      \"devOptional\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/runtime\": \"^7.18.3\",\n        \"ts-algebra\": \"^2.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16\"\n      }\n    },\n    \"node_modules/json-schema-traverse\": {\n      \"version\": \"0.4.1\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/json-stable-stringify-without-jsonify\": {\n      \"version\": \"1.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/keyv\": {\n      \"version\": \"4.5.4\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"json-buffer\": \"3.0.1\"\n      }\n    },\n    \"node_modules/levn\": {\n      \"version\": \"0.4.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prelude-ls\": \"^1.2.1\",\n        \"type-check\": \"~0.4.0\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/lilconfig\": {\n      \"version\": \"3.1.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/antonk52\"\n      }\n    },\n    \"node_modules/lint-staged\": {\n      \"version\": \"15.5.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"chalk\": \"^5.4.1\",\n        \"commander\": \"^13.1.0\",\n        \"debug\": \"^4.4.0\",\n        \"execa\": \"^8.0.1\",\n        \"lilconfig\": \"^3.1.3\",\n        \"listr2\": \"^8.2.5\",\n        \"micromatch\": \"^4.0.8\",\n        \"pidtree\": \"^0.6.0\",\n        \"string-argv\": \"^0.3.2\",\n        \"yaml\": \"^2.7.0\"\n      },\n      \"bin\": {\n        \"lint-staged\": \"bin/lint-staged.js\"\n      },\n      \"engines\": {\n        \"node\": \">=18.12.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/lint-staged\"\n      }\n    },\n    \"node_modules/lint-staged/node_modules/chalk\": {\n      \"version\": \"5.6.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^12.17.0 || ^14.13 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/chalk?sponsor=1\"\n      }\n    },\n    \"node_modules/listr2\": {\n      \"version\": \"8.3.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"cli-truncate\": \"^4.0.0\",\n        \"colorette\": \"^2.0.20\",\n        \"eventemitter3\": \"^5.0.1\",\n        \"log-update\": \"^6.1.0\",\n        \"rfdc\": \"^1.4.1\",\n        \"wrap-ansi\": \"^9.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\"\n      }\n    },\n    \"node_modules/locate-path\": {\n      \"version\": \"6.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"p-locate\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/lodash.merge\": {\n      \"version\": \"4.6.2\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/log-update\": {\n      \"version\": \"6.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-escapes\": \"^7.0.0\",\n        \"cli-cursor\": \"^5.0.0\",\n        \"slice-ansi\": \"^7.1.0\",\n        \"strip-ansi\": \"^7.1.0\",\n        \"wrap-ansi\": \"^9.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/log-update/node_modules/ansi-styles\": {\n      \"version\": \"6.2.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/log-update/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"5.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"get-east-asian-width\": \"^1.3.1\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/log-update/node_modules/slice-ansi\": {\n      \"version\": \"7.1.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^6.2.1\",\n        \"is-fullwidth-code-point\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/slice-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/loupe\": {\n      \"version\": \"3.2.1\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/lru-cache\": {\n      \"version\": \"10.4.3\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/magic-string\": {\n      \"version\": \"0.30.21\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@jridgewell/sourcemap-codec\": \"^1.5.5\"\n      }\n    },\n    \"node_modules/magicast\": {\n      \"version\": \"0.3.5\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@babel/parser\": \"^7.25.4\",\n        \"@babel/types\": \"^7.25.4\",\n        \"source-map-js\": \"^1.2.0\"\n      }\n    },\n    \"node_modules/make-dir\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"semver\": \"^7.5.3\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/merge-stream\": {\n      \"version\": \"2.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/micromatch\": {\n      \"version\": \"4.0.8\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"braces\": \"^3.0.3\",\n        \"picomatch\": \"^2.3.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8.6\"\n      }\n    },\n    \"node_modules/mimic-fn\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/mimic-function\": {\n      \"version\": \"5.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/minimatch\": {\n      \"version\": \"3.1.5\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^1.1.7\"\n      },\n      \"engines\": {\n        \"node\": \"*\"\n      }\n    },\n    \"node_modules/minipass\": {\n      \"version\": \"7.1.3\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"engines\": {\n        \"node\": \">=16 || 14 >=14.17\"\n      }\n    },\n    \"node_modules/mrmime\": {\n      \"version\": \"2.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/ms\": {\n      \"version\": \"2.1.3\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/nanoid\": {\n      \"version\": \"3.3.11\",\n      \"dev\": true,\n      \"funding\": [\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"nanoid\": \"bin/nanoid.cjs\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\"\n      }\n    },\n    \"node_modules/natural-compare\": {\n      \"version\": \"1.4.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/npm-run-path\": {\n      \"version\": \"5.3.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"path-key\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \"^12.20.0 || ^14.13.1 || >=16.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/npm-run-path/node_modules/path-key\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/onetime\": {\n      \"version\": \"6.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"mimic-fn\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/openai\": {\n      \"version\": \"6.25.0\",\n      \"devOptional\": true,\n      \"license\": \"Apache-2.0\",\n      \"bin\": {\n        \"openai\": \"bin/cli\"\n      },\n      \"peerDependencies\": {\n        \"ws\": \"^8.18.0\",\n        \"zod\": \"^3.25 || ^4.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"ws\": {\n          \"optional\": true\n        },\n        \"zod\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/optionator\": {\n      \"version\": \"0.9.4\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"deep-is\": \"^0.1.3\",\n        \"fast-levenshtein\": \"^2.0.6\",\n        \"levn\": \"^0.4.1\",\n        \"prelude-ls\": \"^1.2.1\",\n        \"type-check\": \"^0.4.0\",\n        \"word-wrap\": \"^1.2.5\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/p-limit\": {\n      \"version\": \"3.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"yocto-queue\": \"^0.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/p-locate\": {\n      \"version\": \"5.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"p-limit\": \"^3.0.2\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/package-json-from-dist\": {\n      \"version\": \"1.0.1\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\"\n    },\n    \"node_modules/parent-module\": {\n      \"version\": \"1.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"callsites\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/path-exists\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/path-key\": {\n      \"version\": \"3.1.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/path-parse\": {\n      \"version\": \"1.0.7\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/path-scurry\": {\n      \"version\": \"1.11.1\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"lru-cache\": \"^10.2.0\",\n        \"minipass\": \"^5.0.0 || ^6.0.2 || ^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=16 || 14 >=14.18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/pathe\": {\n      \"version\": \"1.1.2\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/pathval\": {\n      \"version\": \"2.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 14.16\"\n      }\n    },\n    \"node_modules/picocolors\": {\n      \"version\": \"1.1.1\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/picomatch\": {\n      \"version\": \"2.3.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8.6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/jonschlinkert\"\n      }\n    },\n    \"node_modules/pidtree\": {\n      \"version\": \"0.6.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"pidtree\": \"bin/pidtree.js\"\n      },\n      \"engines\": {\n        \"node\": \">=0.10\"\n      }\n    },\n    \"node_modules/postcss\": {\n      \"version\": \"8.5.6\",\n      \"dev\": true,\n      \"funding\": [\n        {\n          \"type\": \"opencollective\",\n          \"url\": \"https://opencollective.com/postcss/\"\n        },\n        {\n          \"type\": \"tidelift\",\n          \"url\": \"https://tidelift.com/funding/github/npm/postcss\"\n        },\n        {\n          \"type\": \"github\",\n          \"url\": \"https://github.com/sponsors/ai\"\n        }\n      ],\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"nanoid\": \"^3.3.11\",\n        \"picocolors\": \"^1.1.1\",\n        \"source-map-js\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \"^10 || ^12 || >=14\"\n      }\n    },\n    \"node_modules/prelude-ls\": {\n      \"version\": \"1.2.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/prettier\": {\n      \"version\": \"3.8.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"bin\": {\n        \"prettier\": \"bin/prettier.cjs\"\n      },\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/prettier/prettier?sponsor=1\"\n      }\n    },\n    \"node_modules/punycode\": {\n      \"version\": \"2.3.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/require-from-string\": {\n      \"version\": \"2.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/resolve\": {\n      \"version\": \"1.22.11\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-core-module\": \"^2.16.1\",\n        \"path-parse\": \"^1.0.7\",\n        \"supports-preserve-symlinks-flag\": \"^1.0.0\"\n      },\n      \"bin\": {\n        \"resolve\": \"bin/resolve\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/resolve-from\": {\n      \"version\": \"4.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=4\"\n      }\n    },\n    \"node_modules/restore-cursor\": {\n      \"version\": \"5.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"onetime\": \"^7.0.0\",\n        \"signal-exit\": \"^4.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/restore-cursor/node_modules/onetime\": {\n      \"version\": \"7.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"mimic-function\": \"^5.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/rfdc\": {\n      \"version\": \"1.4.1\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/rollup\": {\n      \"version\": \"4.59.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@types/estree\": \"1.0.8\"\n      },\n      \"bin\": {\n        \"rollup\": \"dist/bin/rollup\"\n      },\n      \"engines\": {\n        \"node\": \">=18.0.0\",\n        \"npm\": \">=8.0.0\"\n      },\n      \"optionalDependencies\": {\n        \"@rollup/rollup-android-arm-eabi\": \"4.59.0\",\n        \"@rollup/rollup-android-arm64\": \"4.59.0\",\n        \"@rollup/rollup-darwin-arm64\": \"4.59.0\",\n        \"@rollup/rollup-darwin-x64\": \"4.59.0\",\n        \"@rollup/rollup-freebsd-arm64\": \"4.59.0\",\n        \"@rollup/rollup-freebsd-x64\": \"4.59.0\",\n        \"@rollup/rollup-linux-arm-gnueabihf\": \"4.59.0\",\n        \"@rollup/rollup-linux-arm-musleabihf\": \"4.59.0\",\n        \"@rollup/rollup-linux-arm64-gnu\": \"4.59.0\",\n        \"@rollup/rollup-linux-arm64-musl\": \"4.59.0\",\n        \"@rollup/rollup-linux-loong64-gnu\": \"4.59.0\",\n        \"@rollup/rollup-linux-loong64-musl\": \"4.59.0\",\n        \"@rollup/rollup-linux-ppc64-gnu\": \"4.59.0\",\n        \"@rollup/rollup-linux-ppc64-musl\": \"4.59.0\",\n        \"@rollup/rollup-linux-riscv64-gnu\": \"4.59.0\",\n        \"@rollup/rollup-linux-riscv64-musl\": \"4.59.0\",\n        \"@rollup/rollup-linux-s390x-gnu\": \"4.59.0\",\n        \"@rollup/rollup-linux-x64-gnu\": \"4.59.0\",\n        \"@rollup/rollup-linux-x64-musl\": \"4.59.0\",\n        \"@rollup/rollup-openbsd-x64\": \"4.59.0\",\n        \"@rollup/rollup-openharmony-arm64\": \"4.59.0\",\n        \"@rollup/rollup-win32-arm64-msvc\": \"4.59.0\",\n        \"@rollup/rollup-win32-ia32-msvc\": \"4.59.0\",\n        \"@rollup/rollup-win32-x64-gnu\": \"4.59.0\",\n        \"@rollup/rollup-win32-x64-msvc\": \"4.59.0\",\n        \"fsevents\": \"~2.3.2\"\n      }\n    },\n    \"node_modules/semver\": {\n      \"version\": \"7.7.4\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"semver\": \"bin/semver.js\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      }\n    },\n    \"node_modules/shebang-command\": {\n      \"version\": \"2.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"shebang-regex\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/shebang-regex\": {\n      \"version\": \"3.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/siginfo\": {\n      \"version\": \"2.0.0\",\n      \"dev\": true,\n      \"license\": \"ISC\"\n    },\n    \"node_modules/signal-exit\": {\n      \"version\": \"4.1.0\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"engines\": {\n        \"node\": \">=14\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/sirv\": {\n      \"version\": \"3.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@polka/url\": \"^1.0.0-next.24\",\n        \"mrmime\": \"^2.0.0\",\n        \"totalist\": \"^3.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/slice-ansi\": {\n      \"version\": \"5.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^6.0.0\",\n        \"is-fullwidth-code-point\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/slice-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/slice-ansi/node_modules/ansi-styles\": {\n      \"version\": \"6.2.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/source-map-js\": {\n      \"version\": \"1.2.1\",\n      \"dev\": true,\n      \"license\": \"BSD-3-Clause\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/stackback\": {\n      \"version\": \"0.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/std-env\": {\n      \"version\": \"3.10.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/string-argv\": {\n      \"version\": \"0.3.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.6.19\"\n      }\n    },\n    \"node_modules/string-width\": {\n      \"version\": \"7.2.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"emoji-regex\": \"^10.3.0\",\n        \"get-east-asian-width\": \"^1.0.0\",\n        \"strip-ansi\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/string-width-cjs\": {\n      \"name\": \"string-width\",\n      \"version\": \"4.2.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string-width-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string-width-cjs/node_modules/emoji-regex\": {\n      \"version\": \"8.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/string-width-cjs/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/string-width-cjs/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/strip-ansi\": {\n      \"version\": \"7.2.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^6.2.2\"\n      },\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/strip-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/strip-ansi-cjs\": {\n      \"name\": \"strip-ansi\",\n      \"version\": \"6.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/strip-ansi-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/strip-final-newline\": {\n      \"version\": \"3.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/strip-json-comments\": {\n      \"version\": \"3.1.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    },\n    \"node_modules/supports-color\": {\n      \"version\": \"7.2.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"has-flag\": \"^4.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/supports-preserve-symlinks-flag\": {\n      \"version\": \"1.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">= 0.4\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/ljharb\"\n      }\n    },\n    \"node_modules/test-exclude\": {\n      \"version\": \"7.0.2\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"@istanbuljs/schema\": \"^0.1.2\",\n        \"glob\": \"^10.4.1\",\n        \"minimatch\": \"^10.2.2\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      }\n    },\n    \"node_modules/test-exclude/node_modules/balanced-match\": {\n      \"version\": \"4.0.4\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/test-exclude/node_modules/brace-expansion\": {\n      \"version\": \"5.0.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"balanced-match\": \"^4.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      }\n    },\n    \"node_modules/test-exclude/node_modules/minimatch\": {\n      \"version\": \"10.2.4\",\n      \"dev\": true,\n      \"license\": \"BlueOak-1.0.0\",\n      \"dependencies\": {\n        \"brace-expansion\": \"^5.0.2\"\n      },\n      \"engines\": {\n        \"node\": \"18 || 20 || >=22\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/isaacs\"\n      }\n    },\n    \"node_modules/tinybench\": {\n      \"version\": \"2.9.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/tinyexec\": {\n      \"version\": \"0.3.2\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/tinyglobby\": {\n      \"version\": \"0.2.15\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"fdir\": \"^6.5.0\",\n        \"picomatch\": \"^4.0.3\"\n      },\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/SuperchupuDev\"\n      }\n    },\n    \"node_modules/tinyglobby/node_modules/fdir\": {\n      \"version\": \"6.5.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12.0.0\"\n      },\n      \"peerDependencies\": {\n        \"picomatch\": \"^3 || ^4\"\n      },\n      \"peerDependenciesMeta\": {\n        \"picomatch\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/tinyglobby/node_modules/picomatch\": {\n      \"version\": \"4.0.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/jonschlinkert\"\n      }\n    },\n    \"node_modules/tinypool\": {\n      \"version\": \"1.1.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \"^18.0.0 || >=20.0.0\"\n      }\n    },\n    \"node_modules/tinyrainbow\": {\n      \"version\": \"1.2.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/tinyspy\": {\n      \"version\": \"3.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=14.0.0\"\n      }\n    },\n    \"node_modules/to-regex-range\": {\n      \"version\": \"5.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"is-number\": \"^7.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=8.0\"\n      }\n    },\n    \"node_modules/totalist\": {\n      \"version\": \"3.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=6\"\n      }\n    },\n    \"node_modules/ts-algebra\": {\n      \"version\": \"2.0.0\",\n      \"devOptional\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/ts-api-utils\": {\n      \"version\": \"2.4.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=18.12\"\n      },\n      \"peerDependencies\": {\n        \"typescript\": \">=4.8.4\"\n      }\n    },\n    \"node_modules/type-check\": {\n      \"version\": \"0.4.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"prelude-ls\": \"^1.2.1\"\n      },\n      \"engines\": {\n        \"node\": \">= 0.8.0\"\n      }\n    },\n    \"node_modules/typescript\": {\n      \"version\": \"5.9.3\",\n      \"dev\": true,\n      \"license\": \"Apache-2.0\",\n      \"bin\": {\n        \"tsc\": \"bin/tsc\",\n        \"tsserver\": \"bin/tsserver\"\n      },\n      \"engines\": {\n        \"node\": \">=14.17\"\n      }\n    },\n    \"node_modules/typescript-eslint\": {\n      \"version\": \"8.56.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@typescript-eslint/eslint-plugin\": \"8.56.1\",\n        \"@typescript-eslint/parser\": \"8.56.1\",\n        \"@typescript-eslint/typescript-estree\": \"8.56.1\",\n        \"@typescript-eslint/utils\": \"8.56.1\"\n      },\n      \"engines\": {\n        \"node\": \"^18.18.0 || ^20.9.0 || >=21.1.0\"\n      },\n      \"funding\": {\n        \"type\": \"opencollective\",\n        \"url\": \"https://opencollective.com/typescript-eslint\"\n      },\n      \"peerDependencies\": {\n        \"eslint\": \"^8.57.0 || ^9.0.0 || ^10.0.0\",\n        \"typescript\": \">=4.8.4 <6.0.0\"\n      }\n    },\n    \"node_modules/undici-types\": {\n      \"version\": \"6.21.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/uri-js\": {\n      \"version\": \"4.4.1\",\n      \"dev\": true,\n      \"license\": \"BSD-2-Clause\",\n      \"dependencies\": {\n        \"punycode\": \"^2.1.0\"\n      }\n    },\n    \"node_modules/vite\": {\n      \"version\": \"5.4.21\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"esbuild\": \"^0.21.3\",\n        \"postcss\": \"^8.4.43\",\n        \"rollup\": \"^4.20.0\"\n      },\n      \"bin\": {\n        \"vite\": \"bin/vite.js\"\n      },\n      \"engines\": {\n        \"node\": \"^18.0.0 || >=20.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/vitejs/vite?sponsor=1\"\n      },\n      \"optionalDependencies\": {\n        \"fsevents\": \"~2.3.3\"\n      },\n      \"peerDependencies\": {\n        \"@types/node\": \"^18.0.0 || >=20.0.0\",\n        \"less\": \"*\",\n        \"lightningcss\": \"^1.21.0\",\n        \"sass\": \"*\",\n        \"sass-embedded\": \"*\",\n        \"stylus\": \"*\",\n        \"sugarss\": \"*\",\n        \"terser\": \"^5.4.0\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@types/node\": {\n          \"optional\": true\n        },\n        \"less\": {\n          \"optional\": true\n        },\n        \"lightningcss\": {\n          \"optional\": true\n        },\n        \"sass\": {\n          \"optional\": true\n        },\n        \"sass-embedded\": {\n          \"optional\": true\n        },\n        \"stylus\": {\n          \"optional\": true\n        },\n        \"sugarss\": {\n          \"optional\": true\n        },\n        \"terser\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/vite-node\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"cac\": \"^6.7.14\",\n        \"debug\": \"^4.3.7\",\n        \"es-module-lexer\": \"^1.5.4\",\n        \"pathe\": \"^1.1.2\",\n        \"vite\": \"^5.0.0\"\n      },\n      \"bin\": {\n        \"vite-node\": \"vite-node.mjs\"\n      },\n      \"engines\": {\n        \"node\": \"^18.0.0 || >=20.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      }\n    },\n    \"node_modules/vitest\": {\n      \"version\": \"2.1.9\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"@vitest/expect\": \"2.1.9\",\n        \"@vitest/mocker\": \"2.1.9\",\n        \"@vitest/pretty-format\": \"^2.1.9\",\n        \"@vitest/runner\": \"2.1.9\",\n        \"@vitest/snapshot\": \"2.1.9\",\n        \"@vitest/spy\": \"2.1.9\",\n        \"@vitest/utils\": \"2.1.9\",\n        \"chai\": \"^5.1.2\",\n        \"debug\": \"^4.3.7\",\n        \"expect-type\": \"^1.1.0\",\n        \"magic-string\": \"^0.30.12\",\n        \"pathe\": \"^1.1.2\",\n        \"std-env\": \"^3.8.0\",\n        \"tinybench\": \"^2.9.0\",\n        \"tinyexec\": \"^0.3.1\",\n        \"tinypool\": \"^1.0.1\",\n        \"tinyrainbow\": \"^1.2.0\",\n        \"vite\": \"^5.0.0\",\n        \"vite-node\": \"2.1.9\",\n        \"why-is-node-running\": \"^2.3.0\"\n      },\n      \"bin\": {\n        \"vitest\": \"vitest.mjs\"\n      },\n      \"engines\": {\n        \"node\": \"^18.0.0 || >=20.0.0\"\n      },\n      \"funding\": {\n        \"url\": \"https://opencollective.com/vitest\"\n      },\n      \"peerDependencies\": {\n        \"@edge-runtime/vm\": \"*\",\n        \"@types/node\": \"^18.0.0 || >=20.0.0\",\n        \"@vitest/browser\": \"2.1.9\",\n        \"@vitest/ui\": \"2.1.9\",\n        \"happy-dom\": \"*\",\n        \"jsdom\": \"*\"\n      },\n      \"peerDependenciesMeta\": {\n        \"@edge-runtime/vm\": {\n          \"optional\": true\n        },\n        \"@types/node\": {\n          \"optional\": true\n        },\n        \"@vitest/browser\": {\n          \"optional\": true\n        },\n        \"@vitest/ui\": {\n          \"optional\": true\n        },\n        \"happy-dom\": {\n          \"optional\": true\n        },\n        \"jsdom\": {\n          \"optional\": true\n        }\n      }\n    },\n    \"node_modules/which\": {\n      \"version\": \"2.0.2\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"dependencies\": {\n        \"isexe\": \"^2.0.0\"\n      },\n      \"bin\": {\n        \"node-which\": \"bin/node-which\"\n      },\n      \"engines\": {\n        \"node\": \">= 8\"\n      }\n    },\n    \"node_modules/why-is-node-running\": {\n      \"version\": \"2.3.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"siginfo\": \"^2.0.0\",\n        \"stackback\": \"0.0.2\"\n      },\n      \"bin\": {\n        \"why-is-node-running\": \"cli.js\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/word-wrap\": {\n      \"version\": \"1.2.5\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=0.10.0\"\n      }\n    },\n    \"node_modules/wrap-ansi\": {\n      \"version\": \"9.0.2\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^6.2.1\",\n        \"string-width\": \"^7.0.0\",\n        \"strip-ansi\": \"^7.1.0\"\n      },\n      \"engines\": {\n        \"node\": \">=18\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs\": {\n      \"name\": \"wrap-ansi\",\n      \"version\": \"7.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-styles\": \"^4.0.0\",\n        \"string-width\": \"^4.1.0\",\n        \"strip-ansi\": \"^6.0.0\"\n      },\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/wrap-ansi?sponsor=1\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/ansi-regex\": {\n      \"version\": \"5.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/emoji-regex\": {\n      \"version\": \"8.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\"\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point\": {\n      \"version\": \"3.0.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/string-width\": {\n      \"version\": \"4.2.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"emoji-regex\": \"^8.0.0\",\n        \"is-fullwidth-code-point\": \"^3.0.0\",\n        \"strip-ansi\": \"^6.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi-cjs/node_modules/strip-ansi\": {\n      \"version\": \"6.0.1\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"dependencies\": {\n        \"ansi-regex\": \"^5.0.1\"\n      },\n      \"engines\": {\n        \"node\": \">=8\"\n      }\n    },\n    \"node_modules/wrap-ansi/node_modules/ansi-styles\": {\n      \"version\": \"6.2.3\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=12\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/chalk/ansi-styles?sponsor=1\"\n      }\n    },\n    \"node_modules/yaml\": {\n      \"version\": \"2.8.2\",\n      \"dev\": true,\n      \"license\": \"ISC\",\n      \"bin\": {\n        \"yaml\": \"bin.mjs\"\n      },\n      \"engines\": {\n        \"node\": \">= 14.6\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/eemeli\"\n      }\n    },\n    \"node_modules/yocto-queue\": {\n      \"version\": \"0.1.0\",\n      \"dev\": true,\n      \"license\": \"MIT\",\n      \"engines\": {\n        \"node\": \">=10\"\n      },\n      \"funding\": {\n        \"url\": \"https://github.com/sponsors/sindresorhus\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "memori-ts/package.json",
    "content": "{\n  \"name\": \"@memorilabs/memori\",\n  \"version\": \"0.0.5\",\n  \"description\": \"The official TypeScript SDK for Memori\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    },\n    \"./integrations\": {\n      \"import\": \"./dist/integrations/index.js\",\n      \"types\": \"./dist/integrations/index.d.ts\"\n    }\n  },\n  \"bin\": {\n    \"memori\": \"./dist/bin/cli.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"scripts\": {\n    \"prebuild\": \"rm -rf dist\",\n    \"build\": \"tsc -p tsconfig.build.json\",\n    \"build:dev\": \"tsc -p tsconfig.json\",\n    \"memori\": \"node dist/bin/cli.js\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"format\": \"prettier --write .\",\n    \"format:check\": \"prettier --check .\",\n    \"check\": \"npm run format:check && npm run lint && npm run typecheck\",\n    \"test\": \"vitest run\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"example\": \"npm run build:dev && node dist/examples/cloud/simple.js\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"keywords\": [\n    \"memori\",\n    \"llm\",\n    \"memory\",\n    \"ai\",\n    \"openai\",\n    \"anthropic\",\n    \"gemini\",\n    \"sdk\",\n    \"typescript\"\n  ],\n  \"author\": \"Memori Labs\",\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/MemoriLabs/Memori.git\",\n    \"directory\": \"memori-ts\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/MemoriLabs/Memori/issues\"\n  },\n  \"homepage\": \"https://github.com/MemoriLabs/Memori\",\n  \"peerDependencies\": {\n    \"@anthropic-ai/sdk\": \"*\",\n    \"openai\": \"*\"\n  },\n  \"peerDependenciesMeta\": {\n    \"openai\": {\n      \"optional\": true\n    },\n    \"@anthropic-ai/sdk\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@anthropic-ai/sdk\": \"*\",\n    \"@eslint/js\": \"^9.0.0\",\n    \"@types/node\": \"^20.0.0\",\n    \"@vitest/coverage-v8\": \"^2.1.9\",\n    \"@vitest/ui\": \"^2.1.9\",\n    \"dotenv\": \"^16.4.5\",\n    \"eslint\": \"^9.0.0\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-plugin-tsdoc\": \"^0.5.1\",\n    \"lint-staged\": \"^15.0.0\",\n    \"openai\": \"*\",\n    \"prettier\": \"^3.0.0\",\n    \"typescript\": \"^5.0.0\",\n    \"typescript-eslint\": \"^8.0.0\",\n    \"vitest\": \"^2.1.9\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"dependencies\": {\n    \"@memorilabs/axon\": \"^0.1.2\"\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/bin/cli.ts",
    "content": "#!/usr/bin/env node\nimport { main } from '../cli/router.js';\n\nvoid main();\n"
  },
  {
    "path": "memori-ts/src/cli/commands/help.ts",
    "content": "import { printBanner } from '../utils.js';\n\nexport function helpCommand(_args: string[]): Promise<void> {\n  printBanner();\n  console.log('Usage: memori <command> [options]\\n');\n  console.log('Available Commands:');\n  console.log('  quota       View your current memory usage and limits');\n  console.log('  help        Display this help message\\n');\n\n  return Promise.resolve();\n}\n"
  },
  {
    "path": "memori-ts/src/cli/commands/quota.ts",
    "content": "import { Config } from '../../core/config.js';\nimport { Api } from '../../core/network.js';\nimport { printBanner } from '../utils.js';\n\ninterface QuotaResponse {\n  memories: {\n    max: number;\n    num: number;\n  };\n  message: string;\n}\n\nexport async function quotaCommand(_args: string[]): Promise<void> {\n  const config = new Config();\n  const api = new Api(config);\n\n  printBanner();\n\n  try {\n    const response = await api.get<QuotaResponse>('sdk/quota');\n\n    console.log(`+ Maximum # of Memories: ${response.memories.max.toLocaleString()}`);\n    console.log(`+ Current # of Memories: ${response.memories.num.toLocaleString()}\\n`);\n    console.log(`+ ${response.message}\\n`);\n  } catch (error) {\n    console.error('Failed to fetch quota. Please check your MEMORI_API_KEY.');\n    if (error instanceof Error) {\n      console.error(`Error details: ${error.message}`);\n    }\n    process.exit(1);\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/cli/router.ts",
    "content": "import { quotaCommand } from './commands/quota.js';\nimport { helpCommand } from './commands/help.js';\n\nconst commands: Record<string, ((args: string[]) => Promise<void>) | undefined> = {\n  quota: quotaCommand,\n  help: helpCommand,\n};\n\nexport async function main() {\n  const args = process.argv.slice(2);\n  const commandName = args[0] || 'help';\n  const commandArgs = args.slice(1);\n\n  const handler = commands[commandName];\n\n  if (!handler) {\n    console.error(`\\nError: Unknown command '${commandName}'\\n`);\n    await helpCommand([]);\n    process.exit(1);\n  }\n\n  try {\n    await handler(commandArgs);\n  } catch (error) {\n    console.error(`\\nUnexpected error executing command '${commandName}':`);\n    console.error(error instanceof Error ? error.message : error);\n    process.exit(1);\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/cli/utils.ts",
    "content": "export const printBanner = () => {\n  console.log(`\n __  __                           _\n|  \\\\/  | ___ _ __ ___   ___  _ __(_)\n| |\\\\/| |/ _ \\\\ '_ \\` _ \\\\ / _ \\\\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\\\___|_| |_| |_|\\\\___/|_|  |_|\n                  perfectam memoriam\n                       memorilabs.ai\n`);\n};\n"
  },
  {
    "path": "memori-ts/src/core/config.ts",
    "content": "import { randomUUID } from 'node:crypto';\n\n/**\n * Utility to safely retrieve environment variables across Node.js and other runtimes.\n */\nfunction getEnv(key: string): string | undefined {\n  if (typeof process !== 'undefined') {\n    return process.env[key];\n  }\n  return undefined;\n}\n\nexport class Config {\n  /**\n   * The API Key used for authentication.\n   * Defaults to `MEMORI_API_KEY` environment variable.\n   */\n  public apiKey: string | null;\n\n  /**\n   * The base URL for the Memori API.\n   * Automatically switches between production and staging based on `testMode`.\n   */\n  public baseUrl: string;\n\n  /**\n   * Whether the SDK is running in test/staging mode.\n   * Defaults to `true` if `MEMORI_TEST_MODE` is set to '1'.\n   */\n  public testMode: boolean;\n\n  /**\n   * The unique identifier for the end-user associated with the current memories.\n   */\n  public entityId?: string;\n\n  /**\n   * The unique identifier for the specific process or workflow.\n   */\n  public processId?: string;\n\n  /**\n   * The current conversation session ID.\n   * Included in all requests to track conversation history.\n   */\n  public sessionId: string;\n\n  /**\n   * The minimum relevance score (0.0 to 1.0) required for a memory to be included in the context.\n   * Defaults to 0.1.\n   */\n  public recallRelevanceThreshold: number;\n\n  /**\n   * Request timeout in milliseconds.\n   * Defaults to 5000ms (5 seconds).\n   */\n  public timeout: number;\n\n  constructor() {\n    // 1. Environment and Base URL Logic\n    this.testMode = getEnv('MEMORI_TEST_MODE') === '1';\n\n    const envUrl = getEnv('MEMORI_API_URL_BASE');\n    if (envUrl) {\n      this.baseUrl = envUrl;\n    } else {\n      this.baseUrl = this.testMode\n        ? 'https://staging-api.memorilabs.ai'\n        : 'https://api.memorilabs.ai';\n    }\n\n    // 2. Authentication\n    this.apiKey = getEnv('MEMORI_API_KEY') ?? null;\n\n    // 3. Session and Defaults\n    this.sessionId = randomUUID();\n    this.recallRelevanceThreshold = 0.1;\n    this.timeout = 5000;\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/core/errors.ts",
    "content": "export { UnsupportedLLMProviderError } from '@memorilabs/axon';\n\n/** Base class for all Memori SDK errors. */\nexport class MemoriError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = 'MemoriError';\n    Object.setPrototypeOf(this, new.target.prototype);\n  }\n}\n\n/** Thrown when the Memori Cloud IP rate limit or account quota is exceeded. */\nexport class QuotaExceededError extends MemoriError {\n  constructor(message?: string) {\n    super(\n      message ||\n        'Your IP address is over quota; register for an API key now: https://app.memorilabs.ai/signup'\n    );\n    this.name = 'QuotaExceededError';\n  }\n}\n\n/** Thrown when the Memori Cloud API returns a 4xx or 5xx status code. */\nexport class MemoriApiClientError extends MemoriError {\n  public readonly statusCode: number;\n  public readonly details?: unknown;\n\n  constructor(statusCode: number, message?: string, details?: unknown) {\n    super(message || `Memori API request failed with status ${statusCode}`);\n    this.name = 'MemoriApiClientError';\n    this.statusCode = statusCode;\n    this.details = details;\n  }\n}\n\n/** Thrown when the Memori Cloud API rejects a request due to validation errors (422). */\nexport class MemoriApiValidationError extends MemoriApiClientError {\n  constructor(statusCode: number, message: string, details?: unknown) {\n    super(statusCode, message, details);\n    this.name = 'MemoriApiValidationError';\n  }\n}\n\n/** Thrown when the Memori Cloud API explicitly rejects a request (433). */\nexport class MemoriApiRequestRejectedError extends MemoriApiClientError {\n  constructor(statusCode: number, message: string, details?: unknown) {\n    super(statusCode, message, details);\n    this.name = 'MemoriApiRequestRejectedError';\n  }\n}\n\n/** Thrown when a request requires an API key but none was found in the environment or config. */\nexport class MissingMemoriApiKeyError extends MemoriError {\n  constructor(envVar = 'MEMORI_API_KEY') {\n    super(\n      `A ${envVar} is required to use the Memori cloud API. Sign up at https://app.memorilabs.ai/signup`\n    );\n    this.name = 'MissingMemoriApiKeyError';\n  }\n}\n\n/** Thrown when a network request to the Memori API exceeds the configured timeout duration. */\nexport class TimeoutError extends MemoriError {\n  constructor(timeout: number) {\n    super(`Request timed out after ${timeout}ms`);\n    this.name = 'TimeoutError';\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/core/network.ts",
    "content": "import { Config } from './config.js';\nimport {\n  MemoriApiClientError,\n  MemoriApiRequestRejectedError,\n  MemoriApiValidationError,\n  QuotaExceededError,\n  TimeoutError,\n} from './errors.js';\n\nexport enum ApiSubdomain {\n  DEFAULT = 'api',\n  COLLECTOR = 'collector',\n}\n\nconst PUBLIC_PROD_KEY = '96a7ea3e-11c2-428c-b9ae-5a168363dc80';\nconst PUBLIC_STAGING_KEY = 'c18b1022-7fe2-42af-ab01-b1f9139184f0';\n\ninterface FetchOptions extends RequestInit {\n  maxRetries?: number;\n}\n\nconst delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport class Api {\n  private readonly config: Config;\n  private readonly xApiKey: string;\n  private readonly baseUrl: string;\n\n  constructor(config: Config, subdomain: ApiSubdomain = ApiSubdomain.DEFAULT) {\n    this.config = config;\n\n    if (subdomain === ApiSubdomain.COLLECTOR) {\n      this.baseUrl = this.config.baseUrl\n        .replace('://api.', '://collector.')\n        .replace('://staging-api.', '://staging-collector.');\n    } else {\n      this.baseUrl = this.config.baseUrl;\n    }\n\n    this.xApiKey = this.config.testMode ? PUBLIC_STAGING_KEY : PUBLIC_PROD_KEY;\n  }\n\n  private getHeaders(): HeadersInit {\n    const headers: Record<string, string> = {\n      'Content-Type': 'application/json',\n      'X-Memori-API-Key': this.xApiKey,\n    };\n\n    if (this.config.apiKey) {\n      headers['Authorization'] = `Bearer ${this.config.apiKey}`;\n    }\n\n    return headers;\n  }\n\n  /**\n   * Performs a fetch request with exponential backoff and timeout handling.\n   * Retries on network errors, timeouts, and 5xx server errors.\n   */\n  private async request<T>(\n    method: string,\n    route: string,\n    body?: unknown,\n    options: FetchOptions = {}\n  ): Promise<T> {\n    const url = `${this.baseUrl}/v1/${route}`;\n    const { maxRetries = 5, ...fetchInit } = options;\n\n    // Prepare the RequestInit once\n    const init: RequestInit = {\n      method,\n      headers: this.getHeaders(),\n      body: body ? JSON.stringify(body) : undefined,\n      ...fetchInit,\n    };\n\n    let attempt = 0;\n    let lastError: Error | null = null;\n\n    while (attempt <= maxRetries) {\n      try {\n        return await this.executeAttempt<T>(url, init);\n      } catch (err: unknown) {\n        lastError = err as Error;\n\n        // Determine if we should retry\n        const isNetworkError = err instanceof TypeError;\n        const isServer5xx =\n          err instanceof MemoriApiClientError && err.statusCode >= 500 && err.statusCode <= 599;\n        const isTimeout = err instanceof TimeoutError;\n\n        if ((isNetworkError || isServer5xx || isTimeout) && attempt < maxRetries) {\n          const backoff = Math.pow(2, attempt) * 1000;\n          await delay(backoff);\n          attempt++;\n          continue;\n        }\n\n        throw lastError;\n      }\n    }\n\n    throw lastError || new Error('Unknown network error');\n  }\n\n  private async executeAttempt<T>(url: string, init: RequestInit): Promise<T> {\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => {\n      controller.abort();\n    }, this.config.timeout);\n\n    try {\n      const response = await fetch(url, { ...init, signal: controller.signal });\n      clearTimeout(timeoutId);\n\n      if (response.ok) {\n        if (response.status === 204) return {} as T;\n\n        return (await response.json()) as T;\n      }\n\n      await this.throwOnApiError(response);\n      throw new Error('Unreachable');\n    } catch (err: unknown) {\n      clearTimeout(timeoutId);\n\n      if ((err as Error).name === 'AbortError') {\n        throw new TimeoutError(this.config.timeout);\n      }\n      throw err;\n    }\n  }\n\n  private async throwOnApiError(response: Response): Promise<void> {\n    // Try to parse the error details safely\n    let errorData: { message?: string; detail?: string } = {};\n\n    try {\n      errorData = (await response.json()) as typeof errorData;\n    } catch {\n      // ignore parsing error\n    }\n\n    const message = errorData.message || errorData.detail;\n    const status = response.status;\n\n    // Map status codes to errors\n    if (status === 429) throw new QuotaExceededError(message);\n    if (status === 422) {\n      throw new MemoriApiValidationError(\n        status,\n        message || 'Memori API rejected the request (422 validation error).'\n      );\n    }\n    if (status === 433) {\n      throw new MemoriApiRequestRejectedError(status, message || 'The request was rejected (433).');\n    }\n    if (status >= 500) throw new MemoriApiClientError(status, message);\n    if (status >= 400) throw new MemoriApiClientError(status, message);\n\n    throw new MemoriApiClientError(status, `Unknown error ${status}`);\n  }\n\n  public get<T>(route: string): Promise<T> {\n    return this.request<T>('GET', route);\n  }\n\n  public post<T>(route: string, body?: unknown): Promise<T> {\n    return this.request<T>('POST', route, body);\n  }\n\n  public patch<T>(route: string, body?: unknown): Promise<T> {\n    return this.request<T>('PATCH', route, body);\n  }\n\n  public delete<T>(route: string): Promise<T> {\n    return this.request<T>('DELETE', route);\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/core/session.ts",
    "content": "import { randomUUID } from 'node:crypto';\n\n/**\n * Manages the conversation session lifecycle.\n * Ensures consistent session IDs across requests to maintain conversation history.\n */\nexport class SessionManager {\n  private _id: string;\n\n  constructor() {\n    this._id = randomUUID();\n  }\n\n  /**\n   * The current active session UUID.\n   */\n  public get id(): string {\n    return this._id;\n  }\n\n  /**\n   * Generates a brand new random UUID for the session.\n   * Use this to clear context and start fresh.\n   */\n  public reset(): this {\n    this._id = randomUUID();\n    return this;\n  }\n\n  /**\n   * Manually sets the session ID to a specific value.\n   * Useful for resuming a conversation from a database or frontend client.\n   *\n   * @param id - The UUID to reuse.\n   */\n  public set(id: string): this {\n    this._id = id;\n    return this;\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/engines/augmentation.ts",
    "content": "import { CallContext, LLMRequest, LLMResponse } from '@memorilabs/axon';\nimport { Api } from '../core/network.js';\nimport { Config } from '../core/config.js';\nimport { SessionManager } from '../core/session.js';\nimport { extractLastUserMessage } from '../utils/utils.js';\nimport { SDK_VERSION } from '../version.js';\n\nexport class AugmentationEngine {\n  constructor(\n    private readonly api: Api,\n    private readonly config: Config,\n    private readonly session: SessionManager\n  ) {}\n\n  public handleAugmentation(\n    req: LLMRequest,\n    res: LLMResponse,\n    ctx: CallContext\n  ): Promise<LLMResponse> {\n    const sessionId = this.session.id;\n    if (!sessionId) return Promise.resolve(res);\n\n    const lastUserMessage = extractLastUserMessage(req.messages);\n    if (!lastUserMessage) return Promise.resolve(res);\n\n    const messages = [\n      { role: 'user', content: lastUserMessage },\n      { role: 'assistant', content: res.content },\n    ];\n\n    const payload = {\n      conversation: { messages, summary: null },\n      meta: this.buildMeta(req, ctx),\n      session: { id: sessionId },\n    };\n\n    // Fire-and-forget\n    this.api.post('cloud/augmentation', payload).catch((e: unknown) => {\n      if (this.config.testMode) console.warn('Augmentation failed:', e);\n    });\n\n    return Promise.resolve(res);\n  }\n\n  private buildMeta(req: LLMRequest, ctx: CallContext): Record<string, unknown> {\n    return {\n      attribution: {\n        entity: { id: this.config.entityId },\n        process: { id: this.config.processId },\n      },\n      sdk: { lang: 'javascript', version: ctx.metadata.integrationSdkVersion || SDK_VERSION },\n      framework: null,\n      llm: {\n        model: {\n          provider: ctx.metadata.provider || null,\n          sdk: {\n            version: ctx.metadata.sdkVersion || null,\n          },\n          version: req.model || null,\n        },\n      },\n      platform: {\n        provider: ctx.metadata.platform || null,\n      },\n      storage: null,\n    };\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/engines/persistence.ts",
    "content": "import { CallContext, LLMRequest, LLMResponse } from '@memorilabs/axon';\nimport { Api } from '../core/network.js';\nimport { Config } from '../core/config.js';\nimport { SessionManager } from '../core/session.js';\nimport { extractLastUserMessage } from '../utils/utils.js';\n\nexport class PersistenceEngine {\n  constructor(\n    private readonly api: Api,\n    private readonly config: Config,\n    private readonly session: SessionManager\n  ) {}\n\n  public async handlePersistence(\n    req: LLMRequest,\n    res: LLMResponse,\n    _ctx: CallContext\n  ): Promise<LLMResponse> {\n    const sessionId = this.session.id;\n    if (!sessionId) return res;\n\n    const lastUserMessage = extractLastUserMessage(req.messages);\n    if (!lastUserMessage) return res;\n\n    const payload = {\n      attribution: {\n        entity: { id: this.config.entityId },\n        process: { id: this.config.processId },\n      },\n      messages: [\n        { role: 'user', type: 'text', text: lastUserMessage },\n        { role: 'assistant', type: 'text', text: res.content },\n      ],\n      session: { id: sessionId },\n    };\n\n    try {\n      await this.api.post('cloud/conversation/messages', payload);\n    } catch (e) {\n      console.warn('Memori Persistence failed:', e);\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/engines/recall.ts",
    "content": "import { CallContext, LLMRequest, Message, Role } from '@memorilabs/axon';\nimport { Api } from '../core/network.js';\nimport { Config } from '../core/config.js';\nimport { SessionManager } from '../core/session.js';\nimport { extractFacts, extractHistory, stringifyContent } from '../utils/utils.js';\nimport { CloudRecallResponse, ParsedFact } from '../types/api.js';\nimport { extractLastUserMessage } from '../utils/utils.js';\n\nexport class RecallEngine {\n  constructor(\n    private readonly api: Api,\n    private readonly config: Config,\n    private readonly session: SessionManager\n  ) {}\n\n  public async recall(query: string): Promise<ParsedFact[]> {\n    const payload = {\n      attribution: {\n        entity: { id: this.config.entityId },\n        process: { id: this.config.processId },\n      },\n      query,\n      session: { id: this.session.id },\n    };\n\n    try {\n      const response = await this.api.post<CloudRecallResponse>('cloud/recall', payload);\n      return extractFacts(response);\n    } catch (e) {\n      console.warn('Memori Manual Recall failed:', e);\n      return [];\n    }\n  }\n\n  public async handleRecall(req: LLMRequest, _ctx: CallContext): Promise<LLMRequest> {\n    const sessionId = this.session.id;\n    if (!sessionId) return req;\n\n    const userQuery = extractLastUserMessage(req.messages);\n    if (!userQuery) return req;\n\n    const payload = {\n      attribution: {\n        entity: { id: this.config.entityId },\n        process: { id: this.config.processId },\n      },\n      query: userQuery,\n      session: { id: sessionId },\n    };\n\n    let response: CloudRecallResponse;\n    try {\n      response = await this.api.post<CloudRecallResponse>('cloud/recall', payload);\n    } catch (e) {\n      console.warn('Memori Recall failed:', e);\n      return req;\n    }\n\n    const facts = extractFacts(response);\n    const historyRaw = extractHistory(response);\n\n    const historyMessages: Message[] = (\n      historyRaw as Array<{ role: Role; content?: unknown; text?: string }>\n    )\n      .filter((m) => m.role !== 'system')\n      .map((m) => ({\n        role: m.role,\n        content: stringifyContent(m.content ?? m.text),\n      }));\n\n    const relevantFacts = facts\n      // Filter out low-relevance memories to prevent hallucination or context pollution\n      .filter((f) => f.score >= this.config.recallRelevanceThreshold)\n      .map((f) => {\n        const dateSuffix = f.dateCreated ? `. Stated at ${f.dateCreated}` : '';\n        return `- ${f.content}${dateSuffix}`;\n      });\n\n    let messages = [...req.messages];\n\n    if (historyMessages.length > 0) {\n      messages = [...historyMessages, ...messages];\n    }\n\n    if (relevantFacts.length > 0) {\n      const factList = relevantFacts.join('\\n');\n      const recallContext = `\\n\\n<memori_context>\\nOnly use the relevant context if it is relevant to the user's query. Relevant context about the user:\\n${factList}\\n</memori_context>`;\n\n      const systemIdx = messages.findIndex((m) => m.role === 'system');\n      if (systemIdx >= 0) {\n        messages[systemIdx] = {\n          ...messages[systemIdx],\n          content: messages[systemIdx].content + recallContext,\n        };\n      } else {\n        messages.unshift({ role: 'system', content: recallContext });\n      }\n    }\n\n    return { ...req, messages };\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/index.ts",
    "content": "export { Memori } from './memori.js';\nexport { ParsedFact } from './types/api.js';\nexport * from './core/errors.js';\n"
  },
  {
    "path": "memori-ts/src/integrations/base.ts",
    "content": "import { LLMRequest, LLMResponse, CallContext } from '@memorilabs/axon';\nimport { MemoriCore, IntegrationRequest } from '../types/integrations.js';\n\n/**\n * Abstract base class for Memori framework integrations.\n *\n * Provides common functionality for translating framework-specific data formats\n * (like OpenClaw messages) into Axon's internal LLM request/response format.\n *\n * This class is internal to the SDK - framework integrations should extend it\n * and implement their own public-facing methods.\n *\n * @internal\n */\nexport abstract class BaseIntegration {\n  constructor(protected readonly core: MemoriCore) {}\n\n  /**\n   * Internal helper: Captures a conversation turn by translating it into Axon format\n   * and feeding it to both the Persistence and Augmentation engines.\n   *\n   * @param req - The unified integration message containing user text, agent text, and metadata\n   * @internal\n   */\n  protected async executeAugmentation(req: IntegrationRequest): Promise<void> {\n    if (!this.core.session.id) return;\n\n    const syntheticReq: LLMRequest = {\n      messages: [{ role: 'user', content: req.userMessage }],\n      model: req.metadata?.model || '',\n    };\n    const syntheticRes: LLMResponse = {\n      content: req.agentResponse,\n    };\n\n    const syntheticCtx: CallContext = {\n      traceId: `integration-trace-${Date.now()}`,\n      startedAt: new Date(),\n      metadata: req.metadata as unknown as Record<string, unknown>,\n    };\n\n    try {\n      await this.core.persistence.handlePersistence(syntheticReq, syntheticRes, syntheticCtx);\n      await this.core.augmentation.handleAugmentation(syntheticReq, syntheticRes, syntheticCtx);\n    } catch (e) {\n      console.warn('Memori Integration Capture failed:', e);\n    }\n  }\n\n  /**\n   * Internal helper: Recalls memories by translating the query into Axon format,\n   * passing it through the Recall engine, and extracting the injected system prompt.\n   *\n   * @param userMessage - Raw user query text\n   * @returns XML-formatted memory context, or undefined if no session or recall fails\n   * @internal\n   */\n  protected async executeRecall(userMessage: string): Promise<string | undefined> {\n    if (!this.core.session.id) return undefined;\n\n    const syntheticReq: LLMRequest = {\n      messages: [{ role: 'user', content: userMessage }],\n    };\n\n    const syntheticCtx: CallContext = {\n      traceId: `integration-trace-${Date.now()}`,\n      startedAt: new Date(),\n      metadata: {},\n    };\n\n    try {\n      const updatedReq = await this.core.recall.handleRecall(syntheticReq, syntheticCtx);\n      const systemMsg = updatedReq.messages.find((m) => m.role === 'system');\n      return systemMsg?.content;\n    } catch (e) {\n      console.warn('Memori Integration Recall failed:', e);\n      return undefined;\n    }\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/integrations/index.ts",
    "content": "export { OpenClawIntegration } from './openclaw.js';\nexport { IntegrationRequest, IntegrationMetadata } from '../types/integrations.js';\n"
  },
  {
    "path": "memori-ts/src/integrations/openclaw.ts",
    "content": "import { IntegrationRequest } from 'src/types/integrations.js';\nimport { BaseIntegration } from './base.js';\n\n/**\n * OpenClaw-specific integration for Memori.\n * Provides memory capture and recall functionality for OpenClaw agents.\n */\nexport class OpenClawIntegration extends BaseIntegration {\n  /**\n   * Sets the attribution context for memory operations.\n   *\n   * @param entityId - Unique identifier for the entity (required)\n   * @param processId - Optional identifier for the workflow/process/agent\n   * @returns This instance for method chaining\n   */\n  public setAttribution(entityId: string, processId?: string): this {\n    this.core.config.entityId = entityId;\n    if (processId) this.core.config.processId = processId;\n    return this;\n  }\n\n  /**\n   * Sets the current conversation session ID.\n   *\n   * @param sessionId - Unique session identifier\n   * @returns This instance for method chaining\n   */\n  public setSession(sessionId: string): this {\n    this.core.session.set(sessionId);\n    return this;\n  }\n\n  /**\n   * Captures a conversation turn and sends it to Memori for processing.\n   *\n   * @param req - The unified integration message containing user text, agent text, and metadata\n   * @returns Promise that resolves when capture is complete\n   *\n   * @throws Does not throw - errors are logged but swallowed to prevent disrupting the agent\n   */\n  public async augmentation(req: IntegrationRequest): Promise<void> {\n    await this.executeAugmentation(req);\n  }\n\n  /**\n   * Retrieves relevant memories for the given prompt and returns formatted context.\n   *\n   * @param promptText - The user's prompt/query\n   * @returns Formatted XML context string to inject, or undefined if no relevant memories found\n   *\n   * @throws Does not throw - errors are logged but swallowed, returns undefined on failure\n   */\n  public async recall(promptText: string): Promise<string | undefined> {\n    return this.executeRecall(promptText);\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/memori.ts",
    "content": "import { Axon } from '@memorilabs/axon';\nimport { Config } from './core/config.js';\nimport { SessionManager } from './core/session.js';\nimport { Api, ApiSubdomain } from './core/network.js';\nimport { RecallEngine } from './engines/recall.js';\nimport { PersistenceEngine } from './engines/persistence.js';\nimport { AugmentationEngine } from './engines/augmentation.js';\nimport { ParsedFact } from './types/api.js';\nimport { IntegrationConstructor, SupportedIntegration } from './types/integrations.js';\n\n/**\n * The main entry point for the Memori SDK.\n *\n * This class orchestrates the connection between your application, the Memori Cloud,\n * and your LLM provider. It automatically handles:\n * - Long-term memory recall (fetching relevant facts)\n * - Conversation persistence (storing messages)\n * - User augmentation (learning from interactions)\n */\nexport class Memori {\n  /**\n   * The configuration state for the SDK.\n   * Modifying properties here (like timeout) affects all future requests.\n   */\n  public readonly config: Config;\n\n  /**\n   * Manages the current conversation session ID.\n   */\n  public readonly session: SessionManager;\n\n  /**\n   * The underlying Axon instance used for LLM middleware hooks.\n   */\n  public readonly axon: Axon;\n\n  private readonly api: Api;\n  private readonly collectorApi: Api;\n\n  private readonly recallEngine: RecallEngine;\n  private readonly persistenceEngine: PersistenceEngine;\n  private readonly augmentationEngine: AugmentationEngine;\n\n  /**\n   * Access the LLM integration layer.\n   */\n  public readonly llm = {\n    /**\n     * Registers a third-party LLM client (e.g., OpenAI, Anthropic) with Memori.\n     * This enables Memori to automatically inject recalled memories into the system prompt.\n     *\n     * @param client - An instantiated client from a supported provider (OpenAI, Anthropic, etc).\n     */\n    register: (client: unknown): Memori => {\n      this.axon.llm.register(client);\n      return this;\n    },\n  };\n\n  constructor() {\n    // 1. Core State\n    this.config = new Config();\n    this.session = new SessionManager();\n    this.axon = new Axon();\n\n    // 2. Network Layer\n    this.api = new Api(this.config, ApiSubdomain.DEFAULT);\n    this.collectorApi = new Api(this.config, ApiSubdomain.COLLECTOR);\n\n    // 3. Engines\n    this.recallEngine = new RecallEngine(this.api, this.config, this.session);\n    this.persistenceEngine = new PersistenceEngine(this.api, this.config, this.session);\n    this.augmentationEngine = new AugmentationEngine(this.collectorApi, this.config, this.session);\n\n    // 4. Register Hooks\n    this.axon.hook.before(this.recallEngine.handleRecall.bind(this.recallEngine));\n    this.axon.hook.after(this.persistenceEngine.handlePersistence.bind(this.persistenceEngine));\n    this.axon.hook.after(this.augmentationEngine.handleAugmentation.bind(this.augmentationEngine));\n  }\n\n  /**\n   * Configures the attribution context for subsequent operations.\n   * This helps segregate memories by user (Entity) or workflow (Process).\n   *\n   * @param entityId - Unique identifier for the end-user (e.g., user GUID).\n   * @param processId - Unique identifier for the specific workflow or agent.\n   */\n  public attribution(entityId?: string, processId?: string): this {\n    if (entityId) this.config.entityId = entityId;\n    if (processId) this.config.processId = processId;\n    return this;\n  }\n\n  /**\n   * Manually retrieves relevant facts from Memori based on a query.\n   * Useful if you need to fetch memories without triggering a full LLM completion.\n   *\n   * @param query - The search text used to find relevant memories.\n   * @returns A list of parsed facts with their relevance scores.\n   */\n  public async recall(query: string): Promise<ParsedFact[]> {\n    return this.recallEngine.recall(query);\n  }\n\n  /**\n   * Resets the current session ID to a new random UUID.\n   * Call this when starting a completely new conversation thread.\n   */\n  public resetSession(): this {\n    this.session.reset();\n    return this;\n  }\n\n  /**\n   * Manually sets the session ID.\n   * Use this to resume an existing conversation thread from your database.\n   *\n   * @param id - The UUID of the session to resume.\n   */\n  public setSession(id: string): this {\n    this.session.set(id);\n    return this;\n  }\n\n  /**\n   * Securely attaches a supported framework integration to this Memori instance.\n   *\n   * @typeParam T - The type of integration being created\n   * @param IntegrationClass - The integration class constructor to instantiate\n   * @returns A new instance of the specified integration with access to Memori's core engines\n   */\n  public integrate<T extends SupportedIntegration>(IntegrationClass: IntegrationConstructor<T>): T {\n    return new IntegrationClass({\n      recall: this.recallEngine,\n      persistence: this.persistenceEngine,\n      augmentation: this.augmentationEngine,\n      config: this.config,\n      session: this.session,\n    });\n  }\n}\n"
  },
  {
    "path": "memori-ts/src/types/api.ts",
    "content": "/**\n * Represents a single recalled item from the backend.\n * Can be a simple string or a structured object with scoring metadata.\n * @internal\n */\nexport interface RecallObject {\n  content: string;\n  rank_score?: number;\n  similarity?: number;\n  date_created?: string;\n}\n\n/**\n * @internal\n */\nexport type RecallItem = string | RecallObject;\n\n/**\n * Raw response shape from the Memori Cloud API.\n * @internal\n */\nexport interface CloudRecallResponse {\n  // The API might return the list of facts under any of these keys\n  facts?: RecallItem[];\n  results?: RecallItem[];\n  memories?: RecallItem[];\n  data?: RecallItem[];\n\n  // History fields\n  messages?: unknown[];\n  conversation_messages?: unknown[];\n  history?: unknown[];\n  conversation?: { messages?: unknown[] };\n}\n\n/**\n * A normalized memory fact returned to the user.\n */\nexport interface ParsedFact {\n  /**\n   * The actual text content of the memory or fact.\n   */\n  content: string;\n\n  /**\n   * The relevance score of this fact to the query (0.0 to 1.0).\n   * Higher is more relevant.\n   */\n  score: number;\n\n  /**\n   * The ISO timestamp (YYYY-MM-DD HH:mm) when this memory was originally created.\n   * Undefined if the backend did not return temporal data.\n   */\n  dateCreated?: string;\n}\n"
  },
  {
    "path": "memori-ts/src/types/integrations.ts",
    "content": "import { Config } from '../core/config.js';\nimport { SessionManager } from '../core/session.js';\nimport { RecallEngine } from '../engines/recall.js';\nimport { PersistenceEngine } from '../engines/persistence.js';\nimport { AugmentationEngine } from '../engines/augmentation.js';\nimport type { OpenClawIntegration } from '../integrations/openclaw.js';\n\n/**\n * Internal dependency injection container passed to framework integrations.\n * Provides access to Memori's core engines and configuration state.\n * @internal\n */\nexport interface MemoriCore {\n  recall: RecallEngine;\n  persistence: PersistenceEngine;\n  augmentation: AugmentationEngine;\n  config: Config;\n  session: SessionManager;\n}\n\n/**\n * Represents a single turn of conversation to be captured by Memori.\n * This payload is used by framework integrations to send user and agent messages\n * to the persistence and augmentation engines.\n */\nexport interface IntegrationRequest {\n  /**\n   * The raw text input provided by the user.\n   * @example \"What is my favorite color?\"\n   */\n  userMessage: string;\n\n  /**\n   * The final text response generated by the AI agent.\n   * @example \"Based on your past messages, your favorite color is blue.\"\n   */\n  agentResponse: string;\n\n  /**\n   * Optional telemetry and contextual metadata about the LLM execution.\n   */\n  metadata?: IntegrationMetadata;\n}\n\n/**\n * Telemetry and execution metadata associated with a conversation turn.\n * Used by Memori's Augmentation Engine to track usage patterns, model performance,\n * and platform-specific metrics.\n */\nexport interface IntegrationMetadata {\n  /**\n   * The LLM provider used to generate the response.\n   * @example 'openai', 'anthropic', 'openclaw'\n   */\n  provider: string | null | undefined;\n\n  /**\n   * The specific model identifier used for the completion.\n   * @example 'gpt-4o', 'claude-3.5-sonnet'\n   */\n  model: string | null | undefined;\n\n  /**\n   * The version of the underlying LLM provider's SDK used, if available.\n   * @example '4.28.0'\n   */\n  sdkVersion: string | null | undefined;\n\n  /**\n   * The version of the Memori integration or plugin capturing this data.\n   * @example '1.0.0-openclaw-plugin'\n   */\n  integrationSdkVersion: string | null | undefined;\n\n  /**\n   * Theframework where the agent is running.\n   * @example 'openclaw'\n   */\n  platform: string | null | undefined;\n}\n\n/**\n * A union type representing all officially supported framework integrations.\n * Used to strictly type the return value of the `memori.integrate()` method.\n */\nexport type SupportedIntegration = OpenClawIntegration;\n\n/**\n * Constructor signature for a Memori integration class.\n * Ensures that all integrations can be instantiated with the `MemoriCore` DI container.\n * @typeParam T - The specific type of the integration being constructed\n */\nexport type IntegrationConstructor<T extends SupportedIntegration> = new (core: MemoriCore) => T;\n"
  },
  {
    "path": "memori-ts/src/utils/utils.ts",
    "content": "import { Message } from '@memorilabs/axon';\nimport { CloudRecallResponse, ParsedFact } from '../types/api.js';\n\n/** @internal */\nexport function formatDate(dateStr?: string): string | undefined {\n  if (!dateStr) return undefined;\n  try {\n    const d = new Date(dateStr);\n    if (isNaN(d.getTime())) return dateStr.substring(0, 16);\n    return d.toISOString().replace('T', ' ').substring(0, 16);\n  } catch {\n    return undefined;\n  }\n}\n\n/**\n * Safely converts message content (string, array, or object) into a simple string.\n * Handles multi-modal arrays (e.g. OpenAI/Anthropic content blocks) by extracting text.\n * @internal\n */\nexport function stringifyContent(content: unknown): string {\n  if (!content) return '';\n  if (typeof content === 'string') return content;\n\n  if (Array.isArray(content)) {\n    return content\n      .map((part) => {\n        if (typeof part === 'string') return part;\n        if (part && typeof part === 'object') {\n          const obj = part as Record<string, unknown>;\n          const text = obj.text ?? obj.content;\n          return typeof text === 'string' ? text : '';\n        }\n        return '';\n      })\n      .join('\\n');\n  }\n\n  if (typeof content === 'object') {\n    const obj = content as Record<string, unknown>;\n    const text = obj.text ?? obj.content;\n    return typeof text === 'string' ? text : JSON.stringify(content);\n  }\n\n  return String(content as string | number | boolean);\n}\n\n/** @internal */\nexport function extractFacts(response: CloudRecallResponse): ParsedFact[] {\n  const raw = response.facts || response.results || response.memories || response.data || [];\n\n  if (!Array.isArray(raw)) return [];\n\n  const facts: ParsedFact[] = [];\n\n  for (const item of raw) {\n    if (typeof item === 'string') {\n      facts.push({ content: item, score: 1.0 });\n    } else if (typeof item === 'object' && 'content' in item && typeof item.content === 'string') {\n      let score = 0.0;\n      if (typeof item.rank_score === 'number') score = item.rank_score;\n      else if (typeof item.similarity === 'number') score = item.similarity;\n\n      facts.push({\n        content: item.content,\n        score,\n        dateCreated: formatDate(item.date_created),\n      });\n    }\n  }\n  return facts;\n}\n\n/** @internal */\nexport function extractHistory(response: CloudRecallResponse): unknown[] {\n  const raw =\n    response.messages ||\n    response.conversation_messages ||\n    response.history ||\n    response.conversation?.messages ||\n    [];\n\n  return Array.isArray(raw) ? raw : [];\n}\n\n/** @internal */\nexport function extractLastUserMessage(messages: Message[]): string | undefined {\n  return messages.findLast((m) => m.role === 'user')?.content;\n}\n"
  },
  {
    "path": "memori-ts/src/version.ts",
    "content": "// This file is auto-generated during CI builds.\nexport const SDK_VERSION = '0.0.0';\n"
  },
  {
    "path": "memori-ts/tests/cli/bin/router.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { main } from '../../../src/cli/router.js';\nimport { quotaCommand } from '../../../src/cli/commands/quota.js';\nimport { helpCommand } from '../../../src/cli/commands/help.js';\n\nvi.mock('../../../src/cli/commands/quota.js', () => ({\n  quotaCommand: vi.fn(),\n}));\nvi.mock('../../../src/cli/commands/help.js', () => ({\n  helpCommand: vi.fn(),\n}));\n\ndescribe('CLI Router', () => {\n  let originalArgv: string[];\n  let consoleErrorSpy: ReturnType<typeof vi.spyOn>;\n  let processExitSpy: ReturnType<typeof vi.spyOn>;\n\n  beforeEach(() => {\n    originalArgv = process.argv;\n\n    consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n    processExitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any) as any;\n\n    vi.clearAllMocks();\n  });\n\n  afterEach(() => {\n    process.argv = originalArgv;\n    vi.restoreAllMocks();\n  });\n\n  it('should route to the quota command successfully', async () => {\n    process.argv = ['node', 'cli.js', 'quota'];\n    await main();\n\n    expect(quotaCommand).toHaveBeenCalledWith([]);\n  });\n\n  it('should route to the help command when no arguments are provided', async () => {\n    process.argv = ['node', 'cli.js'];\n    await main();\n\n    expect(helpCommand).toHaveBeenCalled();\n  });\n\n  it('should print an error, show help, and exit on an unknown command', async () => {\n    process.argv = ['node', 'cli.js', 'unknown-command'];\n    await main();\n\n    expect(consoleErrorSpy).toHaveBeenCalledWith(\n      expect.stringContaining(\"Unknown command 'unknown-command'\")\n    );\n    expect(helpCommand).toHaveBeenCalled();\n    expect(processExitSpy).toHaveBeenCalledWith(1);\n  });\n\n  it('should catch unexpected errors, log them, and exit gracefully', async () => {\n    vi.mocked(quotaCommand).mockRejectedValueOnce(new Error('Explosion!'));\n\n    process.argv = ['node', 'cli.js', 'quota'];\n    await main();\n\n    expect(consoleErrorSpy).toHaveBeenCalledWith(\n      expect.stringContaining(\"Unexpected error executing command 'quota'\")\n    );\n    expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Explosion!'));\n    expect(processExitSpy).toHaveBeenCalledWith(1);\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/cli/commands/help.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { helpCommand } from '../../../src/cli/commands/help.js';\nimport * as utils from '../../../src/cli/utils.js';\n\nvi.mock('../../../src/cli/utils.js', () => ({\n  printBanner: vi.fn(),\n}));\n\ndescribe('helpCommand', () => {\n  let consoleSpy: ReturnType<typeof vi.spyOn>;\n\n  beforeEach(() => {\n    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it('should print the banner and help message', async () => {\n    await helpCommand([]);\n\n    expect(utils.printBanner).toHaveBeenCalled();\n\n    expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Usage: memori <command>'));\n    expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Available Commands:'));\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/cli/commands/quota.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { quotaCommand } from '../../../src/cli/commands/quota.js';\nimport * as utils from '../../../src/cli/utils.js';\n\nvi.mock('../../../src/cli/utils.js', () => ({\n  printBanner: vi.fn(),\n}));\n\nconst mockGet = vi.fn();\n\nvi.mock('../../../src/core/network.js', () => {\n  return {\n    Api: class MockApi {\n      get = mockGet;\n    },\n  };\n});\n\ndescribe('quotaCommand', () => {\n  let consoleLogSpy: ReturnType<typeof vi.spyOn>;\n  let consoleErrorSpy: ReturnType<typeof vi.spyOn>;\n  let processExitSpy: ReturnType<typeof vi.spyOn>;\n\n  beforeEach(() => {\n    consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});\n    consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});\n\n    processExitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any) as any;\n\n    mockGet.mockReset();\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it('should fetch and print quota details successfully', async () => {\n    mockGet.mockResolvedValue({\n      memories: { max: 5000, num: 1234 },\n      message: 'Quota is healthy',\n    });\n\n    await quotaCommand([]);\n\n    expect(utils.printBanner).toHaveBeenCalled();\n    expect(mockGet).toHaveBeenCalledWith('sdk/quota');\n\n    expect(consoleLogSpy).toHaveBeenCalledWith(\n      expect.stringContaining('Maximum # of Memories: 5,000')\n    );\n    expect(consoleLogSpy).toHaveBeenCalledWith(\n      expect.stringContaining('Current # of Memories: 1,234')\n    );\n    expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('+ Quota is healthy'));\n\n    expect(processExitSpy).not.toHaveBeenCalled();\n  });\n\n  it('should print error and exit with code 1 on API failure', async () => {\n    mockGet.mockRejectedValue(new Error('Invalid API Key'));\n\n    await quotaCommand([]);\n\n    expect(consoleErrorSpy).toHaveBeenCalledWith(\n      'Failed to fetch quota. Please check your MEMORI_API_KEY.'\n    );\n    expect(consoleErrorSpy).toHaveBeenCalledWith('Error details: Invalid API Key');\n\n    expect(processExitSpy).toHaveBeenCalledWith(1);\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/cli/utils.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { printBanner } from '../../src/cli/utils.js';\n\ndescribe('CLI Utils', () => {\n  let consoleSpy: ReturnType<typeof vi.spyOn>;\n\n  beforeEach(() => {\n    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  it('should print the ASCII banner successfully', () => {\n    printBanner();\n\n    expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('perfectam memoriam'));\n    expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('memorilabs.ai'));\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/core/config.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { Config } from '../../src/core/config.js';\n\ndescribe('Config', () => {\n  const originalEnv = process.env;\n\n  beforeEach(() => {\n    vi.resetModules();\n    process.env = { ...originalEnv };\n  });\n\n  afterEach(() => {\n    process.env = originalEnv;\n  });\n\n  it('should load API key from environment', () => {\n    process.env.MEMORI_API_KEY = 'env-key';\n    const config = new Config();\n    expect(config.apiKey).toBe('env-key');\n  });\n\n  it('should use staging URL if test mode is enabled via env', () => {\n    process.env.MEMORI_TEST_MODE = '1';\n    const config = new Config();\n    expect(config.testMode).toBe(true);\n    expect(config.baseUrl).toContain('staging-api');\n  });\n\n  it('should allow overriding base URL via environment', () => {\n    process.env.MEMORI_API_URL_BASE = 'https://custom.memori.ai';\n    const config = new Config();\n    expect(config.baseUrl).toBe('https://custom.memori.ai');\n  });\n\n  it('should default to production URL if no env vars set', () => {\n    delete process.env.MEMORI_TEST_MODE;\n    delete process.env.MEMORI_API_URL_BASE;\n    const config = new Config();\n    expect(config.baseUrl).toBe('https://api.memorilabs.ai');\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/core/network.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { Api, ApiSubdomain } from '../../src/core/network.js';\nimport { Config } from '../../src/core/config.js';\nimport {\n  MemoriApiClientError,\n  MemoriApiValidationError,\n  QuotaExceededError,\n  TimeoutError,\n} from '../../src/core/errors.js';\n\n// Mock the global fetch\nconst fetchMock = vi.fn();\nglobal.fetch = fetchMock;\n\ndescribe('Api Class', () => {\n  let config: Config;\n  let api: Api;\n\n  beforeEach(() => {\n    fetchMock.mockReset();\n    vi.useFakeTimers();\n\n    config = new Config();\n    config.apiKey = 'test-api-key';\n    config.baseUrl = 'https://api.test.com';\n    config.timeout = 5000;\n    config.testMode = true;\n\n    api = new Api(config);\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks();\n    vi.useRealTimers();\n  });\n\n  it('should initialize with the correct collector subdomain', () => {\n    const collectorApi = new Api(config, ApiSubdomain.COLLECTOR);\n    expect(collectorApi).toBeDefined();\n  });\n\n  it('should make a successful GET request', async () => {\n    fetchMock.mockResolvedValue({\n      ok: true,\n      status: 200,\n      json: async () => ({ data: 'success' }),\n    });\n\n    const result = await api.get<{ data: string }>('test-route');\n\n    expect(result).toEqual({ data: 'success' });\n    expect(fetchMock).toHaveBeenCalledWith(\n      'https://api.test.com/v1/test-route',\n      expect.objectContaining({\n        method: 'GET',\n        headers: expect.objectContaining({\n          Authorization: 'Bearer test-api-key',\n          'Content-Type': 'application/json',\n        }),\n      })\n    );\n  });\n\n  it('should handle 204 No Content', async () => {\n    fetchMock.mockResolvedValue({\n      ok: true,\n      status: 204,\n      json: async () => ({}),\n    });\n\n    const result = await api.post('test-route');\n    expect(result).toEqual({});\n  });\n\n  it('should throw QuotaExceededError on 429', async () => {\n    fetchMock.mockResolvedValue({\n      ok: false,\n      status: 429,\n      json: async () => ({ message: 'Rate limit exceeded' }),\n    });\n\n    await expect(api.get('test')).rejects.toThrow(QuotaExceededError);\n  });\n\n  it('should throw MemoriApiValidationError on 422', async () => {\n    fetchMock.mockResolvedValue({\n      ok: false,\n      status: 422,\n      json: async () => ({ detail: 'Invalid input' }),\n    });\n\n    await expect(api.post('test', {})).rejects.toThrow(MemoriApiValidationError);\n  });\n\n  it('should retry on 5xx errors and eventually fail', async () => {\n    fetchMock.mockResolvedValue({\n      ok: false,\n      status: 502,\n      json: async () => ({ message: 'Server Error' }),\n    });\n\n    const promise = api.get('test');\n\n    // Attach expectation BEFORE advancing timers to catch the rejection as it happens\n    const assertion = expect(promise).rejects.toThrow(MemoriApiClientError);\n\n    // Advance time enough to cover 5 retries with exponential backoff\n    await vi.advanceTimersByTimeAsync(40000);\n\n    await assertion;\n    expect(fetchMock).toHaveBeenCalledTimes(6);\n  });\n\n  it('should handle network timeouts and retry until failure', async () => {\n    config.timeout = 1000;\n\n    fetchMock.mockImplementation((_url, options) => {\n      const signal = options.signal;\n      return new Promise((resolve, reject) => {\n        if (signal) {\n          signal.addEventListener('abort', () => {\n            const err = new Error('Aborted');\n            err.name = 'AbortError';\n            reject(err);\n          });\n        }\n      });\n    });\n\n    const promise = api.get('test');\n\n    // Attach expectation BEFORE advancing timers\n    const assertion = expect(promise).rejects.toThrow(TimeoutError);\n\n    // Retry loop: Initial + 5 Retries.\n    // Each has 1000ms timeout + backoff delays.\n    // Total duration needed is > 30s.\n    await vi.advanceTimersByTimeAsync(40000);\n\n    await assertion;\n    expect(fetchMock).toHaveBeenCalledTimes(6);\n  });\n\n  it('should make a successful PATCH request', async () => {\n    fetchMock.mockResolvedValue({\n      ok: true,\n      status: 200,\n      json: async () => ({ updated: true }),\n    });\n\n    const result = await api.patch('test-route', { key: 'value' });\n    expect(result).toEqual({ updated: true });\n    expect(fetchMock).toHaveBeenCalledWith(\n      expect.stringContaining('test-route'),\n      expect.objectContaining({ method: 'PATCH' })\n    );\n  });\n\n  it('should make a successful DELETE request', async () => {\n    fetchMock.mockResolvedValue({\n      ok: true,\n      status: 200,\n      json: async () => ({ deleted: true }),\n    });\n\n    const result = await api.delete('test-route');\n    expect(result).toEqual({ deleted: true });\n    expect(fetchMock).toHaveBeenCalledWith(\n      expect.stringContaining('test-route'),\n      expect.objectContaining({ method: 'DELETE' })\n    );\n  });\n\n  it('should throw MemoriApiRequestRejectedError on 433', async () => {\n    const { MemoriApiRequestRejectedError } = await import('../../src/core/errors.js');\n\n    fetchMock.mockResolvedValue({\n      ok: false,\n      status: 433,\n      json: async () => ({ message: 'Rejected' }),\n    });\n\n    await expect(api.post('test', {})).rejects.toThrow(MemoriApiRequestRejectedError);\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/core/session.test.ts",
    "content": "import { describe, it, expect, beforeEach } from 'vitest';\nimport { SessionManager } from '../../src/core/session.js';\n\ndescribe('SessionManager', () => {\n  let session: SessionManager;\n\n  beforeEach(() => {\n    session = new SessionManager();\n  });\n\n  it('should generate a valid UUID on initialization', () => {\n    expect(session.id).toBeDefined();\n    expect(typeof session.id).toBe('string');\n    expect(session.id.length).toBeGreaterThan(0);\n  });\n\n  it('should reset the session ID to a new UUID', () => {\n    const originalId = session.id;\n    session.reset();\n    expect(session.id).not.toBe(originalId);\n    expect(session.id).toBeDefined();\n  });\n\n  it('should allow manually setting the session ID', () => {\n    const customId = 'custom-session-id';\n    session.set(customId);\n    expect(session.id).toBe(customId);\n  });\n\n  it('should support chaining for methods', () => {\n    const result = session.reset();\n    expect(result).toBeInstanceOf(SessionManager);\n\n    const result2 = session.set('chain-test');\n    expect(result2).toBeInstanceOf(SessionManager);\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/engines/augmentation.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { AugmentationEngine } from '../../src/engines/augmentation.js';\nimport { Api } from '../../src/core/network.js';\nimport { Config } from '../../src/core/config.js';\nimport { SessionManager } from '../../src/core/session.js';\nimport { LLMRequest, LLMResponse } from '@memorilabs/axon';\n\ndescribe('AugmentationEngine', () => {\n  let engine: AugmentationEngine;\n  let mockApi: Api;\n  let mockConfig: Config;\n  let mockSession: SessionManager;\n\n  beforeEach(() => {\n    mockApi = { post: vi.fn().mockResolvedValue({}) } as unknown as Api;\n    mockConfig = {\n      entityId: 'u-1',\n      processId: 'p-1',\n      testMode: true,\n    } as unknown as Config;\n    mockSession = { id: 'sess-1' } as unknown as SessionManager;\n\n    engine = new AugmentationEngine(mockApi, mockConfig, mockSession);\n  });\n\n  it('should trigger API post on handleAugmentation', async () => {\n    const req = { messages: [{ role: 'user', content: 'learn this' }] } as unknown as LLMRequest;\n    const res = { content: 'ok' } as LLMResponse;\n\n    const mockCtx = {\n      traceId: '123',\n      startedAt: new Date(),\n      metadata: {\n        provider: 'openai',\n        sdkVersion: '4.28.0',\n        platform: null,\n        framework: null,\n      },\n    } as any;\n\n    await engine.handleAugmentation(req, res, mockCtx);\n\n    expect(mockApi.post).toHaveBeenCalledWith(\n      'cloud/augmentation',\n      expect.objectContaining({\n        conversation: expect.objectContaining({\n          messages: [\n            { role: 'user', content: 'learn this' },\n            { role: 'assistant', content: 'ok' },\n          ],\n        }),\n      })\n    );\n  });\n\n  it('should log warning in testMode if API fails', async () => {\n    (mockApi.post as any).mockRejectedValue(new Error('Augment fail'));\n    const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});\n\n    const req = { messages: [{ role: 'user', content: 'hi' }] } as unknown as LLMRequest;\n    const res = { content: 'ho' } as LLMResponse;\n\n    const mockCtx = {\n      traceId: '123',\n      startedAt: new Date(),\n      metadata: {\n        provider: 'openai',\n        sdkVersion: '4.28.0',\n        platform: null,\n        framework: null,\n      },\n    } as any;\n\n    await engine.handleAugmentation(req, res, mockCtx);\n\n    // Wait for the fire-and-forget promise\n    await new Promise(process.nextTick);\n\n    expect(consoleSpy).toHaveBeenCalled();\n    consoleSpy.mockRestore();\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/engines/persistence.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { PersistenceEngine } from '../../src/engines/persistence.js';\nimport { Api } from '../../src/core/network.js';\nimport { Config } from '../../src/core/config.js';\nimport { SessionManager } from '../../src/core/session.js';\nimport { LLMRequest, LLMResponse } from '@memorilabs/axon';\n\ndescribe('PersistenceEngine', () => {\n  let engine: PersistenceEngine;\n  let mockApi: Api;\n  let mockConfig: Config;\n  let mockSession: SessionManager;\n\n  beforeEach(() => {\n    mockApi = { post: vi.fn().mockResolvedValue({}) } as unknown as Api;\n    mockConfig = { entityId: 'u-1', processId: 'p-1' } as unknown as Config;\n    mockSession = { id: 'sess-1' } as unknown as SessionManager;\n\n    engine = new PersistenceEngine(mockApi, mockConfig, mockSession);\n  });\n\n  it('should return response immediately if no session ID', async () => {\n    (mockSession as any).id = undefined;\n    const req = { messages: [] } as unknown as LLMRequest;\n    const res = { content: 'response' } as LLMResponse;\n\n    await engine.handlePersistence(req, res, {} as any);\n    expect(mockApi.post).not.toHaveBeenCalled();\n  });\n\n  it('should post conversation to API if valid user message exists', async () => {\n    const req = {\n      messages: [\n        { role: 'system', content: 'sys' },\n        { role: 'user', content: 'hello' },\n      ],\n    } as unknown as LLMRequest;\n    const res = { content: 'world' } as LLMResponse;\n\n    await engine.handlePersistence(req, res, {} as any);\n\n    expect(mockApi.post).toHaveBeenCalledWith(\n      'cloud/conversation/messages',\n      expect.objectContaining({\n        session: { id: 'sess-1' },\n        messages: [\n          { role: 'user', type: 'text', text: 'hello' },\n          { role: 'assistant', type: 'text', text: 'world' },\n        ],\n      })\n    );\n  });\n\n  it('should handle API errors gracefully (no throw)', async () => {\n    (mockApi.post as any).mockRejectedValue(new Error('fail'));\n    const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});\n\n    const req = { messages: [{ role: 'user', content: 'hi' }] } as unknown as LLMRequest;\n    const res = { content: 'ho' } as LLMResponse;\n\n    await expect(engine.handlePersistence(req, res, {} as any)).resolves.toEqual(res);\n    expect(consoleSpy).toHaveBeenCalled();\n  });\n\n  it('should not post if no user message is found in history', async () => {\n    const req = {\n      messages: [{ role: 'system', content: 'sys' }],\n    } as unknown as LLMRequest;\n    const res = { content: 'resp' } as LLMResponse;\n\n    await engine.handlePersistence(req, res, {} as any);\n    expect(mockApi.post).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/engines/recall.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { RecallEngine } from '../../src/engines/recall.js';\nimport { Api } from '../../src/core/network.js';\nimport { Config } from '../../src/core/config.js';\nimport { SessionManager } from '../../src/core/session.js';\nimport { LLMRequest } from '@memorilabs/axon';\n\ndescribe('RecallEngine', () => {\n  let recallEngine: RecallEngine;\n  let mockApi: Api;\n  let mockConfig: Config;\n  let mockSession: SessionManager;\n\n  beforeEach(() => {\n    mockApi = { post: vi.fn() } as unknown as Api;\n    mockConfig = {\n      entityId: 'test-entity',\n      processId: 'test-process',\n      recallRelevanceThreshold: 0.5,\n    } as unknown as Config;\n    mockSession = { id: 'test-session-id' } as unknown as SessionManager;\n\n    recallEngine = new RecallEngine(mockApi, mockConfig, mockSession);\n  });\n\n  describe('recall()', () => {\n    it('should call API with correct payload', async () => {\n      (mockApi.post as any).mockResolvedValue({ facts: ['fact1'] });\n      const result = await recallEngine.recall('query');\n      expect(result).toHaveLength(1);\n      expect(mockApi.post).toHaveBeenCalled();\n    });\n  });\n\n  describe('handleRecall()', () => {\n    it('should inject context into system prompt if facts are relevant', async () => {\n      (mockApi.post as any).mockResolvedValue({\n        facts: [{ content: 'User likes apples', rank_score: 0.9 }],\n      });\n\n      const req = {\n        messages: [\n          { role: 'system', content: 'You are helpful.' },\n          { role: 'user', content: 'What do I like?' },\n        ],\n      } as unknown as LLMRequest;\n\n      const newReq = await recallEngine.handleRecall(req, {} as any);\n\n      const systemMsg = newReq.messages.find((m) => m.role === 'system');\n      expect(systemMsg?.content).toContain('User likes apples');\n      expect(systemMsg?.content).toContain('<memori_context>');\n    });\n\n    it('should prepend history if API returns conversation history', async () => {\n      (mockApi.post as any).mockResolvedValue({\n        facts: [],\n        messages: [\n          { role: 'user', content: 'past msg' },\n          { role: 'assistant', content: 'past answer' },\n        ],\n      });\n\n      const req = {\n        messages: [{ role: 'user', content: 'current msg' }],\n      } as unknown as LLMRequest;\n\n      const newReq = await recallEngine.handleRecall(req, {} as any);\n\n      expect(newReq.messages).toHaveLength(3);\n      expect(newReq.messages[0].content).toBe('past msg');\n    });\n\n    it('should fail silently and return original request on API error', async () => {\n      (mockApi.post as any).mockRejectedValue(new Error('Network fail'));\n      const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});\n\n      const req = { messages: [{ role: 'user', content: 'hi' }] } as unknown as LLMRequest;\n      const newReq = await recallEngine.handleRecall(req, {} as any);\n\n      expect(newReq).toBe(req);\n      expect(consoleSpy).toHaveBeenCalled();\n\n      consoleSpy.mockRestore();\n    });\n\n    it('should create a new system message if one does not exist', async () => {\n      (mockApi.post as any).mockResolvedValue({\n        facts: [{ content: 'Fact', rank_score: 0.9 }],\n      });\n\n      // Request WITHOUT a system message\n      const req = {\n        messages: [{ role: 'user', content: 'Query' }],\n      } as unknown as LLMRequest;\n\n      const newReq = await recallEngine.handleRecall(req, {} as any);\n\n      // Verify a system message was added to the front\n      expect(newReq.messages[0].role).toBe('system');\n      expect(newReq.messages[0].content).toContain('Fact');\n    });\n\n    it('should return original request if no user message is found', async () => {\n      // Empty messages array\n      const req = { messages: [] } as unknown as LLMRequest;\n      const newReq = await recallEngine.handleRecall(req, {} as any);\n      expect(newReq).toBe(req);\n    });\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/integrations/base.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { BaseIntegration } from '../../src/integrations/base.js';\nimport { IntegrationRequest, MemoriCore } from '../../src/types/integrations.js';\nimport { LLMRequest } from '@memorilabs/axon';\n\n// Create a concrete implementation to test the protected methods of the abstract class\nclass TestIntegration extends BaseIntegration {\n  public testCapture(req: IntegrationRequest) {\n    return this.executeAugmentation(req);\n  }\n  public testRecall(userMessage: string) {\n    return this.executeRecall(userMessage);\n  }\n}\n\ndescribe('BaseIntegration', () => {\n  let mockCore: MemoriCore;\n  let integration: TestIntegration;\n  let consoleWarnSpy: ReturnType<typeof vi.spyOn>;\n\n  beforeEach(() => {\n    mockCore = {\n      recall: { handleRecall: vi.fn() },\n      persistence: { handlePersistence: vi.fn() },\n      augmentation: { handleAugmentation: vi.fn() },\n      config: { entityId: 'test-user', processId: 'test-process' },\n      session: { id: 'test-session-id' },\n    } as unknown as MemoriCore;\n\n    integration = new TestIntegration(mockCore);\n    consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleWarnSpy.mockRestore();\n  });\n\n  describe('executeCapture()', () => {\n    it('should silently abort if no session ID is present', async () => {\n      (mockCore.session as any).id = undefined;\n\n      const req = { userMessage: 'user msg', agentResponse: 'ai msg' };\n\n      await integration.testCapture(req);\n\n      expect(mockCore.persistence.handlePersistence).not.toHaveBeenCalled();\n      expect(mockCore.augmentation.handleAugmentation).not.toHaveBeenCalled();\n    });\n\n    it('should format requests and invoke engines, properly passing metadata', async () => {\n      const req: IntegrationRequest = {\n        userMessage: 'hello bot',\n        agentResponse: 'hello human',\n        metadata: {\n          provider: 'openclaw',\n          model: 'gpt-4o',\n          platform: 'openclaw',\n          sdkVersion: null,\n          integrationSdkVersion: '1.0.0',\n        },\n      };\n\n      await integration.testCapture(req);\n\n      const expectedReq = expect.objectContaining({\n        messages: [{ role: 'user', content: 'hello bot' }],\n        model: 'gpt-4o',\n      });\n      const expectedRes = expect.objectContaining({\n        content: 'hello human',\n      });\n      const expectedCtx = expect.objectContaining({\n        traceId: expect.stringContaining('integration-trace-'),\n        metadata: req.metadata,\n      });\n\n      expect(mockCore.persistence.handlePersistence).toHaveBeenCalledWith(\n        expectedReq,\n        expectedRes,\n        expectedCtx\n      );\n      expect(mockCore.augmentation.handleAugmentation).toHaveBeenCalledWith(\n        expectedReq,\n        expectedRes,\n        expectedCtx\n      );\n    });\n\n    it('should swallow errors and log a warning if engines fail', async () => {\n      (mockCore.persistence.handlePersistence as any).mockRejectedValue(\n        new Error('Persistence failed')\n      );\n\n      const req = { userMessage: 'msg', agentResponse: 'resp' };\n\n      // Should not throw\n      await expect(integration.testCapture(req)).resolves.toBeUndefined();\n\n      expect(consoleWarnSpy).toHaveBeenCalledWith(\n        'Memori Integration Capture failed:',\n        expect.any(Error)\n      );\n    });\n  });\n\n  describe('executeRecall()', () => {\n    it('should return undefined if no session ID is present', async () => {\n      (mockCore.session as any).id = undefined;\n\n      const result = await integration.testRecall('who am i?');\n\n      expect(result).toBeUndefined();\n      expect(mockCore.recall.handleRecall).not.toHaveBeenCalled();\n    });\n\n    it('should format the request, invoke the recall engine, and extract the system message', async () => {\n      const mockUpdatedReq: LLMRequest = {\n        messages: [\n          { role: 'system', content: '<memori_context>You like apples.</memori_context>' },\n          { role: 'user', content: 'what do I like?' },\n        ],\n      };\n      (mockCore.recall.handleRecall as any).mockResolvedValue(mockUpdatedReq);\n\n      const result = await integration.testRecall('what do I like?');\n\n      expect(mockCore.recall.handleRecall).toHaveBeenCalledWith(\n        expect.objectContaining({\n          messages: [{ role: 'user', content: 'what do I like?' }],\n        }),\n        expect.objectContaining({\n          traceId: expect.stringContaining('integration-trace-'),\n          metadata: {},\n        })\n      );\n      expect(result).toBe('<memori_context>You like apples.</memori_context>');\n    });\n\n    it('should return undefined if the recall engine does not inject a system message', async () => {\n      const mockUpdatedReq: LLMRequest = {\n        messages: [{ role: 'user', content: 'what do I like?' }],\n      };\n      (mockCore.recall.handleRecall as any).mockResolvedValue(mockUpdatedReq);\n\n      const result = await integration.testRecall('what do I like?');\n\n      expect(result).toBeUndefined();\n    });\n\n    it('should swallow errors, log a warning, and return undefined on failure', async () => {\n      (mockCore.recall.handleRecall as any).mockRejectedValue(new Error('Recall failed'));\n\n      const result = await integration.testRecall('query');\n\n      expect(result).toBeUndefined();\n      expect(consoleWarnSpy).toHaveBeenCalledWith(\n        'Memori Integration Recall failed:',\n        expect.any(Error)\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/integrations/openclaw.test.ts",
    "content": "import { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { OpenClawIntegration } from '../../src/integrations/openclaw.js';\nimport { MemoriCore } from '../../src/types/integrations.js';\n\ndescribe('OpenClawIntegration', () => {\n  let mockCore: MemoriCore;\n  let openclaw: OpenClawIntegration;\n\n  beforeEach(() => {\n    mockCore = {\n      recall: {} as any,\n      persistence: {} as any,\n      augmentation: {} as any,\n      config: { entityId: undefined, processId: undefined },\n      session: {\n        id: 'default-session-id',\n        set: vi.fn().mockReturnThis(),\n      },\n    } as unknown as MemoriCore;\n\n    openclaw = new OpenClawIntegration(mockCore);\n  });\n\n  describe('setAttribution()', () => {\n    it('should update entityId and return instance for chaining', () => {\n      const result = openclaw.setAttribution('user-123');\n\n      expect(mockCore.config.entityId).toBe('user-123');\n      expect(mockCore.config.processId).toBeUndefined();\n      expect(result).toBe(openclaw); // Chainable\n    });\n\n    it('should update both entityId and processId', () => {\n      openclaw.setAttribution('user-123', 'openclaw-agent');\n\n      expect(mockCore.config.entityId).toBe('user-123');\n      expect(mockCore.config.processId).toBe('openclaw-agent');\n    });\n  });\n\n  describe('setSession()', () => {\n    it('should delegate to core session manager and return instance for chaining', () => {\n      const result = openclaw.setSession('custom-session-uuid');\n\n      expect(mockCore.session.set).toHaveBeenCalledWith('custom-session-uuid');\n      expect(result).toBe(openclaw); // Chainable\n    });\n  });\n\n  describe('capture()', () => {\n    it('should delegate to the inherited augmentation method', async () => {\n      // Spy on the protected method inherited from BaseIntegration\n      const augmentationSpy = vi\n        .spyOn(openclaw as any, 'augmentation')\n        .mockResolvedValue(undefined);\n\n      const req = { userMessage: 'user says hi', agentResponse: 'bot says hello' };\n\n      await openclaw.augmentation(req);\n\n      expect(augmentationSpy).toHaveBeenCalledWith(req);\n    });\n  });\n\n  describe('recall()', () => {\n    it('should delegate to the inherited executeRecall method and return the result', async () => {\n      const mockMemoryContext = '<memori_context>context data</memori_context>';\n\n      // Spy on the protected method inherited from BaseIntegration\n      const executeRecallSpy = vi\n        .spyOn(openclaw as any, 'executeRecall')\n        .mockResolvedValue(mockMemoryContext);\n\n      const result = await openclaw.recall('prompt text');\n\n      expect(executeRecallSpy).toHaveBeenCalledWith('prompt text');\n      expect(result).toBe(mockMemoryContext);\n    });\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/memori.test.ts",
    "content": "import { describe, it, expect, vi } from 'vitest';\nimport { Memori } from '../src/memori.js';\nimport { SessionManager } from '../src/core/session.js';\nimport { Config } from '../src/core/config.js';\n\ndescribe('Memori SDK', () => {\n  it('should instantiate with default components', () => {\n    const memori = new Memori();\n\n    expect(memori.config).toBeInstanceOf(Config);\n    expect(memori.session).toBeInstanceOf(SessionManager);\n    expect(memori.axon).toBeDefined();\n    expect(memori.llm).toBeDefined();\n  });\n\n  it('should update attribution config correctly', () => {\n    const memori = new Memori();\n\n    memori.attribution('user-123', 'process-xyz');\n\n    expect(memori.config.entityId).toBe('user-123');\n    expect(memori.config.processId).toBe('process-xyz');\n  });\n\n  it('should reset session correctly', () => {\n    const memori = new Memori();\n    const oldId = memori.session.id;\n\n    memori.resetSession();\n\n    expect(memori.session.id).not.toBe(oldId);\n  });\n\n  it('should set session correctly', () => {\n    const memori = new Memori();\n    const specificId = 'uuid-123-456';\n\n    memori.setSession(specificId);\n\n    expect(memori.session.id).toBe(specificId);\n  });\n\n  it('should register an LLM client via the llm helper', () => {\n    const memori = new Memori();\n    // Spy on the internal axon.llm.register method and mock implementation\n    // to prevent the real registry from throwing validation errors on our mock object\n    const registerSpy = vi.spyOn(memori.axon.llm, 'register').mockImplementation(() => ({}) as any);\n    const mockClient = { name: 'mock-client' };\n\n    const result = memori.llm.register(mockClient);\n\n    expect(registerSpy).toHaveBeenCalledWith(mockClient);\n    expect(result).toBe(memori); // Check chaining\n  });\n});\n"
  },
  {
    "path": "memori-ts/tests/utils/utils.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport {\n  stringifyContent,\n  formatDate,\n  extractFacts,\n  extractHistory,\n  extractLastUserMessage,\n} from '../../src/utils/utils.js';\nimport { Message } from '@memorilabs/axon';\n\ndescribe('Utils', () => {\n  describe('formatDate', () => {\n    it('should format valid ISO strings', () => {\n      const input = '2023-10-25T14:30:00.000Z';\n      const output = formatDate(input);\n      expect(output).toBe('2023-10-25 14:30');\n    });\n\n    it('should return undefined for undefined input', () => {\n      expect(formatDate(undefined)).toBeUndefined();\n    });\n\n    it('should return substring if date parsing fails but string exists', () => {\n      const invalidDate = 'not-a-date-string-that-is-long';\n      expect(formatDate(invalidDate)).toBe('not-a-date-strin');\n    });\n  });\n\n  describe('stringifyContent', () => {\n    it('should return string as is', () => {\n      expect(stringifyContent('hello')).toBe('hello');\n    });\n\n    it('should handle array of strings', () => {\n      expect(stringifyContent(['a', 'b'])).toBe('a\\nb');\n    });\n\n    it('should handle array of objects (LLM content blocks)', () => {\n      const input = [{ text: 'part1' }, { content: 'part2' }];\n      expect(stringifyContent(input)).toBe('part1\\npart2');\n    });\n\n    it('should handle single object', () => {\n      expect(stringifyContent({ text: 'hello' })).toBe('hello');\n    });\n\n    it('should fallback to JSON stringify for unknown objects', () => {\n      expect(stringifyContent({ other: 'value' })).toContain('{\"other\":\"value\"}');\n    });\n  });\n\n  describe('extractFacts', () => {\n    it('should extract strings directly', () => {\n      const response = { facts: ['fact1', 'fact2'] };\n      const result = extractFacts(response);\n      expect(result).toHaveLength(2);\n      expect(result[0].content).toBe('fact1');\n      expect(result[0].score).toBe(1.0);\n    });\n\n    it('should extract structured objects using rank_score', () => {\n      const response = {\n        results: [{ content: 'fact1', rank_score: 0.8, date_created: '2023-01-01T12:00:00Z' }],\n      };\n      const result = extractFacts(response);\n      expect(result[0].score).toBe(0.8);\n      expect(result[0].dateCreated).toBeDefined();\n    });\n\n    it('should fallback to similarity if rank_score is missing', () => {\n      const response = {\n        results: [{ content: 'fact2', similarity: 0.65, date_created: '2023-01-01T12:00:00Z' }],\n      };\n      const result = extractFacts(response);\n      expect(result[0].score).toBe(0.65);\n    });\n\n    it('should ignore objects without a valid content string', () => {\n      const response = {\n        results: [{ missing_content: 'foo' }, { content: 'valid fact', rank_score: 0.9 }],\n      } as any;\n\n      const result = extractFacts(response);\n      expect(result).toHaveLength(1);\n      expect(result[0].content).toBe('valid fact');\n    });\n  });\n\n  describe('extractHistory', () => {\n    it('should extract from messages key', () => {\n      const response = { messages: ['msg1'] };\n      expect(extractHistory(response)).toEqual(['msg1']);\n    });\n\n    it('should extract from conversation.messages key', () => {\n      const response = { conversation: { messages: ['msg2'] } };\n      expect(extractHistory(response)).toEqual(['msg2']);\n    });\n\n    it('should extract from history key', () => {\n      const response = { history: ['msg3'] };\n      expect(extractHistory(response)).toEqual(['msg3']);\n    });\n\n    it('should return empty array if no history found', () => {\n      expect(extractHistory({})).toEqual([]);\n    });\n  });\n\n  describe('extractLastUserMessage', () => {\n    it('should extract the content of the last user message', () => {\n      const messages: Message[] = [\n        { role: 'user', content: 'first user message' },\n        { role: 'assistant', content: 'assistant reply' },\n        { role: 'user', content: 'second user message' },\n        { role: 'system', content: 'system message' },\n      ];\n      expect(extractLastUserMessage(messages)).toBe('second user message');\n    });\n\n    it('should return undefined if there are no user messages', () => {\n      const messages: Message[] = [\n        { role: 'assistant', content: 'assistant reply' },\n        { role: 'system', content: 'system message' },\n      ];\n      expect(extractLastUserMessage(messages)).toBeUndefined();\n    });\n\n    it('should return undefined for an empty messages array', () => {\n      expect(extractLastUserMessage([])).toBeUndefined();\n    });\n  });\n});\n"
  },
  {
    "path": "memori-ts/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"src\",\n    \"outDir\": \"dist\",\n    \"types\": [\"node\"],\n    \"declaration\": true,\n    \"sourceMap\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\n    \"tests\",\n    \"examples\",\n    \"**/*.test.ts\",\n    \"dist\",\n    \"node_modules\",\n    \"vitest.config.ts\",\n    \"eslint.config.js\"\n  ]\n}\n"
  },
  {
    "path": "memori-ts/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2023\",\n    \"lib\": [\"ES2023\", \"DOM\"],\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \".\",\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"types\": [\"node\"],\n    \"forceConsistentCasingInFileNames\": true,\n    \"baseUrl\": \".\"\n  },\n  \"include\": [\"src\", \"tests\", \"examples\", \"vitest.config.ts\"],\n  \"exclude\": [\"dist\", \"node_modules\"]\n}\n"
  },
  {
    "path": "memori-ts/vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\nimport path from 'node:path';\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: 'node',\n    coverage: {\n      provider: 'v8',\n      reporter: ['text', 'json', 'html'],\n      include: ['src/**'],\n      exclude: [\n        'node_modules/',\n        'dist/',\n        'tests/',\n        'examples/',\n        '**/*.config.ts',\n        '**/*.d.ts',\n        '**/types/**',\n        '**/index.ts',\n        'src/bin/cli.ts',\n      ],\n    },\n    include: ['tests/**/*.test.ts'],\n    exclude: ['node_modules/', 'dist/'],\n  },\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, './src'),\n    },\n  },\n});\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools>=60\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"memori\"\nversion = \"3.2.3\"\ndescription = \"Memori Python SDK\"\nauthors = [{name = \"Memori Labs Team\", email = \"noc@memorilabs.ai\"}]\nlicense = {text = \"Apache-2.0\"}\nreadme = {file = \"README.md\", content-type = \"text/markdown\"}\nrequires-python = \">=3.10\"\nkeywords = [\"ai\", \"memory\", \"agents\", \"llm\", \"artificial-intelligence\", \"multi-agent\"]\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: Apache Software License\",\n    \"Operating System :: OS Independent\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Scientific/Engineering :: Artificial Intelligence\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n    \"Topic :: Database :: Database Engines/Servers\",\n    \"Typing :: Typed\",\n]\n\ndependencies = [\n    \"aiohttp>=3.9.0\",\n    \"botocore>=1.34.0\",\n    \"faiss-cpu>=1.7.0\",\n    \"grpcio>=1.60.0\",\n    \"numpy>=1.24.0\",\n    \"protobuf>=4.25.0,<6.0.0\",\n    \"pyfiglet>=0.8.0\",\n    \"requests>=2.32.5\",\n    \"sentence-transformers>=3.0.0\",\n]\n\n[project.optional-dependencies]\ncockroachdb = [\"psycopg[binary]>=3.1.0\"]\n\n[tool.setuptools.packages.find]\nwhere = [\".\"]\ninclude = [\"memori*\"]\nexclude = [\"tests*\", \"docs*\", \"*.egg-info\", \"memori-ts*\", \"integrations*\"]\n\n[tool.setuptools.package-data]\nmemori = [\"py.typed\", \"models/**/*\"]\n\n[project.urls]\nHomepage = \"https://memorilabs.ai\"\nDocumentation = \"https://memorilabs.ai/docs/\"\nRepository = \"https://github.com/MemoriLabs/Memori\"\n\"Bug Tracker\" = \"https://github.com/MemoriLabs/Memori/issues\"\n\"Changelog\" = \"https://github.com/MemoriLabs/Memori/blob/main/CHANGELOG.md\"\n\"Contributing\" = \"https://github.com/MemoriLabs/Memori/blob/main/CONTRIBUTING.md\"\n\n[tool.ruff]\nline-length = 88\ntarget-version = \"py310\"\n\n[tool.ruff.lint]\nselect = [\n    \"E\",   # pycodestyle errors\n    \"W\",   # pycodestyle warnings\n    \"F\",   # pyflakes\n    \"I\",   # isort\n    \"B\",   # flake8-bugbear\n    \"C4\",  # flake8-comprehensions\n    \"UP\",  # pyupgrade\n]\nignore = [\n    \"E501\",  # line too long (handled by formatter)\n]\n\n[tool.ruff.lint.per-file-ignores]\n\"memori/storage/__init__.py\" = [\"I001\"]  # Import order matters for adapter registration\n\n[tool.ruff.format]\nquote-style = \"double\"\nindent-style = \"space\"\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\npython_files = [\"test_*.py\", \"*_test.py\"]\npython_classes = [\"Test*\"]\npython_functions = [\"test_*\"]\nmarkers = [\n    \"asyncio: marks tests as async (deselect with '-m \\\"not asyncio\\\"')\",\n    \"benchmark: marks tests as performance benchmarks\",\n    \"integration: marks tests as integration tests requiring external API keys\",\n]\nasyncio_mode = \"auto\"\naddopts = [\n    \"-v\",\n    \"--strict-markers\",\n    \"--ignore=tests/benchmarks\",\n    \"--ignore=tests/integration\",\n    \"-m\",\n    \"not benchmark\",\n    \"--cov=memori\",\n    \"--cov-report=term-missing\",\n    \"--cov-report=html\",\n    \"--cov-report=xml\",\n]\nfilterwarnings = [\n    \"ignore::DeprecationWarning\",\n    \"ignore::pytest.PytestUnraisableExceptionWarning\",\n    \"ignore::UserWarning:langsmith\",\n]\n\n[tool.coverage.run]\nsource = [\"memori\"]\nomit = [\n    \"*/tests/*\",\n    \"*/__pycache__/*\",\n    \"*/site-packages/*\",\n]\n\n[tool.coverage.report]\nexclude_lines = [\n    \"pragma: no cover\",\n    \"def __repr__\",\n    \"raise AssertionError\",\n    \"raise NotImplementedError\",\n    \"if __name__ == .__main__.:\",\n    \"if TYPE_CHECKING:\",\n]\n\n[tool.ty.src]\nexclude = [\n    \"tests/**/*.py\",\n    \"**/__pycache__/**\",\n]\n\n[tool.ty.environment]\npython-version = \"3.12\"\n\n[tool.ty.rules]\npossibly-missing-attribute = \"ignore\"\nunresolved-attribute = \"ignore\"\n\n\n[dependency-groups]\ndev = [\n    \"agno>=2.0.0\",\n    \"anthropic>=0.71.0\",\n    \"bandit>=1.8.0\",\n    \"google-genai>=1.46.0\",\n    \"langchain-community>=0.3.0\",\n    \"langchain-core>=1.0.1\",\n    \"langchain-google-genai>=3.0.0\",\n    \"langchain-google-vertexai>=2.0.0\",\n    \"langchain-openai>=1.0.1\",\n    \"openai>=2.6.0\",\n    \"oracledb>=3.0.0\",\n    \"pip-audit>=2.8.0\",\n    \"pre-commit>=4.0.0\",\n    \"psycopg2-binary>=2.9.0\",\n    \"psycopg[binary]>=3.1.0\",\n    \"pymongo>=4.15.3\",\n    \"pymysql>=1.1.2\",\n    \"pytest>=8.4.2\",\n    \"pytest-asyncio>=0.24.0\",\n    \"pytest-benchmark>=4.0.0\",\n    \"pytest-cov>=6.0.0\",\n    \"pytest-mock>=3.15.1\",\n    \"psutil>=5.9.0\",\n    \"requests>=2.32.5\",\n    \"ruff>=0.8.0\",\n    \"sqlalchemy>=2.0.44\",\n    \"sqlalchemy-cockroachdb>=1.4.0\",\n    \"xai-sdk>=1.3.1\",\n    \"langchain-aws>=0.2.0\",\n    \"pyobvector>=0.2.1\",\n]\n"
  },
  {
    "path": "tests/benchmarks_locomo/test_locomo_aa_pairing.py",
    "content": "from __future__ import annotations\n\nfrom benchmarks.locomo._run_impl import _build_per_pair_requests\n\n\ndef test_build_per_pair_requests_cumulative() -> None:\n    msgs = [\n        {\"role\": \"user\", \"content\": \"u0\"},\n        {\"role\": \"assistant\", \"content\": \"a0\"},\n        {\"role\": \"user\", \"content\": \"u1\"},\n        {\"role\": \"assistant\", \"content\": \"a1\"},\n    ]\n    turn_ids = [\"D1:1\", \"D1:2\", \"D1:3\", \"D1:4\"]\n    reqs = _build_per_pair_requests(msgs, turn_ids)\n    assert len(reqs) == 2\n    assert [m[\"content\"] for m in reqs[0].messages] == [\"u0\", \"a0\"]\n    assert reqs[0].pair_turn_ids == (\"D1:1\", \"D1:2\")\n    assert [m[\"content\"] for m in reqs[1].messages] == [\"u0\", \"a0\", \"u1\", \"a1\"]\n    assert reqs[1].pair_turn_ids == (\"D1:3\", \"D1:4\")\n\n\ndef test_build_per_pair_requests_skips_unpairable_pair_boundaries() -> None:\n    msgs = [\n        {\"role\": \"assistant\", \"content\": \"a0\"},\n        {\"role\": \"user\", \"content\": \"u0\"},\n        {\"role\": \"user\", \"content\": \"u1\"},\n        {\"role\": \"assistant\", \"content\": \"a1\"},\n        {\"role\": \"user\", \"content\": \"u2\"},\n    ]\n    turn_ids = [\"D1:1\", \"D1:2\", \"D1:3\", \"D1:4\", \"D1:5\"]\n    reqs = _build_per_pair_requests(msgs, turn_ids)\n    assert len(reqs) == 1\n    # Context includes everything up to the paired assistant message.\n    assert [m[\"content\"] for m in reqs[0].messages] == [\"a0\", \"u0\", \"u1\", \"a1\"]\n    assert reqs[0].pair_turn_ids == (\"D1:3\", \"D1:4\")\n"
  },
  {
    "path": "tests/benchmarks_locomo/test_locomo_loader.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom pathlib import Path\n\nfrom benchmarks.locomo.loader import load_locomo_json\n\n\ndef _write_locomo_tiny(path: Path) -> Path:\n    data = [\n        {\n            \"sample_id\": \"sample-001\",\n            \"conversation\": [\n                {\n                    \"session_id\": \"session-1\",\n                    \"dialogue\": [\n                        {\n                            \"turn_id\": \"t0\",\n                            \"speaker\": \"user\",\n                            \"text\": \"My favorite color is blue.\",\n                        },\n                        {\"turn_id\": \"t1\", \"speaker\": \"assistant\", \"text\": \"Got it.\"},\n                    ],\n                }\n            ],\n            \"qa\": [\n                {\n                    \"question_id\": \"q0\",\n                    \"question\": \"What is my favorite color?\",\n                    \"answer\": \"blue\",\n                    \"evidence\": 0,\n                },\n                {\n                    \"question_id\": \"q1\",\n                    \"question\": \"Which color do I like best?\",\n                    \"answer\": \"blue\",\n                    \"evidence\": 0,\n                },\n            ],\n        }\n    ]\n    path.write_text(json.dumps(data), encoding=\"utf-8\")\n    return path\n\n\ndef test_load_locomo_tiny_fixture(tmp_path: Path):\n    path = _write_locomo_tiny(tmp_path / \"locomo_tiny.json\")\n    samples = load_locomo_json(path)\n\n    assert len(samples) == 1\n    sample = samples[0]\n    assert sample.sample_id == \"sample-001\"\n    assert len(sample.sessions) == 1\n    assert len(sample.sessions[0].turns) == 2\n    assert len(sample.qa) == 2\n"
  },
  {
    "path": "tests/benchmarks_locomo/test_locomo_preprocess.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom pathlib import Path\n\nfrom benchmarks.locomo.preprocess import preprocess_locomo_json\n\n\ndef test_preprocess_strips_multimodal_fields(tmp_path: Path) -> None:\n    src = tmp_path / \"in.json\"\n    dst = tmp_path / \"out.json\"\n    src.write_text(\n        json.dumps(\n            [\n                {\n                    \"sample_id\": \"conv-1\",\n                    \"conversation\": {\n                        \"speaker_a\": \"A\",\n                        \"speaker_b\": \"B\",\n                        \"session_1\": [\n                            {\"speaker\": \"A\", \"dia_id\": \"D1:1\", \"text\": \"hi\"},\n                            {\n                                \"speaker\": \"B\",\n                                \"dia_id\": \"D1:2\",\n                                \"text\": \"look\",\n                                \"img_url\": [\"https://example.com/x.jpg\"],\n                            },\n                        ],\n                    },\n                    \"qa\": [],\n                }\n            ]\n        ),\n        encoding=\"utf-8\",\n    )\n\n    stats = preprocess_locomo_json(src, dst)\n    assert stats[\"samples_in\"] == 1\n    assert stats[\"samples_out\"] == 1\n    assert stats[\"removed_multimodal_turns\"] == 1\n\n    out = json.loads(dst.read_text(encoding=\"utf-8\"))\n    turns = out[0][\"conversation\"][\"session_1\"]\n    assert \"img_url\" not in turns[1]\n\n\ndef test_preprocess_rewrites_speaker_b_to_assistant(tmp_path: Path) -> None:\n    src = tmp_path / \"in.json\"\n    dst = tmp_path / \"out.json\"\n    src.write_text(\n        json.dumps(\n            [\n                {\n                    \"sample_id\": \"conv-1\",\n                    \"conversation\": {\n                        \"speaker_a\": \"A\",\n                        \"speaker_b\": \"B\",\n                        \"session_1\": [\n                            {\"speaker\": \"A\", \"dia_id\": \"D1:1\", \"text\": \"hi\"},\n                            {\"speaker\": \"B\", \"dia_id\": \"D1:2\", \"text\": \"hello\"},\n                        ],\n                    },\n                    \"qa\": [],\n                }\n            ]\n        ),\n        encoding=\"utf-8\",\n    )\n\n    preprocess_locomo_json(src, dst)\n    out = json.loads(dst.read_text(encoding=\"utf-8\"))\n    turns = out[0][\"conversation\"][\"session_1\"]\n    assert turns[0][\"speaker\"] == \"user\"\n    assert turns[1][\"speaker\"] == \"assistant\"\n\n\ndef test_preprocess_keeps_speakers_when_speaker_b_missing(tmp_path: Path) -> None:\n    src = tmp_path / \"in.json\"\n    dst = tmp_path / \"out.json\"\n    src.write_text(\n        json.dumps(\n            [\n                {\n                    \"sample_id\": \"conv-1\",\n                    \"conversation\": {\n                        \"speaker_a\": \"A\",\n                        \"session_1\": [\n                            {\"speaker\": \"A\", \"dia_id\": \"D1:1\", \"text\": \"hi\"},\n                            {\"speaker\": \"B\", \"dia_id\": \"D1:2\", \"text\": \"hello\"},\n                        ],\n                    },\n                    \"qa\": [],\n                }\n            ]\n        ),\n        encoding=\"utf-8\",\n    )\n\n    preprocess_locomo_json(src, dst)\n    out = json.loads(dst.read_text(encoding=\"utf-8\"))\n    turns = out[0][\"conversation\"][\"session_1\"]\n    assert turns[0][\"speaker\"] == \"A\"\n    assert turns[1][\"speaker\"] == \"B\"\n"
  },
  {
    "path": "tests/benchmarks_locomo/test_locomo_provenance.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import cast\n\nfrom benchmarks.locomo._run_impl import _format_top_k\nfrom benchmarks.locomo.provenance import attribute_facts_to_turn_ids\n\n\ndef test_attribute_facts_to_turn_ids_maps_best_match():\n    turn_ids = [\"D1:3\", \"D1:12\"]\n    turn_embeddings = [\n        [1.0, 0.0],  # D1:3\n        [0.0, 1.0],  # D1:12\n    ]\n\n    fact_ids = [101, 102]\n    fact_embeddings = [\n        [0.9, 0.1],  # should map to D1:3\n        [0.1, 0.9],  # should map to D1:12\n    ]\n\n    mapping = attribute_facts_to_turn_ids(\n        turn_ids=turn_ids,\n        turn_embeddings=turn_embeddings,\n        fact_ids=fact_ids,\n        fact_embeddings=fact_embeddings,\n        top_n=1,\n        min_score=0.0,\n    )\n\n    assert mapping[101][0][0] == \"D1:3\"\n    assert mapping[102][0][0] == \"D1:12\"\n\n\ndef test_attribute_facts_to_turn_ids_can_use_lexical_signal(monkeypatch):\n    # Force semantic similarity to prefer the wrong turn, then lexical should fix it.\n    monkeypatch.setattr(\n        \"benchmarks.locomo.provenance.find_similar_embeddings\",\n        lambda _embs, _q, limit=5: [(0, 0.9), (1, 0.8)][:limit],\n    )\n\n    turn_ids = [\"D1:1\", \"D1:2\"]\n    turn_embeddings = [[1.0, 0.0], [0.0, 1.0]]\n    turn_texts = [\"Alice loves skiing\", \"Alice loves painting blue skies\"]\n\n    fact_ids = [201]\n    fact_embeddings = [[1.0, 0.0]]\n    fact_texts = [\"blue painting\"]\n\n    mapping = attribute_facts_to_turn_ids(\n        turn_ids=turn_ids,\n        turn_embeddings=turn_embeddings,\n        turn_texts=turn_texts,\n        fact_ids=fact_ids,\n        fact_embeddings=fact_embeddings,\n        fact_texts=fact_texts,\n        top_n=1,\n        min_score=0.0,\n    )\n    assert mapping[201][0][0] == \"D1:2\"\n\n\ndef test_provenance_store_delete_sample(tmp_path: Path):\n    from benchmarks.locomo.provenance import FactAttribution, ProvenanceStore\n\n    store = ProvenanceStore(tmp_path / \"prov.sqlite\")\n    store.upsert_many(\n        [FactAttribution(fact_id=1, dia_id=\"D1:1\", score=0.5)],\n        run_id=\"r1\",\n        sample_id=\"s1\",\n    )\n    assert store.has_any(run_id=\"r1\", sample_id=\"s1\") is True\n\n    store.delete_sample(run_id=\"r1\", sample_id=\"s1\")\n    assert store.has_any(run_id=\"r1\", sample_id=\"s1\") is False\n\n\ndef test_group_scoring_any_match():\n    from benchmarks.locomo.scoring import hit_at_k_groups, mrr_groups\n\n    relevant = {\"D7:19\"}\n    retrieved = [\n        {\"D7:20\", \"D7:19\"},  # rank 1 contains the relevant ID\n        {\"D1:1\"},\n    ]\n    assert hit_at_k_groups(relevant, retrieved, 1) == 1.0\n    assert mrr_groups(relevant, retrieved) == 1.0\n\n\ndef test_format_top_k_includes_turn_ids_for_aa_mode(tmp_path: Path):\n    # This validates the intended contract: AA-mode facts can map to multiple dia_ids.\n    from benchmarks.locomo.provenance import FactAttribution, ProvenanceStore\n\n    prov = ProvenanceStore(tmp_path / \"prov.sqlite\")\n    prov.upsert_many(\n        [\n            FactAttribution(fact_id=1, dia_id=\"D7:19\", score=1.0),\n            FactAttribution(fact_id=1, dia_id=\"D7:20\", score=0.9),\n        ],\n        run_id=\"r1\",\n        sample_id=\"s1\",\n    )\n\n    retrieved_ids: list[str] = []\n    retrieved_groups: list[set[str]] = []\n    top_k = _format_top_k(\n        results=[{\"id\": 1, \"content\": \"some fact\", \"similarity\": 0.5}],\n        prov_store=prov,\n        run_id=\"r1\",\n        sample_id=\"s1\",\n        retrieved_ids_out=retrieved_ids,\n        retrieved_groups_out=retrieved_groups,\n        provenance_limit=3,\n    )\n    assert top_k[0][\"turn_id\"] in {\"D7:19\", \"D7:20\"}\n    turn_ids = cast(list[str], top_k[0][\"turn_ids\"])\n    assert set(turn_ids) == {\"D7:19\", \"D7:20\"}\n    assert retrieved_groups[0] == {\"D7:19\", \"D7:20\"}\n"
  },
  {
    "path": "tests/benchmarks_locomo/test_locomo_run_and_report.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom pathlib import Path\n\nfrom benchmarks.locomo.report import main as report_main\nfrom benchmarks.locomo.run import main as run_main\n\n\ndef _write_locomo_tiny(path: Path) -> Path:\n    data = [\n        {\n            \"sample_id\": \"sample-001\",\n            \"conversation\": [\n                {\n                    \"session_id\": \"session-1\",\n                    \"dialogue\": [\n                        {\n                            \"turn_id\": \"t0\",\n                            \"speaker\": \"user\",\n                            \"text\": \"My favorite color is blue.\",\n                        },\n                        {\"turn_id\": \"t1\", \"speaker\": \"assistant\", \"text\": \"Got it.\"},\n                    ],\n                }\n            ],\n            \"qa\": [\n                {\n                    \"question_id\": \"q0\",\n                    \"question\": \"What is my favorite color?\",\n                    \"answer\": \"blue\",\n                    \"evidence\": 0,\n                },\n                {\n                    \"question_id\": \"q1\",\n                    \"question\": \"Which color do I like best?\",\n                    \"answer\": \"blue\",\n                    \"evidence\": 0,\n                },\n            ],\n        }\n    ]\n    path.write_text(json.dumps(data), encoding=\"utf-8\")\n    return path\n\n\ndef _fake_embed_texts(\n    texts: str | list[str], model: str, *, async_: bool = False\n) -> list[list[int | float]]:\n    _ = model\n    _ = async_\n    if isinstance(texts, str):\n        items = [texts]\n    else:\n        items = list(texts)\n\n    def v(s: str) -> list[float]:\n        s = s.lower()\n        return [\n            1.0 if \"favorite\" in s else 0.0,\n            1.0 if \"color\" in s else 0.0,\n            1.0 if \"blue\" in s else 0.0,\n        ]\n\n    return [v(x) for x in items]\n\n\ndef _seed_reuse_db(\n    *, sqlite_db: Path, provenance_db: Path, run_id: str, sample_id: str\n) -> None:\n    import sqlite3\n\n    from benchmarks.locomo.provenance import FactAttribution, ProvenanceStore\n    from memori import Memori\n\n    def _conn():\n        return sqlite3.connect(str(sqlite_db), check_same_thread=False)\n\n    mem = Memori(conn=_conn)\n    mem.config.storage.build()\n\n    entity_external_id = f\"locomo:{run_id}:{sample_id}\"\n    entity_db_id = mem.config.storage.driver.entity.create(entity_external_id)\n\n    contents = [\n        \"My favorite color is blue.\",\n        \"Got it.\",\n    ]\n    embeddings = _fake_embed_texts(\n        contents,\n        model=mem.config.embeddings.model,\n    )\n    mem.config.storage.driver.entity_fact.create(entity_db_id, contents, embeddings)\n\n    with sqlite3.connect(str(sqlite_db), check_same_thread=False) as conn2:\n        rows = conn2.execute(\n            \"SELECT id, content FROM memori_entity_fact WHERE entity_id = ? ORDER BY id ASC\",\n            (int(entity_db_id),),\n        ).fetchall()\n    fact_id_by_content = {r[1]: int(r[0]) for r in rows if r and r[0] and r[1]}\n\n    prov = ProvenanceStore(provenance_db)\n    prov.upsert_many(\n        [\n            FactAttribution(\n                fact_id=fact_id_by_content[\"My favorite color is blue.\"],\n                dia_id=\"t0\",\n                score=1.0,\n            ),\n            FactAttribution(\n                fact_id=fact_id_by_content[\"Got it.\"],\n                dia_id=\"t1\",\n                score=1.0,\n            ),\n        ],\n        run_id=run_id,\n        sample_id=sample_id,\n    )\n\n\ndef test_run_writes_predictions_and_summary(tmp_path: Path, monkeypatch):\n    dataset = _write_locomo_tiny(tmp_path / \"locomo_tiny.json\")\n    out_dir = tmp_path / \"run\"\n    sqlite_db = tmp_path / \"seeded.sqlite\"\n    provenance_db = tmp_path / \"seeded_prov.sqlite\"\n    run_id = \"test-run\"\n    _seed_reuse_db(\n        sqlite_db=sqlite_db,\n        provenance_db=provenance_db,\n        run_id=run_id,\n        sample_id=\"sample-001\",\n    )\n\n    # Prevent embedding model downloads during tests.\n    import benchmarks.locomo._run_impl as run_impl_mod\n    import memori.memory.recall as recall_mod\n\n    # run_mod.embed_texts = _fake_embed_texts\n    monkeypatch.setattr(run_impl_mod, \"embed_texts\", _fake_embed_texts)\n    monkeypatch.setattr(recall_mod, \"embed_texts\", _fake_embed_texts)\n\n    rc = run_main(\n        [\n            \"--dataset\",\n            str(dataset),\n            \"--out\",\n            str(out_dir),\n            \"--sqlite-db\",\n            str(sqlite_db),\n            \"--provenance-db\",\n            str(provenance_db),\n            \"--reuse-db\",\n            \"--run-id\",\n            run_id,\n        ]\n    )\n    assert rc == 0\n\n    predictions = out_dir / \"predictions.jsonl\"\n    summary = out_dir / \"summary.json\"\n    assert predictions.exists()\n    assert summary.exists()\n\n    lines = predictions.read_text(encoding=\"utf-8\").splitlines()\n    assert len(lines) == 2\n    row0 = json.loads(lines[0])\n    assert row0[\"sample_id\"] == \"sample-001\"\n    assert row0[\"retrieval\"][\"status\"] == \"ok\"\n    assert row0[\"retrieval\"][\"metrics\"][\"hit@1\"] == 1.0\n    assert row0[\"retrieval\"][\"metrics\"][\"hit@10\"] == 1.0\n    assert row0[\"retrieval\"][\"metrics\"][\"hit@30\"] == 1.0\n    assert row0[\"retrieval\"][\"metrics\"][\"mrr\"] == 1.0\n\n    summary_obj = json.loads(summary.read_text(encoding=\"utf-8\"))\n    assert summary_obj[\"sample_count\"] == 1\n    assert summary_obj[\"question_count\"] == 2\n    assert summary_obj[\"metrics_overall\"][\"hit@1\"] == 1.0\n    assert summary_obj[\"metrics_overall\"][\"hit@10\"] == 1.0\n    assert summary_obj[\"metrics_overall\"][\"hit@30\"] == 1.0\n    assert summary_obj[\"metrics_overall\"][\"mrr\"] == 1.0\n\n\ndef test_run_skips_questions_with_evidence_in_removed_sessions(\n    tmp_path: Path, monkeypatch\n):\n    dataset = tmp_path / \"locomo_two_sessions.json\"\n    data = [\n        {\n            \"sample_id\": \"sample-001\",\n            \"conversation\": [\n                {\n                    \"session_id\": \"s1\",\n                    \"dialogue\": [\n                        {\"turn_id\": \"D1:1\", \"speaker\": \"user\", \"text\": \"A\"},\n                        {\"turn_id\": \"D1:2\", \"speaker\": \"assistant\", \"text\": \"B\"},\n                    ],\n                },\n                {\n                    \"session_id\": \"s2\",\n                    \"dialogue\": [\n                        {\"turn_id\": \"D2:1\", \"speaker\": \"user\", \"text\": \"C\"},\n                        {\"turn_id\": \"D2:2\", \"speaker\": \"assistant\", \"text\": \"D\"},\n                    ],\n                },\n            ],\n            \"qa\": [\n                {\n                    \"question_id\": \"q_in_s1\",\n                    \"question\": \"What was the first user message?\",\n                    \"answer\": \"A\",\n                    \"evidence\": [\"D1:1\"],\n                },\n                {\n                    \"question_id\": \"q_in_s2\",\n                    \"question\": \"What was the second session user message?\",\n                    \"answer\": \"C\",\n                    \"evidence\": [\"D2:1\"],\n                },\n            ],\n        }\n    ]\n    dataset.write_text(json.dumps(data), encoding=\"utf-8\")\n\n    # Prevent embedding model downloads during tests.\n    import benchmarks.locomo._run_impl as run_impl_mod\n    import memori.memory.recall as recall_mod\n\n    monkeypatch.setattr(run_impl_mod, \"embed_texts\", _fake_embed_texts)\n    monkeypatch.setattr(recall_mod, \"embed_texts\", _fake_embed_texts)\n\n    sqlite_db = tmp_path / \"seeded.sqlite\"\n    provenance_db = tmp_path / \"seeded_prov.sqlite\"\n    run_id = \"test-run\"\n    _seed_reuse_db(\n        sqlite_db=sqlite_db,\n        provenance_db=provenance_db,\n        run_id=run_id,\n        sample_id=\"sample-001\",\n    )\n\n    out_dir = tmp_path / \"run\"\n    rc = run_main(\n        [\n            \"--dataset\",\n            str(dataset),\n            \"--out\",\n            str(out_dir),\n            \"--sqlite-db\",\n            str(sqlite_db),\n            \"--provenance-db\",\n            str(provenance_db),\n            \"--reuse-db\",\n            \"--run-id\",\n            run_id,\n            \"--max-sessions\",\n            \"1\",\n        ]\n    )\n    assert rc == 0\n\n    lines = (out_dir / \"predictions.jsonl\").read_text(encoding=\"utf-8\").splitlines()\n    # q_in_s2 should be skipped because its evidence is in session 2 (removed).\n    assert len(lines) == 1\n    row0 = json.loads(lines[0])\n    assert row0[\"question_id\"] == \"q_in_s1\"\n\n\ndef test_report_aggregates_predictions(tmp_path: Path, monkeypatch):\n    dataset = _write_locomo_tiny(tmp_path / \"locomo_tiny.json\")\n    out_dir = tmp_path / \"run\"\n    sqlite_db = tmp_path / \"seeded.sqlite\"\n    provenance_db = tmp_path / \"seeded_prov.sqlite\"\n    run_id = \"test-run\"\n    _seed_reuse_db(\n        sqlite_db=sqlite_db,\n        provenance_db=provenance_db,\n        run_id=run_id,\n        sample_id=\"sample-001\",\n    )\n\n    import benchmarks.locomo._run_impl as run_impl_mod\n    import memori.memory.recall as recall_mod\n\n    # run_mod.embed_texts = _fake_embed_texts\n    monkeypatch.setattr(run_impl_mod, \"embed_texts\", _fake_embed_texts)\n    monkeypatch.setattr(recall_mod, \"embed_texts\", _fake_embed_texts)\n\n    run_main(\n        [\n            \"--dataset\",\n            str(dataset),\n            \"--out\",\n            str(out_dir),\n            \"--sqlite-db\",\n            str(sqlite_db),\n            \"--provenance-db\",\n            str(provenance_db),\n            \"--reuse-db\",\n            \"--run-id\",\n            run_id,\n        ]\n    )\n\n    summary_out = tmp_path / \"summary.json\"\n    rc = report_main(\n        [\n            \"--predictions\",\n            str(out_dir / \"predictions.jsonl\"),\n            \"--out\",\n            str(summary_out),\n        ]\n    )\n    assert rc == 0\n    summary_obj = json.loads(summary_out.read_text(encoding=\"utf-8\"))\n    assert summary_obj[\"question_count\"] == 2\n    assert summary_obj[\"metrics_overall\"][\"hit@1\"] == 1.0\n\n\ndef test_run_can_reuse_existing_sqlite_db_without_ingestion(\n    tmp_path: Path, monkeypatch\n):\n    dataset = _write_locomo_tiny(tmp_path / \"locomo_tiny.json\")\n    sqlite_db = tmp_path / \"shared.sqlite\"\n    provenance_db = tmp_path / \"shared_prov.sqlite\"\n    out_dir_2 = tmp_path / \"run2\"\n\n    import benchmarks.locomo._run_impl as run_impl_mod\n    import memori.memory.recall as recall_mod\n\n    run_id = \"test-run\"\n    _seed_reuse_db(\n        sqlite_db=sqlite_db,\n        provenance_db=provenance_db,\n        run_id=run_id,\n        sample_id=\"sample-001\",\n    )\n\n    # Second run must not ingest; make ingestion embedding calls fail if they happen.\n    def _should_not_be_called(\n        texts: str | list[str], model: str, *, async_: bool = False\n    ) -> list[list[int | float]]:\n        _ = texts\n        _ = model\n        _ = async_\n        raise AssertionError(\n            \"embed_texts should not be called during --reuse-db scoring\"\n        )\n\n    # run_mod.embed_texts = _should_not_be_called\n    monkeypatch.setattr(run_impl_mod, \"embed_texts\", _should_not_be_called)\n    monkeypatch.setattr(recall_mod, \"embed_texts\", _fake_embed_texts)\n\n    rc2 = run_main(\n        [\n            \"--dataset\",\n            str(dataset),\n            \"--out\",\n            str(out_dir_2),\n            \"--sqlite-db\",\n            str(sqlite_db),\n            \"--provenance-db\",\n            str(provenance_db),\n            \"--reuse-db\",\n            \"--run-id\",\n            run_id,\n        ]\n    )\n    assert rc2 == 0\n    summary_obj = json.loads((out_dir_2 / \"summary.json\").read_text(encoding=\"utf-8\"))\n    assert summary_obj[\"metrics_overall\"][\"hit@1\"] == 1.0\n"
  },
  {
    "path": "tests/build/mongodb.py",
    "content": "#!/usr/bin/env python3\n\nfrom memori import Memori\nfrom tests.database.core import MongoTestDBSession\n\nclient = MongoTestDBSession()\ndb = client[\"memori_test\"]\n\n# Drop existing collections\nfor collection_name in [\n    \"memori_conversation_message\",\n    \"memori_conversation\",\n    \"memori_session\",\n    \"memori_entity\",\n    \"memori_process\",\n    \"memori_schema_version\",\n]:\n    if collection_name in db.list_collection_names():\n        db.drop_collection(collection_name)\n\n# Executes all migrations.\nmem = Memori(conn=client)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n# Has no effect, version number is set correctly.\nmem = Memori(conn=client)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n\n# Drop schema version collection\nif \"memori_schema_version\" in db.list_collection_names():\n    db.drop_collection(\"memori_schema_version\")\n\n# Executes all migrations again.\nmem = Memori(conn=client)\nif mem.config.storage is not None:\n    mem.config.storage.build()\n\n# Clear schema version\ndb[\"memori_schema_version\"].delete_many({})\nclient.admin.command(\"ping\")\n"
  },
  {
    "path": "tests/build/mysql.py",
    "content": "#!/usr/bin/env python3\n\nfrom memori import Memori\nfrom tests.database.core import MySQLTestDBSession\n\nsession = MySQLTestDBSession()\n\nfor table_name in [\n    \"memori_conversation_message\",\n    \"memori_conversation\",\n    \"memori_session\",\n    \"memori_entity\",\n    \"memori_process\",\n    \"memori_schema_version\",\n]:\n    session.connection().exec_driver_sql(f\"drop table if exists {table_name}\")\n\n# Executes all migrations.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n# Has no effect, version number is set correctly.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    drop table memori_schema_version\n    \"\"\"\n)\n\n# Executes all migrations again.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    delete from memori_schema_version\n    \"\"\"\n)\nsession.commit()\n"
  },
  {
    "path": "tests/build/oceanbase.py",
    "content": "#!/usr/bin/env python3\n\nfrom memori import Memori\nfrom tests.database.core import OceanBaseTestDBSession\n\nsession = OceanBaseTestDBSession()\n\nfor table_name in [\n    \"memori_conversation_message\",\n    \"memori_conversation\",\n    \"memori_session\",\n    \"memori_entity\",\n    \"memori_process\",\n    \"memori_schema_version\",\n]:\n    session.connection().exec_driver_sql(f\"drop table if exists {table_name}\")\n\n# Executes all migrations.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n# Has no effect, version number is set correctly.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    drop table memori_schema_version\n    \"\"\"\n)\n\n# Executes all migrations again.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    delete from memori_schema_version\n    \"\"\"\n)\nsession.commit()\n"
  },
  {
    "path": "tests/build/oracle.py",
    "content": "#!/usr/bin/env python3\n\nfrom memori import Memori\nfrom tests.database.core import OracleTestDBSession\n\nsession = OracleTestDBSession()\n\n# Drop tables in reverse order of dependencies (Oracle doesn't support DROP TABLE IF EXISTS)\n# Using PL/SQL to handle \"table does not exist\" errors gracefully\nfor table_name in [\n    \"memori_conversation_message\",\n    \"memori_conversation\",\n    \"memori_knowledge_graph\",\n    \"memori_entity_fact\",\n    \"memori_process_attribute\",\n    \"memori_session\",\n    \"memori_subject\",\n    \"memori_predicate\",\n    \"memori_object\",\n    \"memori_entity\",\n    \"memori_process\",\n    \"memori_schema_version\",\n]:\n    session.connection().exec_driver_sql(\n        f\"\"\"\n        BEGIN\n            EXECUTE IMMEDIATE 'DROP TABLE {table_name} CASCADE CONSTRAINTS';\n        EXCEPTION\n            WHEN OTHERS THEN\n                IF SQLCODE != -942 THEN\n                    RAISE;\n                END IF;\n        END;\n        \"\"\"\n    )\n\n# Executes all migrations.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n# Has no effect, version number is set correctly.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    BEGIN\n        EXECUTE IMMEDIATE 'DROP TABLE memori_schema_version CASCADE CONSTRAINTS';\n    EXCEPTION\n        WHEN OTHERS THEN\n            IF SQLCODE != -942 THEN\n                RAISE;\n            END IF;\n    END;\n    \"\"\"\n)\nsession.commit()\n\n# Executes all migrations again.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    DELETE FROM memori_schema_version\n    \"\"\"\n)\nsession.commit()\n"
  },
  {
    "path": "tests/build/postgresql.py",
    "content": "#!/usr/bin/env python3\n\nfrom memori import Memori\nfrom tests.database.core import PostgresTestDBSession\n\nsession = PostgresTestDBSession()\n\nfor table_name in [\n    \"memori_conversation_message\",\n    \"memori_conversation\",\n    \"memori_session\",\n    \"memori_entity\",\n    \"memori_process\",\n    \"memori_schema_version\",\n]:\n    session.connection().exec_driver_sql(f\"DROP TABLE IF EXISTS {table_name} CASCADE\")\n\n# Executes all migrations.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n# Has no effect, version number is set correctly.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    DROP TABLE memori_schema_version CASCADE\n    \"\"\"\n)\nsession.commit()\n\n# Executes all migrations again.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    DELETE FROM memori_schema_version\n    \"\"\"\n)\nsession.commit()\n"
  },
  {
    "path": "tests/build/sqlite.py",
    "content": "#!/usr/bin/env python3\n\nfrom memori import Memori\nfrom tests.database.core import SQLiteTestDBSession\n\nsession = SQLiteTestDBSession()\n\nfor table_name in [\n    \"memori_conversation_message\",\n    \"memori_conversation\",\n    \"memori_session\",\n    \"memori_entity\",\n    \"memori_process\",\n    \"memori_schema_version\",\n]:\n    session.connection().exec_driver_sql(f\"DROP TABLE IF EXISTS {table_name}\")\n\n# Executes all migrations.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n# Has no effect, version number is set correctly.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\nprint(\"-\" * 50)\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    DROP TABLE IF EXISTS memori_schema_version\n    \"\"\"\n)\nsession.commit()\n\n# Executes all migrations again.\nmem = Memori(conn=session)\nif mem.config.storage is not None:\n    mem.config.storage.build()\n\nsession.connection().exec_driver_sql(\n    \"\"\"\n    DELETE FROM memori_schema_version\n    \"\"\"\n)\nsession.commit()\n"
  },
  {
    "path": "tests/database/core.py",
    "content": "import os\n\nfrom pymongo import MongoClient\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.dialects import registry\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import NullPool, StaticPool\n\n# Default test session - uses DATABASE_URL environment variable\n# Falls back to PostgreSQL for backward compatibility\ntest_db_uri = os.environ.get(\n    \"DATABASE_URL\", \"postgresql://memori:memori@postgres:5432/memori_test\"\n)\n\ntest_db_core = create_engine(test_db_uri, pool_pre_ping=True, pool_recycle=300)\n\nTestDBSession = sessionmaker(autocommit=False, autoflush=False, bind=test_db_core)\n\n# PostgreSQL-specific session\npostgres_test_uri = os.environ.get(\n    \"POSTGRES_DATABASE_URL\", \"postgresql://memori:memori@postgres:5432/memori_test\"\n)\n\npostgres_test_db_core = create_engine(\n    postgres_test_uri, pool_pre_ping=True, pool_recycle=300\n)\n\nPostgresTestDBSession = sessionmaker(\n    autocommit=False, autoflush=False, bind=postgres_test_db_core\n)\n\n# MySQL-specific session\nmysql_test_uri = os.environ.get(\n    \"MYSQL_DATABASE_URL\", \"mysql+pymysql://memori:memori@mysql:3306/memori_test\"\n)\n\nmysql_test_db_core = create_engine(mysql_test_uri, pool_pre_ping=True, pool_recycle=300)\n\nMySQLTestDBSession = sessionmaker(\n    autocommit=False, autoflush=False, bind=mysql_test_db_core\n)\n\n# OceanBase-specific session (via pyobvector dialect)\ntry:\n    registry.register(\n        \"mysql.oceanbase\", \"pyobvector.schema.dialect\", \"OceanBaseDialect\"\n    )\nexcept Exception:\n    pass\n\noceanbase_test_uri = os.environ.get(\n    \"OCEANBASE_DATABASE_URL\",\n    \"mysql+oceanbase://root:@127.0.0.1:2882/memori_test?charset=utf8mb4\",\n)\n\noceanbase_test_db_core = create_engine(\n    oceanbase_test_uri, pool_pre_ping=True, pool_recycle=300\n)\n\nOceanBaseTestDBSession = sessionmaker(\n    autocommit=False, autoflush=False, bind=oceanbase_test_db_core\n)\n\n# SQLite-specific session\nsqlite_test_uri = os.environ.get(\"SQLITE_DATABASE_URL\", \"sqlite:///memori_test.db\")\n\nsqlite_test_db_core = create_engine(\n    sqlite_test_uri,\n    connect_args={\"check_same_thread\": False},\n    poolclass=StaticPool if \":memory:\" in sqlite_test_uri else NullPool,\n)\n\n\n@event.listens_for(sqlite_test_db_core, \"connect\")\ndef set_sqlite_pragma(dbapi_conn, connection_record):\n    cursor = dbapi_conn.cursor()\n    cursor.execute(\"PRAGMA foreign_keys=ON\")\n    cursor.execute(\"PRAGMA journal_mode=WAL\")\n    cursor.close()\n\n\nSQLiteTestDBSession = sessionmaker(\n    autocommit=False, autoflush=False, bind=sqlite_test_db_core\n)\n\noracle_test_uri = os.environ.get(\n    \"ORACLE_DATABASE_URL\",\n    \"oracle+oracledb://system:memori@oracle:1521/?service_name=FREEPDB1\",\n)\n\n_oracle_test_db_core = None\n\n\ndef OracleTestDBSession():\n    global _oracle_test_db_core\n    if _oracle_test_db_core is None:\n        _oracle_test_db_core = create_engine(\n            oracle_test_uri, pool_pre_ping=True, pool_recycle=300\n        )\n    return sessionmaker(autocommit=False, autoflush=False, bind=_oracle_test_db_core)()\n\n\n# MongoDB-specific session\nmongodb_test_uri = os.environ.get(\n    \"MONGODB_URL\", \"mongodb://memori:memori@mongodb:27017/memori_test?authSource=admin\"\n)\n\n\ndef MongoTestDBSession():\n    \"\"\"Get a fresh MongoDB client instance.\"\"\"\n    return MongoClient(mongodb_test_uri)\n"
  },
  {
    "path": "tests/database/init_db.py",
    "content": "from memori import Memori\nfrom tests.database.core import TestDBSession\n\n\ndef init_db():\n    try:\n        session = TestDBSession()\n        mem = Memori(conn=session)\n        if mem.config.storage is not None:\n            mem.config.storage.build()\n        print(\"Database schema initialized successfully\")\n        return True\n    except Exception as e:\n        print(f\"Failed to initialize database: {e}\")\n        return False\n\n\nif __name__ == \"__main__\":\n    init_db()\n"
  },
  {
    "path": "tests/embeddings/test_tei_chunking.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport numpy as np\nimport pytest\n\nfrom memori.embeddings._chunking import chunk_text_by_tokens\nfrom memori.embeddings._tei_embed import embed_texts_via_tei\n\n\ndef test_chunk_text_by_tokens_list_input_ids(mocker):\n    tokenizer = mocker.Mock()\n    tokenizer.return_value = {\"input_ids\": [[0, 1, 2, 3]]}\n    tokenizer.decode.side_effect = [\"c1\", \"c2\"]\n\n    out = chunk_text_by_tokens(text=\"abcd\", tokenizer=tokenizer, chunk_size=2)\n\n    assert out == [\"c1\", \"c2\"]\n\n\ndef test_chunk_text_by_tokens_numpy_input_ids(mocker):\n    tokenizer = mocker.Mock()\n    tokenizer.return_value = {\"input_ids\": np.array([[0, 1, 2, 3]], dtype=np.int64)}\n    tokenizer.decode.side_effect = [\"c1\", \"c2\"]\n\n    out = chunk_text_by_tokens(text=\"abcd\", tokenizer=tokenizer, chunk_size=2)\n\n    assert out == [\"c1\", \"c2\"]\n\n\ndef test_embed_texts_via_tei_no_tokenizer_calls_server_once(mocker):\n    tei = mocker.Mock()\n    tei.embed.side_effect = [[[1.0, 2.0]], [[3.0, 4.0]]]\n\n    out = [\n        embed_texts_via_tei(text=t, model=\"m\", tei=tei, tokenizer=None)\n        for t in [\"a\", \"b\"]\n    ]\n\n    assert out == [[1.0, 2.0], [3.0, 4.0]]\n    assert tei.embed.call_count == 2\n    tei.embed.assert_any_call([\"a\"], model=\"m\")\n    tei.embed.assert_any_call([\"b\"], model=\"m\")\n\n\ndef test_embed_texts_via_tei_tokenizer_chunks_and_pools(mocker):\n    tei = mocker.Mock()\n    # Two chunks => mean([1,0],[0,1]) renorm => [0.707..., 0.707...]\n    tei.embed.return_value = [[1.0, 0.0], [0.0, 1.0]]\n\n    tokenizer = mocker.Mock()\n    tokenizer.return_value = {\"input_ids\": [[0, 1, 2, 3]]}\n    tokenizer.decode.side_effect = [\"c1\", \"c2\"]\n\n    out = embed_texts_via_tei(\n        text=\"abcd\",\n        model=\"m\",\n        tei=tei,\n        tokenizer=tokenizer,\n        chunk_size=2,\n    )\n\n    assert out == pytest.approx([0.707106, 0.707106], rel=1e-5)\n    tei.embed.assert_called_once_with([\"c1\", \"c2\"], model=\"m\")\n"
  },
  {
    "path": "tests/integration/__init__.py",
    "content": "\"\"\"Integration tests for Memori LLM providers.\"\"\"\n"
  },
  {
    "path": "tests/integration/cloud/__init__.py",
    "content": "\"\"\"Integration tests for Memori cloud mode.\"\"\"\n"
  },
  {
    "path": "tests/integration/cloud/conftest.py",
    "content": "import os\nimport time\n\nimport pytest\n\nMEMORI_API_KEY = os.environ.get(\"MEMORI_API_KEY\")\n\nrequires_memori_api_key = pytest.mark.skipif(\n    not MEMORI_API_KEY,\n    reason=\"MEMORI_API_KEY environment variable not set (required for cloud tests)\",\n)\n\n\n@pytest.fixture\ndef cloud_test_mode():\n    \"\"\"Set MEMORI_TEST_MODE=1 so cloud API calls hit staging.\n\n    Production cloud-api.memorilabs.ai does not exist yet.\n    Only staging-cloud-api.memorilabs.ai is live.\n    \"\"\"\n    original = os.environ.get(\"MEMORI_TEST_MODE\")\n    os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n    yield\n    if original is None:\n        os.environ.pop(\"MEMORI_TEST_MODE\", None)\n    else:\n        os.environ[\"MEMORI_TEST_MODE\"] = original\n\n\n@pytest.fixture\ndef cloud_memori_instance(sqlite_session_factory, cloud_test_mode):\n    \"\"\"Create a Memori instance in cloud mode with local SQLite for verification.\n\n    Uses conn for local storage (conversation/message verification) but sets\n    config.cloud = True so augmentation and recall hit the staging cloud API.\n    Requires MEMORI_API_KEY and MEMORI_TEST_MODE=1 (set automatically).\n    \"\"\"\n    if not MEMORI_API_KEY:\n        pytest.skip(\"MEMORI_API_KEY not set (required for cloud tests)\")\n\n    from memori import Memori\n\n    mem = Memori(conn=sqlite_session_factory)\n    mem.config.cloud = True\n    mem.config.storage.build()\n\n    yield mem\n\n    mem.close()\n    time.sleep(0.2)\n\n\n@pytest.fixture\ndef cloud_registered_openai_client(cloud_memori_instance, openai_client):\n    cloud_memori_instance.llm.register(openai_client)\n    cloud_memori_instance.attribution(\n        entity_id=\"cloud-test-entity\", process_id=\"cloud-test-process\"\n    )\n    return openai_client\n\n\n@pytest.fixture\ndef cloud_registered_async_openai_client(cloud_memori_instance, async_openai_client):\n    cloud_memori_instance.llm.register(async_openai_client)\n    cloud_memori_instance.attribution(\n        entity_id=\"cloud-test-entity\", process_id=\"cloud-test-process\"\n    )\n    return async_openai_client\n\n\n@pytest.fixture\ndef cloud_registered_anthropic_client(cloud_memori_instance, anthropic_client):\n    cloud_memori_instance.llm.register(anthropic_client)\n    cloud_memori_instance.attribution(\n        entity_id=\"cloud-test-entity\", process_id=\"cloud-test-process\"\n    )\n    return anthropic_client\n\n\n@pytest.fixture\ndef cloud_registered_async_anthropic_client(\n    cloud_memori_instance, async_anthropic_client\n):\n    cloud_memori_instance.llm.register(async_anthropic_client)\n    cloud_memori_instance.attribution(\n        entity_id=\"cloud-test-entity\", process_id=\"cloud-test-process\"\n    )\n    return async_anthropic_client\n\n\n@pytest.fixture\ndef cloud_registered_google_client(cloud_memori_instance, google_client):\n    cloud_memori_instance.llm.register(google_client)\n    cloud_memori_instance.attribution(\n        entity_id=\"cloud-test-entity\", process_id=\"cloud-test-process\"\n    )\n    return google_client\n\n\n@pytest.fixture\ndef cloud_registered_xai_client(cloud_memori_instance, xai_client):\n    cloud_memori_instance.llm.register(xai_client)\n    cloud_memori_instance.attribution(\n        entity_id=\"cloud-test-entity\", process_id=\"cloud-test-process\"\n    )\n    return xai_client\n\n\n@pytest.fixture\ndef cloud_registered_async_xai_client(cloud_memori_instance, async_xai_client):\n    cloud_memori_instance.llm.register(async_xai_client)\n    cloud_memori_instance.attribution(\n        entity_id=\"cloud-test-entity\", process_id=\"cloud-test-process\"\n    )\n    return async_xai_client\n\n\n@pytest.fixture\ndef cloud_registered_bedrock_client(cloud_memori_instance, bedrock_client):\n    cloud_memori_instance.llm.register(chatbedrock=bedrock_client)\n    cloud_memori_instance.attribution(\n        entity_id=\"cloud-test-entity\", process_id=\"cloud-test-process\"\n    )\n    return bedrock_client\n"
  },
  {
    "path": "tests/integration/cloud/test_cloud_anthropic.py",
    "content": "import asyncio\n\nimport pytest\nfrom anthropic import Anthropic, AsyncAnthropic\n\nfrom tests.integration.conftest import requires_anthropic\n\nMODEL = \"claude-3-haiku-20240307\"\nMAX_TOKENS = 50\nTEST_PROMPT = \"Say 'hello' in one word.\"\nAA_WAIT_TIMEOUT = 15.0\n\n\nclass TestCloudAnthropicSync:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_message_through_cloud_pipeline(\n        self, cloud_memori_instance, anthropic_api_key\n    ):\n        client = Anthropic(api_key=anthropic_api_key)\n        cloud_memori_instance.llm.register(client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-test-user\", process_id=\"cloud-test\"\n        )\n\n        response = client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert len(response.content) > 0\n        assert response.content[0].text is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_message_stores_conversation(\n        self, cloud_registered_anthropic_client, cloud_memori_instance\n    ):\n        cloud_registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_message_stores_messages(\n        self, cloud_registered_anthropic_client, cloud_memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        cloud_registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_query}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        messages = (\n            cloud_memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n        assert len(assistant_messages[0][\"content\"]) > 0\n\n\nclass TestCloudAnthropicAsync:\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_message_through_cloud_pipeline(\n        self, cloud_memori_instance, anthropic_api_key\n    ):\n        client = AsyncAnthropic(api_key=anthropic_api_key)\n        cloud_memori_instance.llm.register(client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-async-user\", process_id=\"cloud-async-test\"\n        )\n\n        response = await client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert len(response.content) > 0\n        assert response.content[0].text is not None\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_message_stores_conversation(\n        self, cloud_registered_async_anthropic_client, cloud_memori_instance\n    ):\n        await cloud_registered_async_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n\nclass TestCloudAnthropicStreaming:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_streaming_through_cloud_pipeline(\n        self, cloud_registered_anthropic_client, cloud_memori_instance\n    ):\n        with cloud_registered_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            full_content = \"\".join(stream.text_stream)\n\n        assert len(full_content) > 0\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_through_cloud_pipeline(\n        self, cloud_registered_async_anthropic_client, cloud_memori_instance\n    ):\n        async with cloud_registered_async_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            content_parts = []\n            async for text in stream.text_stream:\n                content_parts.append(text)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestCloudAnthropicAugmentation:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_augmentation_completes_without_error(\n        self, cloud_registered_anthropic_client, cloud_memori_instance\n    ):\n        cloud_registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_multi_turn_triggers_augmentation(\n        self, cloud_registered_anthropic_client, cloud_memori_instance\n    ):\n        cloud_registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"user\", \"content\": \"My name is Alice.\"},\n                {\"role\": \"assistant\", \"content\": \"Nice to meet you, Alice!\"},\n                {\"role\": \"user\", \"content\": \"What is my name?\"},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestCloudAnthropicSessionManagement:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_multiple_calls_same_session(\n        self, cloud_registered_anthropic_client, cloud_memori_instance\n    ):\n        for i in range(3):\n            response = cloud_registered_anthropic_client.messages.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": f\"Say the number {i}\"}],\n                max_tokens=MAX_TOKENS,\n            )\n            assert response is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_new_session_resets_context(\n        self, cloud_registered_anthropic_client, cloud_memori_instance\n    ):\n        cloud_registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        first_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert first_conversation_id is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        cloud_memori_instance.new_session()\n\n        cloud_registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        second_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert second_conversation_id is not None\n        assert first_conversation_id != second_conversation_id\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n"
  },
  {
    "path": "tests/integration/cloud/test_cloud_bedrock.py",
    "content": "import asyncio\n\nimport pytest\n\nfrom tests.integration.conftest import BEDROCK_SDK_AVAILABLE, requires_bedrock\n\npytestmark = pytest.mark.skipif(\n    not BEDROCK_SDK_AVAILABLE,\n    reason=\"langchain-aws package not installed (pip install langchain-aws)\",\n)\n\nMODEL_ID = \"anthropic.claude-3-haiku-20240307-v1:0\"\nTEST_PROMPT = \"Say 'hello' in one word.\"\nAA_WAIT_TIMEOUT = 15.0\n\n\nclass TestCloudBedrockSync:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_invocation_through_cloud_pipeline(\n        self, cloud_memori_instance, aws_credentials\n    ):\n        from langchain_aws import ChatBedrock\n\n        client = ChatBedrock(\n            model=MODEL_ID,\n            region_name=aws_credentials[\"region_name\"],\n        )\n        cloud_memori_instance.llm.register(chatbedrock=client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-test-user\", process_id=\"cloud-test\"\n        )\n\n        response = client.invoke(TEST_PROMPT)\n\n        assert response is not None\n        assert hasattr(response, \"content\")\n        assert len(response.content) > 0\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_invocation_stores_conversation(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        cloud_registered_bedrock_client.invoke(TEST_PROMPT)\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_invocation_stores_messages(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        cloud_registered_bedrock_client.invoke(test_query)\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        messages = (\n            cloud_memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n\n        assert len(messages) >= 1\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n\n\nclass TestCloudBedrockAsync:\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invocation_through_cloud_pipeline(\n        self, cloud_memori_instance, aws_credentials\n    ):\n        from langchain_aws import ChatBedrock\n\n        client = ChatBedrock(\n            model=MODEL_ID,\n            region_name=aws_credentials[\"region_name\"],\n        )\n        cloud_memori_instance.llm.register(chatbedrock=client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-async-user\", process_id=\"cloud-async-test\"\n        )\n\n        response = await client.ainvoke(TEST_PROMPT)\n\n        assert response is not None\n        assert hasattr(response, \"content\")\n        assert len(response.content) > 0\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invocation_stores_conversation(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        await cloud_registered_bedrock_client.ainvoke(TEST_PROMPT)\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n\nclass TestCloudBedrockStreaming:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_streaming_through_cloud_pipeline(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        content_parts = []\n        for chunk in cloud_registered_bedrock_client.stream(TEST_PROMPT):\n            if hasattr(chunk, \"content\") and chunk.content:\n                content_parts.append(chunk.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_through_cloud_pipeline(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        content_parts = []\n        async for chunk in cloud_registered_bedrock_client.astream(TEST_PROMPT):\n            if hasattr(chunk, \"content\") and chunk.content:\n                content_parts.append(chunk.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestCloudBedrockAugmentation:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_augmentation_completes_without_error(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        cloud_registered_bedrock_client.invoke(TEST_PROMPT)\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_multi_turn_triggers_augmentation(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        from langchain_core.messages import AIMessage, HumanMessage\n\n        cloud_registered_bedrock_client.invoke(\n            [\n                HumanMessage(content=\"My name is Alice.\"),\n                AIMessage(content=\"Nice to meet you, Alice!\"),\n                HumanMessage(content=\"What is my name?\"),\n            ]\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestCloudBedrockSessionManagement:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_multiple_calls_same_session(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        for i in range(3):\n            response = cloud_registered_bedrock_client.invoke(f\"Say the number {i}\")\n            assert response is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_new_session_resets_context(\n        self, cloud_registered_bedrock_client, cloud_memori_instance\n    ):\n        cloud_registered_bedrock_client.invoke(TEST_PROMPT)\n\n        first_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert first_conversation_id is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        cloud_memori_instance.new_session()\n\n        cloud_registered_bedrock_client.invoke(TEST_PROMPT)\n\n        second_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert second_conversation_id is not None\n        assert first_conversation_id != second_conversation_id\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n"
  },
  {
    "path": "tests/integration/cloud/test_cloud_gemini.py",
    "content": "import asyncio\n\nimport pytest\n\nfrom tests.integration.conftest import GOOGLE_SDK_AVAILABLE, requires_google\n\npytestmark = pytest.mark.skipif(\n    not GOOGLE_SDK_AVAILABLE,\n    reason=\"google-genai package not installed (pip install google-genai)\",\n)\n\nMODEL = \"gemini-2.0-flash\"\nTEST_PROMPT = \"Say 'hello' in one word.\"\nAA_WAIT_TIMEOUT = 15.0\n\n\nclass TestCloudGeminiSync:\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_generation_through_cloud_pipeline(\n        self, cloud_memori_instance, google_api_key\n    ):\n        from google import genai\n\n        client = genai.Client(api_key=google_api_key)\n        cloud_memori_instance.llm.register(client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-test-user\", process_id=\"cloud-test\"\n        )\n\n        response = client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"text\")\n        assert response.text is not None\n        assert len(response.text) > 0\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        client.close()\n\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_generation_stores_conversation(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        cloud_registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_generation_stores_messages(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        cloud_registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=test_query,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        messages = (\n            cloud_memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n\n        assert len(messages) >= 1\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n\nclass TestCloudGeminiAsync:\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_generation_through_cloud_pipeline(\n        self, cloud_memori_instance, google_api_key\n    ):\n        from google import genai\n\n        client = genai.Client(api_key=google_api_key)\n        cloud_memori_instance.llm.register(client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-async-user\", process_id=\"cloud-async-test\"\n        )\n\n        response = await client.aio.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"text\")\n        assert response.text is not None\n        assert len(response.text) > 0\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        client.close()\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_generation_stores_conversation(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        await cloud_registered_google_client.aio.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n\nclass TestCloudGeminiStreaming:\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_streaming_through_cloud_pipeline(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        stream = cloud_registered_google_client.models.generate_content_stream(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        content_parts = []\n        for chunk in stream:\n            if hasattr(chunk, \"text\") and chunk.text:\n                content_parts.append(chunk.text)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_through_cloud_pipeline(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        stream = (\n            await cloud_registered_google_client.aio.models.generate_content_stream(\n                model=MODEL,\n                contents=TEST_PROMPT,\n            )\n        )\n\n        content_parts = []\n        async for chunk in stream:\n            if hasattr(chunk, \"text\") and chunk.text:\n                content_parts.append(chunk.text)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestCloudGeminiAugmentation:\n    @requires_google\n    @pytest.mark.integration\n    def test_augmentation_completes_without_error(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        cloud_registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_google\n    @pytest.mark.integration\n    def test_multi_turn_triggers_augmentation(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        from google.genai.types import Content, Part\n\n        cloud_registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=[\n                Content(role=\"user\", parts=[Part(text=\"My name is Alice.\")]),\n                Content(role=\"model\", parts=[Part(text=\"Nice to meet you, Alice!\")]),\n                Content(role=\"user\", parts=[Part(text=\"What is my name?\")]),\n            ],\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestCloudGeminiSessionManagement:\n    @requires_google\n    @pytest.mark.integration\n    def test_multiple_calls_same_session(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        for i in range(3):\n            response = cloud_registered_google_client.models.generate_content(\n                model=MODEL,\n                contents=f\"Say the number {i}\",\n            )\n            assert response is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_google\n    @pytest.mark.integration\n    def test_new_session_resets_context(\n        self, cloud_registered_google_client, cloud_memori_instance\n    ):\n        cloud_registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        first_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert first_conversation_id is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        cloud_memori_instance.new_session()\n\n        cloud_registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        second_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert second_conversation_id is not None\n        assert first_conversation_id != second_conversation_id\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n"
  },
  {
    "path": "tests/integration/cloud/test_cloud_openai.py",
    "content": "import asyncio\n\nimport pytest\nfrom openai import AsyncOpenAI, OpenAI\n\nfrom tests.integration.conftest import requires_openai\n\nMODEL = \"gpt-4o-mini\"\nMAX_TOKENS = 50\nMAX_OUTPUT_TOKENS = 50\nTEST_PROMPT = \"Say 'hello' in one word.\"\nAA_WAIT_TIMEOUT = 15.0\n\n\nclass TestCloudOpenAISync:\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_completion_through_cloud_pipeline(\n        self, cloud_memori_instance, openai_api_key\n    ):\n        client = OpenAI(api_key=openai_api_key)\n        cloud_memori_instance.llm.register(client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-test-user\", process_id=\"cloud-test\"\n        )\n\n        response = client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_completion_stores_conversation(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        cloud_registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_completion_stores_messages(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        cloud_registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_query}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        messages = (\n            cloud_memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n        assert len(assistant_messages[0][\"content\"]) > 0\n\n\nclass TestCloudOpenAIAsync:\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_completion_through_cloud_pipeline(\n        self, cloud_memori_instance, openai_api_key\n    ):\n        client = AsyncOpenAI(api_key=openai_api_key)\n        cloud_memori_instance.llm.register(client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-async-user\", process_id=\"cloud-async-test\"\n        )\n\n        response = await client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_completion_stores_conversation(\n        self, cloud_registered_async_openai_client, cloud_memori_instance\n    ):\n        await cloud_registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n\nclass TestCloudOpenAIStreaming:\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_through_cloud_pipeline(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        stream = cloud_registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        content_parts = []\n        for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                content_parts.append(chunk.choices[0].delta.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_through_cloud_pipeline(\n        self, cloud_registered_async_openai_client, cloud_memori_instance\n    ):\n        stream = await cloud_registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        content_parts = []\n        async for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                content_parts.append(chunk.choices[0].delta.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestCloudOpenAIAugmentation:\n    @requires_openai\n    @pytest.mark.integration\n    def test_augmentation_completes_without_error(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        cloud_registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_multi_turn_triggers_augmentation(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        cloud_registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"system\", \"content\": \"You are helpful.\"},\n                {\"role\": \"user\", \"content\": \"My name is Alice.\"},\n                {\"role\": \"assistant\", \"content\": \"Nice to meet you, Alice!\"},\n                {\"role\": \"user\", \"content\": \"What is my name?\"},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_cloud_memori_instance_is_configured(self, cloud_memori_instance):\n        assert cloud_memori_instance.config is not None\n        assert cloud_memori_instance.config.augmentation is not None\n        assert cloud_memori_instance.config.storage is not None\n\n\nclass TestCloudOpenAIResponses:\n    @requires_openai\n    @pytest.mark.integration\n    def test_responses_api_through_cloud_pipeline(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        response = cloud_registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"output_text\")\n        assert len(response.output_text) > 0\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_responses_streaming_through_cloud_pipeline(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        stream = cloud_registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        )\n\n        events = list(stream)\n        assert len(events) > 0\n\n        event_types = [getattr(e, \"type\", None) for e in events]\n        assert \"response.completed\" in event_types\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestCloudOpenAISessionManagement:\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_calls_same_session(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        for i in range(3):\n            response = cloud_registered_openai_client.chat.completions.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": f\"Say the number {i}\"}],\n                max_tokens=MAX_TOKENS,\n            )\n            assert response is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_new_session_resets_context(\n        self, cloud_registered_openai_client, cloud_memori_instance\n    ):\n        cloud_registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        first_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert first_conversation_id is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        cloud_memori_instance.new_session()\n\n        cloud_registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        second_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert second_conversation_id is not None\n        assert first_conversation_id != second_conversation_id\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n"
  },
  {
    "path": "tests/integration/cloud/test_cloud_xai.py",
    "content": "import asyncio\n\nimport pytest\nfrom openai import AsyncOpenAI, OpenAI\n\nfrom tests.integration.conftest import requires_xai\n\nMODEL = \"grok-beta\"\nMAX_TOKENS = 50\nTEST_PROMPT = \"Say 'hello' in one word.\"\nXAI_BASE_URL = \"https://api.x.ai/v1\"\nAA_WAIT_TIMEOUT = 15.0\n\n\nclass TestcloudXAISync:\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_completion_through_cloud_pipeline(\n        self, cloud_memori_instance, xai_api_key\n    ):\n        client = OpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n        cloud_memori_instance.llm.register(client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-test-user\", process_id=\"cloud-test\"\n        )\n\n        response = client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_completion_stores_conversation(\n        self, cloud_registered_xai_client, cloud_memori_instance\n    ):\n        cloud_registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_completion_stores_messages(\n        self, cloud_registered_xai_client, cloud_memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        cloud_registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_query}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        messages = (\n            cloud_memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n        assert len(assistant_messages[0][\"content\"]) > 0\n\n\nclass TestcloudXAIAsync:\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_completion_through_cloud_pipeline(\n        self, cloud_memori_instance, xai_api_key\n    ):\n        client = AsyncOpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n        cloud_memori_instance.llm.register(client)\n        cloud_memori_instance.attribution(\n            entity_id=\"cloud-async-user\", process_id=\"cloud-async-test\"\n        )\n\n        response = await client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_completion_stores_conversation(\n        self, cloud_registered_async_xai_client, cloud_memori_instance\n    ):\n        await cloud_registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = cloud_memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n\nclass TestcloudXAIStreaming:\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_streaming_through_cloud_pipeline(\n        self, cloud_registered_xai_client, cloud_memori_instance\n    ):\n        stream = cloud_registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        content_parts = []\n        for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                content_parts.append(chunk.choices[0].delta.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_through_cloud_pipeline(\n        self, cloud_registered_async_xai_client, cloud_memori_instance\n    ):\n        stream = await cloud_registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        content_parts = []\n        async for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                content_parts.append(chunk.choices[0].delta.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n        await asyncio.sleep(0.5)\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestcloudXAIAugmentation:\n    @requires_xai\n    @pytest.mark.integration\n    def test_augmentation_completes_without_error(\n        self, cloud_registered_xai_client, cloud_memori_instance\n    ):\n        cloud_registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_multi_turn_triggers_augmentation(\n        self, cloud_registered_xai_client, cloud_memori_instance\n    ):\n        cloud_registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"system\", \"content\": \"You are helpful.\"},\n                {\"role\": \"user\", \"content\": \"My name is Alice.\"},\n                {\"role\": \"assistant\", \"content\": \"Nice to meet you, Alice!\"},\n                {\"role\": \"user\", \"content\": \"What is my name?\"},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n\nclass TestcloudXAISessionManagement:\n    @requires_xai\n    @pytest.mark.integration\n    def test_multiple_calls_same_session(\n        self, cloud_registered_xai_client, cloud_memori_instance\n    ):\n        for i in range(3):\n            response = cloud_registered_xai_client.chat.completions.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": f\"Say the number {i}\"}],\n                max_tokens=MAX_TOKENS,\n            )\n            assert response is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_new_session_resets_context(\n        self, cloud_registered_xai_client, cloud_memori_instance\n    ):\n        cloud_registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        first_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert first_conversation_id is not None\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n\n        cloud_memori_instance.new_session()\n\n        cloud_registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        second_conversation_id = cloud_memori_instance.config.cache.conversation_id\n        assert second_conversation_id is not None\n        assert first_conversation_id != second_conversation_id\n\n        cloud_memori_instance.config.augmentation.wait(timeout=AA_WAIT_TIMEOUT)\n"
  },
  {
    "path": "tests/integration/conftest.py",
    "content": "import os\nimport time\nfrom dataclasses import dataclass, field\nfrom unittest.mock import patch\n\nimport pytest\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import NullPool\n\nOPENAI_API_KEY = os.environ.get(\"OPENAI_API_KEY\")\nANTHROPIC_API_KEY = os.environ.get(\"ANTHROPIC_API_KEY\")\nGOOGLE_API_KEY = os.environ.get(\"GOOGLE_API_KEY\")\nXAI_API_KEY = os.environ.get(\"XAI_API_KEY\")\n\nrequires_openai = pytest.mark.skipif(\n    not OPENAI_API_KEY,\n    reason=\"OPENAI_API_KEY environment variable not set\",\n)\n\nrequires_anthropic = pytest.mark.skipif(\n    not ANTHROPIC_API_KEY,\n    reason=\"ANTHROPIC_API_KEY environment variable not set\",\n)\n\ntry:\n    import importlib.util\n\n    GOOGLE_SDK_AVAILABLE = importlib.util.find_spec(\"google.genai\") is not None\nexcept ImportError:\n    GOOGLE_SDK_AVAILABLE = False\n\nrequires_google = pytest.mark.skipif(\n    not GOOGLE_API_KEY or not GOOGLE_SDK_AVAILABLE,\n    reason=\"GOOGLE_API_KEY not set or google-genai not installed\",\n)\n\nrequires_xai = pytest.mark.skipif(\n    not XAI_API_KEY,\n    reason=\"XAI_API_KEY environment variable not set\",\n)\n\nAWS_ACCESS_KEY_ID = os.environ.get(\"AWS_ACCESS_KEY_ID\")\nAWS_SECRET_ACCESS_KEY = os.environ.get(\"AWS_SECRET_ACCESS_KEY\")\nAWS_REGION = os.environ.get(\"AWS_REGION\", \"us-east-1\")\n\ntry:\n    BEDROCK_SDK_AVAILABLE = importlib.util.find_spec(\"langchain_aws\") is not None\nexcept ImportError:\n    BEDROCK_SDK_AVAILABLE = False\n\nrequires_bedrock = pytest.mark.skipif(\n    not (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) or not BEDROCK_SDK_AVAILABLE,\n    reason=\"AWS credentials not set or langchain-aws not installed\",\n)\n\n\n@pytest.fixture(scope=\"session\")\ndef openai_api_key():\n    if not OPENAI_API_KEY:\n        pytest.skip(\"OPENAI_API_KEY not set\")\n    return OPENAI_API_KEY\n\n\n@pytest.fixture\ndef sqlite_session_factory(tmp_path):\n    db_path = tmp_path / \"test_memori.db\"\n    engine = create_engine(\n        f\"sqlite:///{db_path}\",\n        connect_args={\"check_same_thread\": False},\n        poolclass=NullPool,\n    )\n\n    @event.listens_for(engine, \"connect\")\n    def set_sqlite_pragma(dbapi_conn, connection_record):\n        cursor = dbapi_conn.cursor()\n        cursor.execute(\"PRAGMA foreign_keys=ON\")\n        cursor.execute(\"PRAGMA journal_mode=WAL\")\n        cursor.close()\n\n    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n    yield Session\n\n    time.sleep(0.2)\n    engine.dispose()\n\n\n@pytest.fixture\ndef memori_test_mode():\n    original = os.environ.get(\"MEMORI_TEST_MODE\")\n    os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n    yield\n    if original is None:\n        os.environ.pop(\"MEMORI_TEST_MODE\", None)\n    else:\n        os.environ[\"MEMORI_TEST_MODE\"] = original\n\n\n@pytest.fixture\ndef openai_client(openai_api_key):\n    from openai import OpenAI\n\n    return OpenAI(api_key=openai_api_key)\n\n\n@pytest.fixture\ndef async_openai_client(openai_api_key):\n    from openai import AsyncOpenAI\n\n    return AsyncOpenAI(api_key=openai_api_key)\n\n\n@pytest.fixture\ndef memori_instance(sqlite_session_factory, memori_test_mode):\n    from memori import Memori\n\n    mem = Memori(conn=sqlite_session_factory)\n    mem.config.storage.build()\n\n    yield mem\n\n    time.sleep(0.1)\n\n\n@pytest.fixture\ndef registered_openai_client(memori_instance, openai_client):\n    memori_instance.llm.register(openai_client)\n    memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n    return openai_client\n\n\n@pytest.fixture\ndef registered_async_openai_client(memori_instance, async_openai_client):\n    memori_instance.llm.register(async_openai_client)\n    memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n    return async_openai_client\n\n\n@pytest.fixture(scope=\"session\")\ndef anthropic_api_key():\n    if not ANTHROPIC_API_KEY:\n        pytest.skip(\"ANTHROPIC_API_KEY not set\")\n    return ANTHROPIC_API_KEY\n\n\n@pytest.fixture\ndef anthropic_client(anthropic_api_key):\n    from anthropic import Anthropic\n\n    return Anthropic(api_key=anthropic_api_key)\n\n\n@pytest.fixture\ndef async_anthropic_client(anthropic_api_key):\n    from anthropic import AsyncAnthropic\n\n    return AsyncAnthropic(api_key=anthropic_api_key)\n\n\n@pytest.fixture\ndef registered_anthropic_client(memori_instance, anthropic_client):\n    memori_instance.llm.register(anthropic_client)\n    memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n    return anthropic_client\n\n\n@pytest.fixture\ndef registered_async_anthropic_client(memori_instance, async_anthropic_client):\n    memori_instance.llm.register(async_anthropic_client)\n    memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n    return async_anthropic_client\n\n\n@pytest.fixture(scope=\"session\")\ndef google_api_key():\n    if not GOOGLE_API_KEY:\n        pytest.skip(\"GOOGLE_API_KEY not set\")\n    return GOOGLE_API_KEY\n\n\n@pytest.fixture\ndef google_client(google_api_key):\n    if not GOOGLE_SDK_AVAILABLE:\n        pytest.skip(\"google-genai not installed (pip install google-genai)\")\n\n    from google import genai\n\n    client = genai.Client(api_key=google_api_key)\n    yield client\n    client.close()\n\n\n@pytest.fixture\ndef registered_google_client(memori_instance, google_client):\n    memori_instance.llm.register(google_client)\n    memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n    return google_client\n\n\n@pytest.fixture(scope=\"session\")\ndef xai_api_key():\n    if not XAI_API_KEY:\n        pytest.skip(\"XAI_API_KEY not set\")\n    return XAI_API_KEY\n\n\n@pytest.fixture\ndef xai_client(xai_api_key):\n    from openai import OpenAI\n\n    return OpenAI(\n        api_key=xai_api_key,\n        base_url=\"https://api.x.ai/v1\",\n    )\n\n\n@pytest.fixture\ndef async_xai_client(xai_api_key):\n    from openai import AsyncOpenAI\n\n    return AsyncOpenAI(\n        api_key=xai_api_key,\n        base_url=\"https://api.x.ai/v1\",\n    )\n\n\n@pytest.fixture\ndef registered_xai_client(memori_instance, xai_client):\n    memori_instance.llm.register(xai_client)\n    memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n    return xai_client\n\n\n@pytest.fixture\ndef registered_async_xai_client(memori_instance, async_xai_client):\n    memori_instance.llm.register(async_xai_client)\n    memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n    return async_xai_client\n\n\n@pytest.fixture(scope=\"session\")\ndef aws_credentials():\n    if not AWS_ACCESS_KEY_ID or not AWS_SECRET_ACCESS_KEY:\n        pytest.skip(\"AWS credentials not set\")\n    return {\n        \"aws_access_key_id\": AWS_ACCESS_KEY_ID,\n        \"aws_secret_access_key\": AWS_SECRET_ACCESS_KEY,\n        \"region_name\": AWS_REGION,\n    }\n\n\n@pytest.fixture\ndef bedrock_client(aws_credentials):\n    if not BEDROCK_SDK_AVAILABLE:\n        pytest.skip(\"langchain-aws not installed (pip install langchain-aws)\")\n\n    from langchain_aws import ChatBedrock\n\n    return ChatBedrock(\n        model=\"anthropic.claude-3-haiku-20240307-v1:0\",\n        region_name=aws_credentials[\"region_name\"],\n    )\n\n\n@pytest.fixture\ndef registered_bedrock_client(memori_instance, bedrock_client):\n    memori_instance.llm.register(chatbedrock=bedrock_client)\n    memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n    return bedrock_client\n\n\n@dataclass\nclass CapturedPayload:\n    payloads: list = field(default_factory=list)\n\n    def capture(self, payload: dict) -> dict:\n        self.payloads.append(payload)\n        return {\n            \"entity\": {\"facts\": [], \"triples\": []},\n            \"process\": {\"attributes\": []},\n            \"conversation\": {\"summary\": None},\n        }\n\n    @property\n    def last(self) -> dict | None:\n        return self.payloads[-1] if self.payloads else None\n\n    @property\n    def count(self) -> int:\n        return len(self.payloads)\n\n    def validate_structure(self, payload: dict | None = None) -> list[str]:\n        errors = []\n        payload = payload or self.last\n\n        if not payload:\n            return [\"No payload to validate\"]\n\n        if \"conversation\" not in payload:\n            errors.append(\"Missing 'conversation' key\")\n        if \"meta\" not in payload:\n            errors.append(\"Missing 'meta' key\")\n\n        if \"conversation\" in payload:\n            conv = payload[\"conversation\"]\n            if \"messages\" not in conv:\n                errors.append(\"Missing 'conversation.messages'\")\n            elif not isinstance(conv[\"messages\"], list):\n                errors.append(\"'conversation.messages' must be a list\")\n\n        if \"meta\" in payload:\n            meta = payload[\"meta\"]\n\n            required_meta = [\n                \"attribution\",\n                \"framework\",\n                \"llm\",\n                \"platform\",\n                \"sdk\",\n                \"storage\",\n            ]\n            for key in required_meta:\n                if key not in meta:\n                    errors.append(f\"Missing 'meta.{key}'\")\n\n            if \"attribution\" in meta:\n                attr = meta[\"attribution\"]\n                if \"entity\" not in attr or \"id\" not in attr.get(\"entity\", {}):\n                    errors.append(\"Missing 'meta.attribution.entity.id'\")\n                if \"process\" not in attr or \"id\" not in attr.get(\"process\", {}):\n                    errors.append(\"Missing 'meta.attribution.process.id'\")\n\n                entity_id = attr.get(\"entity\", {}).get(\"id\")\n                if entity_id is not None and len(entity_id) != 64:\n                    errors.append(\n                        f\"Entity ID not hashed: {len(entity_id)} chars, expected 64\"\n                    )\n\n                process_id = attr.get(\"process\", {}).get(\"id\")\n                if process_id is not None and len(process_id) != 64:\n                    errors.append(\n                        f\"Process ID not hashed: {len(process_id)} chars, expected 64\"\n                    )\n\n            if \"llm\" in meta:\n                llm = meta[\"llm\"]\n                if \"model\" not in llm:\n                    errors.append(\"Missing 'meta.llm.model'\")\n                elif \"provider\" not in llm.get(\"model\", {}):\n                    errors.append(\"Missing 'meta.llm.model.provider'\")\n\n            if \"sdk\" in meta:\n                sdk = meta[\"sdk\"]\n                if sdk.get(\"lang\") != \"python\":\n                    lang = sdk.get(\"lang\")\n                    errors.append(f\"Expected sdk.lang='python', got '{lang}'\")\n\n            if \"storage\" in meta:\n                storage = meta[\"storage\"]\n                if \"dialect\" not in storage:\n                    errors.append(\"Missing 'meta.storage.dialect'\")\n                if \"cockroachdb\" not in storage:\n                    errors.append(\"Missing 'meta.storage.cockroachdb'\")\n\n        return errors\n\n    def is_valid(self, payload: dict | None = None) -> bool:\n        return len(self.validate_structure(payload)) == 0\n\n\n@pytest.fixture\ndef aa_payload_capture():\n    captured = CapturedPayload()\n\n    async def mock_augmentation(payload: dict) -> dict:\n        return captured.capture(payload)\n\n    with patch(\"memori._network.Api.augmentation_async\", new=mock_augmentation):\n        yield captured\n\n\n@pytest.fixture\ndef memori_instance_with_capture(\n    sqlite_session_factory, memori_test_mode, aa_payload_capture\n):\n    from memori import Memori\n\n    mem = Memori(conn=sqlite_session_factory)\n    mem.config.storage.build()\n\n    yield mem, aa_payload_capture\n\n    time.sleep(0.1)\n"
  },
  {
    "path": "tests/integration/databases/__init__.py",
    "content": "# Database integration tests package\n"
  },
  {
    "path": "tests/integration/databases/conftest.py",
    "content": "import os\nimport time\n\nimport pytest\nfrom pymongo import MongoClient\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import NullPool\n\nSQLITE_DATABASE_URL = os.environ.get(\"SQLITE_DATABASE_URL\")\nPOSTGRES_DATABASE_URL = os.environ.get(\"POSTGRES_DATABASE_URL\")\nMYSQL_DATABASE_URL = os.environ.get(\"MYSQL_DATABASE_URL\")\nMONGODB_URL = os.environ.get(\"MONGODB_URL\")\nOPENAI_API_KEY = os.environ.get(\"OPENAI_API_KEY\")\n\nrequires_sqlite = pytest.mark.skipif(\n    not SQLITE_DATABASE_URL,\n    reason=\"SQLITE_DATABASE_URL environment variable not set\",\n)\n\nrequires_postgres = pytest.mark.skipif(\n    not POSTGRES_DATABASE_URL,\n    reason=\"POSTGRES_DATABASE_URL environment variable not set\",\n)\n\nrequires_mysql = pytest.mark.skipif(\n    not MYSQL_DATABASE_URL,\n    reason=\"MYSQL_DATABASE_URL environment variable not set\",\n)\n\nrequires_mongodb = pytest.mark.skipif(\n    not MONGODB_URL,\n    reason=\"MONGODB_URL environment variable not set\",\n)\n\nrequires_openai = pytest.mark.skipif(\n    not OPENAI_API_KEY,\n    reason=\"OPENAI_API_KEY environment variable not set\",\n)\n\n\n@pytest.fixture\ndef sqlite_session_factory(tmp_path):\n    \"\"\"Create a SQLite session factory for testing.\"\"\"\n    db_path = tmp_path / \"test_memori.db\"\n    engine = create_engine(\n        f\"sqlite:///{db_path}\",\n        connect_args={\"check_same_thread\": False},\n        poolclass=NullPool,\n    )\n\n    @event.listens_for(engine, \"connect\")\n    def set_sqlite_pragma(dbapi_conn, connection_record):\n        cursor = dbapi_conn.cursor()\n        cursor.execute(\"PRAGMA foreign_keys=ON\")\n        cursor.execute(\"PRAGMA journal_mode=WAL\")\n        cursor.close()\n\n    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n    yield Session\n\n    time.sleep(0.2)\n    engine.dispose()\n\n\n@pytest.fixture\ndef postgres_session_factory():\n    \"\"\"Create a PostgreSQL session factory for testing.\"\"\"\n    postgres_url = POSTGRES_DATABASE_URL\n    if postgres_url is None:\n        pytest.skip(\"POSTGRES_DATABASE_URL not set\")\n    assert postgres_url is not None\n\n    engine = create_engine(\n        postgres_url,\n        pool_pre_ping=True,\n        pool_recycle=300,\n    )\n\n    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n    yield Session\n\n    engine.dispose()\n\n\n@pytest.fixture\ndef mysql_session_factory():\n    \"\"\"Create a MySQL session factory for testing.\"\"\"\n    mysql_url = MYSQL_DATABASE_URL\n    if mysql_url is None:\n        pytest.skip(\"MYSQL_DATABASE_URL not set\")\n    assert mysql_url is not None\n\n    engine = create_engine(\n        mysql_url,\n        pool_pre_ping=True,\n        pool_recycle=300,\n    )\n\n    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n    yield Session\n\n    engine.dispose()\n\n\n@pytest.fixture\ndef mongodb_client():\n    \"\"\"Create a MongoDB client for testing.\"\"\"\n    if not MONGODB_URL:\n        pytest.skip(\"MONGODB_URL not set\")\n\n    client = MongoClient(MONGODB_URL)\n\n    yield client\n\n    client.close()\n\n\n@pytest.fixture\ndef memori_test_mode():\n    \"\"\"Enable Memori test mode.\"\"\"\n    original = os.environ.get(\"MEMORI_TEST_MODE\")\n    os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n    yield\n    if original is None:\n        os.environ.pop(\"MEMORI_TEST_MODE\", None)\n    else:\n        os.environ[\"MEMORI_TEST_MODE\"] = original\n\n\n@pytest.fixture\ndef sqlite_memori(sqlite_session_factory, memori_test_mode):\n    \"\"\"Create a Memori instance with SQLite backend.\"\"\"\n    from memori import Memori\n\n    mem = Memori(conn=sqlite_session_factory)\n    mem.config.storage.build()\n\n    yield mem\n\n    time.sleep(0.1)\n    mem.close()\n\n\n@pytest.fixture\ndef postgres_memori(postgres_session_factory, memori_test_mode):\n    \"\"\"Create a Memori instance with PostgreSQL backend.\"\"\"\n    from memori import Memori\n\n    mem = Memori(conn=postgres_session_factory)\n    mem.config.storage.build()\n\n    yield mem\n\n    time.sleep(0.1)\n    mem.close()\n\n\n@pytest.fixture\ndef mysql_memori(mysql_session_factory, memori_test_mode):\n    \"\"\"Create a Memori instance with MySQL backend.\"\"\"\n    from memori import Memori\n\n    mem = Memori(conn=mysql_session_factory)\n    mem.config.storage.build()\n\n    yield mem\n\n    time.sleep(0.1)\n    mem.close()\n\n\n@pytest.fixture\ndef mongodb_memori(mongodb_client, memori_test_mode):\n    \"\"\"Create a Memori instance with MongoDB backend.\"\"\"\n    from memori import Memori\n\n    mem = Memori(conn=mongodb_client)\n    mem.config.storage.build()\n\n    yield mem\n\n    time.sleep(0.1)\n    mem.close()\n\n\n@pytest.fixture(scope=\"session\")\ndef openai_api_key():\n    \"\"\"Get OpenAI API key.\"\"\"\n    if not OPENAI_API_KEY:\n        pytest.skip(\"OPENAI_API_KEY not set\")\n    return OPENAI_API_KEY\n\n\n@pytest.fixture\ndef openai_client(openai_api_key):\n    \"\"\"Create an OpenAI client.\"\"\"\n    from openai import OpenAI\n\n    return OpenAI(api_key=openai_api_key)\n"
  },
  {
    "path": "tests/integration/databases/test_database_storage.py",
    "content": "\"\"\"\nDatabase integration tests for validating memories are properly written to and\nretrieved from SQLite, PostgreSQL, MySQL, and MongoDB databases.\n\"\"\"\n\nimport pytest\nfrom openai import OpenAI\n\nfrom tests.integration.databases.conftest import (\n    requires_mongodb,\n    requires_mysql,\n    requires_openai,\n    requires_postgres,\n    requires_sqlite,\n)\n\nMODEL = \"gpt-4o-mini\"\nMAX_TOKENS = 50\n\n\nclass TestSQLiteStorage:\n    \"\"\"Test suite for SQLite database storage.\"\"\"\n\n    @requires_sqlite\n    @requires_openai\n    @pytest.mark.integration\n    def test_store_and_search_facts(self, sqlite_memori, openai_api_key):\n        \"\"\"Test that facts are stored and can be searched in SQLite.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        sqlite_memori.llm.register(client)\n        sqlite_memori.attribution(entity_id=\"sqlite-test-user\", process_id=\"test\")\n\n        # Make a conversation that should store facts\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"user\", \"content\": \"My name is Alice and I live in Paris.\"}\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        # Verify conversation was stored\n        conversation_id = sqlite_memori.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = sqlite_memori.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_sqlite\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_entities_isolation(self, sqlite_memori, openai_api_key):\n        \"\"\"Test that facts from different entities are isolated in SQLite.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        sqlite_memori.llm.register(client)\n\n        # First entity\n        sqlite_memori.attribution(entity_id=\"sqlite-user-1\", process_id=\"test\")\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"I am User One.\"}],\n            max_tokens=MAX_TOKENS,\n        )\n        conversation_id_1 = sqlite_memori.config.cache.conversation_id\n\n        # New session for second entity\n        sqlite_memori.new_session()\n\n        # Second entity\n        sqlite_memori.attribution(entity_id=\"sqlite-user-2\", process_id=\"test\")\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"I am User Two.\"}],\n            max_tokens=MAX_TOKENS,\n        )\n        conversation_id_2 = sqlite_memori.config.cache.conversation_id\n\n        # Verify both conversations exist and are different\n        assert conversation_id_1 is not None\n        assert conversation_id_2 is not None\n        assert conversation_id_1 != conversation_id_2\n\n    @requires_sqlite\n    @requires_openai\n    @pytest.mark.integration\n    def test_conversation_storage(self, sqlite_memori, openai_api_key):\n        \"\"\"Test that conversation messages are stored correctly in SQLite.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        sqlite_memori.llm.register(client)\n        sqlite_memori.attribution(entity_id=\"sqlite-conv-user\", process_id=\"test\")\n\n        test_message = \"Hello, this is a test message for SQLite storage.\"\n\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_message}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = sqlite_memori.config.cache.conversation_id\n        messages = sqlite_memori.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        # Should have at least user and assistant messages\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_message in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n\n\nclass TestPostgresStorage:\n    \"\"\"Test suite for PostgreSQL database storage.\"\"\"\n\n    @requires_postgres\n    @requires_openai\n    @pytest.mark.integration\n    def test_store_and_search_facts(self, postgres_memori, openai_api_key):\n        \"\"\"Test that facts are stored and can be searched in PostgreSQL.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        postgres_memori.llm.register(client)\n        postgres_memori.attribution(entity_id=\"postgres-test-user\", process_id=\"test\")\n\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"user\", \"content\": \"My name is Bob and I work at Acme Corp.\"}\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = postgres_memori.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = postgres_memori.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_postgres\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_entities_isolation(self, postgres_memori, openai_api_key):\n        \"\"\"Test that facts from different entities are isolated in PostgreSQL.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        postgres_memori.llm.register(client)\n\n        # First entity\n        postgres_memori.attribution(entity_id=\"postgres-user-1\", process_id=\"test\")\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"I am Postgres User One.\"}],\n            max_tokens=MAX_TOKENS,\n        )\n        conversation_id_1 = postgres_memori.config.cache.conversation_id\n\n        # New session for second entity\n        postgres_memori.new_session()\n\n        # Second entity\n        postgres_memori.attribution(entity_id=\"postgres-user-2\", process_id=\"test\")\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"I am Postgres User Two.\"}],\n            max_tokens=MAX_TOKENS,\n        )\n        conversation_id_2 = postgres_memori.config.cache.conversation_id\n\n        assert conversation_id_1 is not None\n        assert conversation_id_2 is not None\n        assert conversation_id_1 != conversation_id_2\n\n    @requires_postgres\n    @requires_openai\n    @pytest.mark.integration\n    def test_conversation_storage(self, postgres_memori, openai_api_key):\n        \"\"\"Test that conversation messages are stored correctly in PostgreSQL.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        postgres_memori.llm.register(client)\n        postgres_memori.attribution(entity_id=\"postgres-conv-user\", process_id=\"test\")\n\n        test_message = \"Hello, this is a test message for PostgreSQL storage.\"\n\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_message}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = postgres_memori.config.cache.conversation_id\n        messages = postgres_memori.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_message in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n\n\nclass TestMySQLStorage:\n    \"\"\"Test suite for MySQL database storage.\"\"\"\n\n    @requires_mysql\n    @requires_openai\n    @pytest.mark.integration\n    def test_store_and_search_facts(self, mysql_memori, openai_api_key):\n        \"\"\"Test that facts are stored and can be searched in MySQL.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        mysql_memori.llm.register(client)\n        mysql_memori.attribution(entity_id=\"mysql-test-user\", process_id=\"test\")\n\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\n                    \"role\": \"user\",\n                    \"content\": \"My name is Charlie and I enjoy programming.\",\n                }\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = mysql_memori.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = mysql_memori.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_mysql\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_entities_isolation(self, mysql_memori, openai_api_key):\n        \"\"\"Test that facts from different entities are isolated in MySQL.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        mysql_memori.llm.register(client)\n\n        # First entity\n        mysql_memori.attribution(entity_id=\"mysql-user-1\", process_id=\"test\")\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"I am MySQL User One.\"}],\n            max_tokens=MAX_TOKENS,\n        )\n        conversation_id_1 = mysql_memori.config.cache.conversation_id\n\n        # New session for second entity\n        mysql_memori.new_session()\n\n        # Second entity\n        mysql_memori.attribution(entity_id=\"mysql-user-2\", process_id=\"test\")\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"I am MySQL User Two.\"}],\n            max_tokens=MAX_TOKENS,\n        )\n        conversation_id_2 = mysql_memori.config.cache.conversation_id\n\n        assert conversation_id_1 is not None\n        assert conversation_id_2 is not None\n        assert conversation_id_1 != conversation_id_2\n\n    @requires_mysql\n    @requires_openai\n    @pytest.mark.integration\n    def test_conversation_storage(self, mysql_memori, openai_api_key):\n        \"\"\"Test that conversation messages are stored correctly in MySQL.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        mysql_memori.llm.register(client)\n        mysql_memori.attribution(entity_id=\"mysql-conv-user\", process_id=\"test\")\n\n        test_message = \"Hello, this is a test message for MySQL storage.\"\n\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_message}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = mysql_memori.config.cache.conversation_id\n        messages = mysql_memori.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_message in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n\n\nclass TestMongoDBStorage:\n    \"\"\"Test suite for MongoDB database storage.\"\"\"\n\n    @requires_mongodb\n    @requires_openai\n    @pytest.mark.integration\n    def test_store_and_search_facts(self, mongodb_memori, openai_api_key):\n        \"\"\"Test that facts are stored and can be searched in MongoDB.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        mongodb_memori.llm.register(client)\n        mongodb_memori.attribution(entity_id=\"mongodb-test-user\", process_id=\"test\")\n\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"user\", \"content\": \"My name is Diana and I love databases.\"}\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = mongodb_memori.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = mongodb_memori.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_mongodb\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_entities_isolation(self, mongodb_memori, openai_api_key):\n        \"\"\"Test that facts from different entities are isolated in MongoDB.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        mongodb_memori.llm.register(client)\n\n        # First entity\n        mongodb_memori.attribution(entity_id=\"mongodb-user-1\", process_id=\"test\")\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"I am MongoDB User One.\"}],\n            max_tokens=MAX_TOKENS,\n        )\n        conversation_id_1 = mongodb_memori.config.cache.conversation_id\n\n        # New session for second entity\n        mongodb_memori.new_session()\n\n        # Second entity\n        mongodb_memori.attribution(entity_id=\"mongodb-user-2\", process_id=\"test\")\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"I am MongoDB User Two.\"}],\n            max_tokens=MAX_TOKENS,\n        )\n        conversation_id_2 = mongodb_memori.config.cache.conversation_id\n\n        assert conversation_id_1 is not None\n        assert conversation_id_2 is not None\n        assert conversation_id_1 != conversation_id_2\n\n    @requires_mongodb\n    @requires_openai\n    @pytest.mark.integration\n    def test_conversation_storage(self, mongodb_memori, openai_api_key):\n        \"\"\"Test that conversation messages are stored correctly in MongoDB.\"\"\"\n        client = OpenAI(api_key=openai_api_key)\n        mongodb_memori.llm.register(client)\n        mongodb_memori.attribution(entity_id=\"mongodb-conv-user\", process_id=\"test\")\n\n        test_message = \"Hello, this is a test message for MongoDB storage.\"\n\n        client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_message}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = mongodb_memori.config.cache.conversation_id\n        messages = mongodb_memori.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_message in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n"
  },
  {
    "path": "tests/integration/providers/__init__.py",
    "content": "\"\"\"Integration tests for LLM providers.\"\"\"\n"
  },
  {
    "path": "tests/integration/providers/test_anthropic.py",
    "content": "import pytest\nfrom anthropic import Anthropic, APIStatusError, AsyncAnthropic, AuthenticationError\n\nfrom tests.integration.conftest import requires_anthropic\n\nMODEL = \"claude-3-haiku-20240307\"\nMAX_TOKENS = 50\nTEST_PROMPT = \"Say 'hello' in one word.\"\n\n\nclass TestClientRegistration:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_client_registration_marks_installed(\n        self, memori_instance, anthropic_api_key\n    ):\n        client = Anthropic(api_key=anthropic_api_key)\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_async_client_registration_marks_installed(\n        self, memori_instance, anthropic_api_key\n    ):\n        client = AsyncAnthropic(api_key=anthropic_api_key)\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_multiple_registrations_are_idempotent(\n        self, memori_instance, anthropic_api_key\n    ):\n        client = Anthropic(api_key=anthropic_api_key)\n\n        memori_instance.llm.register(client)\n        original_create = client.messages.create\n\n        memori_instance.llm.register(client)\n\n        assert client.messages.create is original_create\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_registration_preserves_original_methods(\n        self, memori_instance, anthropic_api_key\n    ):\n        client = Anthropic(api_key=anthropic_api_key)\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n\n\nclass TestSyncMessages:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_message_returns_response(self, registered_anthropic_client):\n        response = registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"content\")\n        assert len(response.content) > 0\n        assert response.content[0].text is not None\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_message_response_structure(self, registered_anthropic_client):\n        response = registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"content\")\n        assert hasattr(response, \"usage\")\n        assert hasattr(response, \"stop_reason\")\n\n        assert len(response.content) > 0\n        assert hasattr(response.content[0], \"type\")\n        assert hasattr(response.content[0], \"text\")\n        assert response.content[0].type == \"text\"\n        assert response.role == \"assistant\"\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_message_with_system_message(self, registered_anthropic_client):\n        response = registered_anthropic_client.messages.create(\n            model=MODEL,\n            system=\"You are a helpful assistant.\",\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.content[0].text is not None\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_message_multi_turn(self, registered_anthropic_client):\n        response = registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"user\", \"content\": \"My name is Alice.\"},\n                {\"role\": \"assistant\", \"content\": \"Nice to meet you, Alice!\"},\n                {\"role\": \"user\", \"content\": \"What is my name?\"},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        content = response.content[0].text.lower()\n        assert \"alice\" in content\n\n\nclass TestAsyncMessages:\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_message_returns_response(\n        self, registered_async_anthropic_client\n    ):\n        response = await registered_async_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"content\")\n        assert len(response.content) > 0\n        assert response.content[0].text is not None\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_message_response_structure(\n        self, registered_async_anthropic_client\n    ):\n        response = await registered_async_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"content\")\n        assert hasattr(response, \"usage\")\n\n        assert len(response.content) > 0\n        assert response.content[0].type == \"text\"\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_message_with_system(self, registered_async_anthropic_client):\n        response = await registered_async_anthropic_client.messages.create(\n            model=MODEL,\n            system=\"You are a helpful assistant.\",\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.content[0].text is not None\n\n\nclass TestSyncStreaming:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_streaming_returns_events(self, registered_anthropic_client):\n        with registered_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            events = list(stream.text_stream)\n\n        assert len(events) > 0\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_streaming_assembles_content(self, registered_anthropic_client):\n        with registered_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            full_content = \"\".join(stream.text_stream)\n\n        assert len(full_content) > 0\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_sync_streaming_event_structure(self, registered_anthropic_client):\n        with registered_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            events = list(stream.text_stream)\n\n        assert len(events) > 0\n        assert all(isinstance(e, str) for e in events)\n\n\nclass TestAsyncStreaming:\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_returns_events(\n        self, registered_async_anthropic_client\n    ):\n        async with registered_async_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            events = []\n            async for text in stream.text_stream:\n                events.append(text)\n\n        assert len(events) > 0\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_assembles_content(\n        self, registered_async_anthropic_client\n    ):\n        async with registered_async_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            content_parts = []\n            async for text in stream.text_stream:\n                content_parts.append(text)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_chunk_structure(\n        self, registered_async_anthropic_client\n    ):\n        async with registered_async_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            async for _ in stream.text_stream:\n                pass\n            final_message = await stream.get_final_message()\n\n        assert hasattr(final_message, \"id\")\n        assert hasattr(final_message, \"model\")\n        assert hasattr(final_message, \"content\")\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_final_message(\n        self, registered_async_anthropic_client\n    ):\n        async with registered_async_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            async for _ in stream.text_stream:\n                pass\n            final_message = await stream.get_final_message()\n\n        assert final_message is not None\n        assert hasattr(final_message, \"content\")\n        assert len(final_message.content) > 0\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_with_usage_info(\n        self, registered_async_anthropic_client\n    ):\n        async with registered_async_anthropic_client.messages.stream(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        ) as stream:\n            async for _ in stream.text_stream:\n                pass\n            final_message = await stream.get_final_message()\n\n        assert final_message is not None\n        assert hasattr(final_message, \"usage\")\n        assert final_message.usage is not None\n        assert hasattr(final_message.usage, \"input_tokens\")\n        assert hasattr(final_message.usage, \"output_tokens\")\n\n\nclass TestErrorHandling:\n    @pytest.mark.integration\n    def test_invalid_api_key_raises_authentication_error(self, memori_instance):\n        client = Anthropic(api_key=\"invalid-key-12345\")\n        memori_instance.llm.register(client)\n\n        with pytest.raises(AuthenticationError):\n            client.messages.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_invalid_model_raises_error(self, registered_anthropic_client):\n        with pytest.raises(APIStatusError):\n            registered_anthropic_client.messages.create(\n                model=\"nonexistent-model-xyz\",\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invalid_api_key_raises_error(self, memori_instance):\n        client = AsyncAnthropic(api_key=\"invalid-key-12345\")\n        memori_instance.llm.register(client)\n\n        with pytest.raises(AuthenticationError):\n            await client.messages.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n\nclass TestResponseFormatValidation:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_response_contains_usage_metadata(self, registered_anthropic_client):\n        response = registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response.usage is not None\n        assert response.usage.input_tokens > 0\n        assert response.usage.output_tokens > 0\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_response_model_matches_request(self, registered_anthropic_client):\n        response = registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert \"claude\" in response.model.lower()\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_response_stop_reason_is_valid(self, registered_anthropic_client):\n        response = registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        valid_reasons = {\"end_turn\", \"max_tokens\", \"stop_sequence\", \"tool_use\"}\n        assert response.stop_reason in valid_reasons\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_response_contains_usage_metadata(\n        self, registered_async_anthropic_client\n    ):\n        response = await registered_async_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response.usage is not None\n        assert response.usage.input_tokens > 0\n        assert response.usage.output_tokens > 0\n\n\nclass TestMemoriIntegration:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_memori_wrapper_does_not_modify_response_type(\n        self, anthropic_api_key, memori_instance\n    ):\n        unwrapped_client = Anthropic(api_key=anthropic_api_key)\n\n        wrapped_client = Anthropic(api_key=anthropic_api_key)\n        memori_instance.llm.register(wrapped_client)\n        memori_instance.attribution(entity_id=\"test\", process_id=\"test\")\n\n        unwrapped_response = unwrapped_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        wrapped_response = wrapped_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert type(unwrapped_response) is type(wrapped_response)\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_config_captures_provider_info(self, memori_instance, anthropic_api_key):\n        client = Anthropic(api_key=anthropic_api_key)\n        memori_instance.llm.register(client)\n\n        assert memori_instance.config.llm.provider_sdk_version is not None\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_attribution_is_preserved_across_calls(\n        self, registered_anthropic_client, memori_instance\n    ):\n        memori_instance.attribution(entity_id=\"user-123\", process_id=\"process-456\")\n\n        registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n        registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n\nclass TestStorageVerification:\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_conversation_stored_after_sync_call(\n        self, registered_anthropic_client, memori_instance\n    ):\n        registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_messages_stored_with_content(\n        self, registered_anthropic_client, memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_query}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        messages = memori_instance.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n        assert len(assistant_messages[0][\"content\"]) > 0\n\n    @requires_anthropic\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_conversation_stored_after_async_call(\n        self, registered_async_anthropic_client, memori_instance\n    ):\n        await registered_async_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n    @requires_anthropic\n    @pytest.mark.integration\n    def test_multiple_calls_accumulate_messages(\n        self, registered_anthropic_client, memori_instance\n    ):\n        registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"First question\"}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        messages_after_first = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_first = len(messages_after_first)\n\n        registered_anthropic_client.messages.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"Second question\"}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        messages_after_second = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_second = len(messages_after_second)\n\n        assert count_after_second > count_after_first\n"
  },
  {
    "path": "tests/integration/providers/test_bedrock.py",
    "content": "import pytest\n\nfrom tests.integration.conftest import BEDROCK_SDK_AVAILABLE, requires_bedrock\n\npytestmark = pytest.mark.skipif(\n    not BEDROCK_SDK_AVAILABLE,\n    reason=\"langchain-aws package not installed (pip install langchain-aws)\",\n)\n\nMODEL_ID = \"anthropic.claude-3-haiku-20240307-v1:0\"\nTEST_PROMPT = \"Say 'hello' in one word.\"\n\n\nclass TestClientRegistration:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_client_registration_marks_installed(\n        self, memori_instance, aws_credentials\n    ):\n        from langchain_aws import ChatBedrock\n\n        client = ChatBedrock(\n            model=MODEL_ID,\n            region_name=aws_credentials[\"region_name\"],\n        )\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(chatbedrock=client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert client._memori_installed is True\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_multiple_registrations_are_idempotent(\n        self, memori_instance, aws_credentials\n    ):\n        from langchain_aws import ChatBedrock\n\n        client = ChatBedrock(\n            model=MODEL_ID,\n            region_name=aws_credentials[\"region_name\"],\n        )\n\n        memori_instance.llm.register(chatbedrock=client)\n        original_invoke = client.invoke\n\n        memori_instance.llm.register(chatbedrock=client)\n\n        assert client.invoke is original_invoke\n        assert hasattr(client, \"_memori_installed\")\n        assert client._memori_installed is True\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_registration_preserves_original_methods(\n        self, memori_instance, aws_credentials\n    ):\n        from langchain_aws import ChatBedrock\n\n        client = ChatBedrock(\n            model=MODEL_ID,\n            region_name=aws_credentials[\"region_name\"],\n        )\n\n        memori_instance.llm.register(chatbedrock=client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert client._memori_installed is True\n\n\nclass TestSyncInvocation:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_invoke_returns_response(self, registered_bedrock_client):\n        response = registered_bedrock_client.invoke(TEST_PROMPT)\n\n        assert response is not None\n        assert hasattr(response, \"content\")\n        assert len(response.content) > 0\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_invoke_response_structure(self, registered_bedrock_client):\n        response = registered_bedrock_client.invoke(TEST_PROMPT)\n\n        assert hasattr(response, \"content\")\n        assert hasattr(response, \"response_metadata\")\n        assert response.type == \"ai\"\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_invoke_with_messages(self, registered_bedrock_client):\n        from langchain_core.messages import HumanMessage, SystemMessage\n\n        response = registered_bedrock_client.invoke(\n            [\n                SystemMessage(content=\"You are a helpful assistant.\"),\n                HumanMessage(content=TEST_PROMPT),\n            ]\n        )\n\n        assert response is not None\n        assert len(response.content) > 0\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_invoke_multi_turn(self, registered_bedrock_client):\n        from langchain_core.messages import AIMessage, HumanMessage\n\n        response = registered_bedrock_client.invoke(\n            [\n                HumanMessage(content=\"My name is Alice.\"),\n                AIMessage(content=\"Nice to meet you, Alice!\"),\n                HumanMessage(content=\"What is my name?\"),\n            ]\n        )\n\n        assert response is not None\n        content = response.content.lower()\n        assert \"alice\" in content\n\n\nclass TestAsyncInvocation:\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invoke_returns_response(self, registered_bedrock_client):\n        response = await registered_bedrock_client.ainvoke(TEST_PROMPT)\n\n        assert response is not None\n        assert hasattr(response, \"content\")\n        assert len(response.content) > 0\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invoke_response_structure(self, registered_bedrock_client):\n        response = await registered_bedrock_client.ainvoke(TEST_PROMPT)\n\n        assert hasattr(response, \"content\")\n        assert hasattr(response, \"response_metadata\")\n        assert response.type == \"ai\"\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invoke_with_system_message(self, registered_bedrock_client):\n        from langchain_core.messages import HumanMessage, SystemMessage\n\n        response = await registered_bedrock_client.ainvoke(\n            [\n                SystemMessage(content=\"You are a helpful assistant.\"),\n                HumanMessage(content=TEST_PROMPT),\n            ]\n        )\n\n        assert response is not None\n        assert len(response.content) > 0\n\n\nclass TestSyncStreaming:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_streaming_returns_chunks(self, registered_bedrock_client):\n        chunks = list(registered_bedrock_client.stream(TEST_PROMPT))\n\n        assert len(chunks) > 0\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_streaming_assembles_content(self, registered_bedrock_client):\n        content_parts = []\n        for chunk in registered_bedrock_client.stream(TEST_PROMPT):\n            if hasattr(chunk, \"content\") and chunk.content:\n                content_parts.append(chunk.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_sync_streaming_chunk_structure(self, registered_bedrock_client):\n        for chunk in registered_bedrock_client.stream(TEST_PROMPT):\n            assert hasattr(chunk, \"content\")\n\n\nclass TestAsyncStreaming:\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_returns_chunks(self, registered_bedrock_client):\n        chunks = []\n        async for chunk in registered_bedrock_client.astream(TEST_PROMPT):\n            chunks.append(chunk)\n\n        assert len(chunks) > 0\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_assembles_content(self, registered_bedrock_client):\n        content_parts = []\n        async for chunk in registered_bedrock_client.astream(TEST_PROMPT):\n            if hasattr(chunk, \"content\") and chunk.content:\n                content_parts.append(chunk.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_chunk_structure(self, registered_bedrock_client):\n        async for chunk in registered_bedrock_client.astream(TEST_PROMPT):\n            assert hasattr(chunk, \"content\")\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_with_usage_info(self, registered_bedrock_client):\n        last_chunk = None\n        async for chunk in registered_bedrock_client.astream(TEST_PROMPT):\n            last_chunk = chunk\n\n        assert last_chunk is not None\n        assert hasattr(last_chunk, \"content\")\n        if hasattr(last_chunk, \"response_metadata\"):\n            metadata = last_chunk.response_metadata\n            assert metadata is not None\n\n\nclass TestErrorHandling:\n    @pytest.mark.integration\n    def test_invalid_credentials_raises_error(self, memori_instance):\n        import os\n        from unittest.mock import patch\n\n        from langchain_aws import ChatBedrock\n\n        with patch.dict(\n            os.environ,\n            {\n                \"AWS_ACCESS_KEY_ID\": \"invalid-key\",\n                \"AWS_SECRET_ACCESS_KEY\": \"invalid-secret\",\n            },\n        ):\n            client = ChatBedrock(\n                model=MODEL_ID,\n                region_name=\"us-east-1\",\n            )\n            memori_instance.llm.register(chatbedrock=client)\n\n            with pytest.raises((ValueError, RuntimeError, Exception)):\n                client.invoke(TEST_PROMPT)\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_invalid_model_raises_error(self, memori_instance, aws_credentials):\n        from langchain_aws import ChatBedrock\n\n        client = ChatBedrock(\n            model=\"invalid-model-xyz\",\n            region_name=aws_credentials[\"region_name\"],\n        )\n        memori_instance.llm.register(chatbedrock=client)\n\n        with pytest.raises((ValueError, RuntimeError, TypeError)):\n            client.invoke(TEST_PROMPT)\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invalid_model_raises_error(\n        self, memori_instance, aws_credentials\n    ):\n        from langchain_aws import ChatBedrock\n\n        client = ChatBedrock(\n            model=\"invalid-model-xyz\",\n            region_name=aws_credentials[\"region_name\"],\n        )\n        memori_instance.llm.register(chatbedrock=client)\n\n        with pytest.raises((ValueError, RuntimeError, TypeError)):\n            await client.ainvoke(TEST_PROMPT)\n\n\nclass TestResponseFormatValidation:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_response_contains_usage_metadata(self, registered_bedrock_client):\n        response = registered_bedrock_client.invoke(TEST_PROMPT)\n\n        assert response.response_metadata is not None\n        metadata = response.response_metadata\n        assert \"usage\" in metadata or \"stopReason\" in metadata\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_response_model_matches_requested(self, registered_bedrock_client):\n        response = registered_bedrock_client.invoke(TEST_PROMPT)\n\n        metadata = response.response_metadata\n        assert metadata is not None\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_response_finish_reason_is_valid(self, registered_bedrock_client):\n        response = registered_bedrock_client.invoke(TEST_PROMPT)\n\n        metadata = response.response_metadata\n        if \"stopReason\" in metadata:\n            valid_reasons = {\"end_turn\", \"max_tokens\", \"stop_sequence\", \"tool_use\"}\n            assert metadata[\"stopReason\"] in valid_reasons\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_response_contains_usage_metadata(\n        self, registered_bedrock_client\n    ):\n        response = await registered_bedrock_client.ainvoke(TEST_PROMPT)\n\n        assert response.response_metadata is not None\n\n\nclass TestMemoriIntegration:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_memori_wrapper_does_not_modify_response_type(\n        self, aws_credentials, memori_instance\n    ):\n        from langchain_aws import ChatBedrock\n\n        unwrapped_client = ChatBedrock(\n            model=MODEL_ID,\n            region_name=aws_credentials[\"region_name\"],\n        )\n\n        wrapped_client = ChatBedrock(\n            model=MODEL_ID,\n            region_name=aws_credentials[\"region_name\"],\n        )\n        memori_instance.llm.register(chatbedrock=wrapped_client)\n        memori_instance.attribution(entity_id=\"test\", process_id=\"test\")\n\n        unwrapped_response = unwrapped_client.invoke(TEST_PROMPT)\n        wrapped_response = wrapped_client.invoke(TEST_PROMPT)\n\n        assert type(unwrapped_response) is type(wrapped_response)\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_config_captures_provider_info(self, memori_instance, aws_credentials):\n        from langchain_aws import ChatBedrock\n\n        client = ChatBedrock(\n            model=MODEL_ID,\n            region_name=aws_credentials[\"region_name\"],\n        )\n        memori_instance.llm.register(chatbedrock=client)\n\n        assert memori_instance.config.llm.provider_sdk_version is not None\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_attribution_is_preserved_across_calls(\n        self, registered_bedrock_client, memori_instance\n    ):\n        memori_instance.attribution(entity_id=\"user-123\", process_id=\"process-456\")\n\n        registered_bedrock_client.invoke(TEST_PROMPT)\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n        registered_bedrock_client.invoke(TEST_PROMPT)\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n\nclass TestStorageVerification:\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_conversation_stored_after_sync_call(\n        self, registered_bedrock_client, memori_instance\n    ):\n        registered_bedrock_client.invoke(TEST_PROMPT)\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_messages_stored_with_content(\n        self, registered_bedrock_client, memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        registered_bedrock_client.invoke(test_query)\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        messages = memori_instance.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 1\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n\n    @requires_bedrock\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_conversation_stored_after_async_call(\n        self, registered_bedrock_client, memori_instance\n    ):\n        await registered_bedrock_client.ainvoke(TEST_PROMPT)\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n    @requires_bedrock\n    @pytest.mark.integration\n    def test_multiple_calls_accumulate_messages(\n        self, registered_bedrock_client, memori_instance\n    ):\n        registered_bedrock_client.invoke(\"First question\")\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        messages_after_first = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_first = len(messages_after_first)\n\n        registered_bedrock_client.invoke(\"Second question\")\n\n        messages_after_second = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_second = len(messages_after_second)\n\n        assert count_after_second > count_after_first\n"
  },
  {
    "path": "tests/integration/providers/test_google.py",
    "content": "import pytest\n\nfrom tests.integration.conftest import GOOGLE_SDK_AVAILABLE, requires_google\n\npytestmark = pytest.mark.skipif(\n    not GOOGLE_SDK_AVAILABLE,\n    reason=\"google-genai package not installed (pip install google-genai)\",\n)\n\nMODEL = \"gemini-2.0-flash\"\nTEST_PROMPT = \"Say 'hello' in one word.\"\n\n\nclass TestClientRegistration:\n    @requires_google\n    @pytest.mark.integration\n    def test_client_registration_marks_installed(self, memori_instance, google_api_key):\n        from google import genai\n\n        client = genai.Client(api_key=google_api_key)\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n        client.close()\n\n    @requires_google\n    @pytest.mark.integration\n    def test_multiple_registrations_are_idempotent(\n        self, memori_instance, google_api_key\n    ):\n        from google import genai\n\n        client = genai.Client(api_key=google_api_key)\n\n        memori_instance.llm.register(client)\n        original_generate = client.models.generate_content\n\n        memori_instance.llm.register(client)\n\n        assert client.models.generate_content is original_generate\n        assert getattr(client, \"_memori_installed\", False) is True\n\n        client.close()\n\n    @requires_google\n    @pytest.mark.integration\n    def test_registration_preserves_original_methods(\n        self, memori_instance, google_api_key\n    ):\n        from google import genai\n\n        client = genai.Client(api_key=google_api_key)\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n        client.close()\n\n\nclass TestSyncContentGeneration:\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_generate_returns_response(self, registered_google_client):\n        response = registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"text\")\n        assert len(response.text) > 0\n\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_generate_response_structure(self, registered_google_client):\n        response = registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert hasattr(response, \"candidates\")\n        assert len(response.candidates) > 0\n        assert hasattr(response.candidates[0], \"content\")\n        assert hasattr(response.candidates[0].content, \"parts\")\n        assert len(response.candidates[0].content.parts) > 0\n\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_generate_with_config(self, registered_google_client):\n        from google.genai.types import GenerateContentConfig\n\n        response = registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n            config=GenerateContentConfig(\n                max_output_tokens=50,\n                temperature=0.5,\n            ),\n        )\n\n        assert response is not None\n        assert len(response.text) > 0\n\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_generate_multi_turn(self, registered_google_client):\n        from google.genai.types import Content, Part\n\n        response = registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=[\n                Content(role=\"user\", parts=[Part(text=\"My name is Alice.\")]),\n                Content(role=\"model\", parts=[Part(text=\"Nice to meet you, Alice!\")]),\n                Content(role=\"user\", parts=[Part(text=\"What is my name?\")]),\n            ],\n        )\n\n        assert response is not None\n        content = response.text.lower()\n        assert \"alice\" in content\n\n\nclass TestAsyncContentGeneration:\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_generate_returns_response(self, registered_google_client):\n        response = await registered_google_client.aio.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"text\")\n        assert len(response.text) > 0\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_generate_response_structure(self, registered_google_client):\n        response = await registered_google_client.aio.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert hasattr(response, \"candidates\")\n        assert len(response.candidates) > 0\n        assert hasattr(response.candidates[0], \"content\")\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_generate_with_system_instruction(\n        self, registered_google_client\n    ):\n        from google.genai.types import GenerateContentConfig\n\n        response = await registered_google_client.aio.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n            config=GenerateContentConfig(\n                system_instruction=\"You are a helpful assistant.\",\n                max_output_tokens=50,\n            ),\n        )\n\n        assert response is not None\n        assert len(response.text) > 0\n\n\nclass TestSyncStreaming:\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_streaming_returns_chunks(self, registered_google_client):\n        stream = registered_google_client.models.generate_content_stream(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        chunks = list(stream)\n        assert len(chunks) > 0\n\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_streaming_assembles_content(self, registered_google_client):\n        stream = registered_google_client.models.generate_content_stream(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        content_parts = []\n        for chunk in stream:\n            if hasattr(chunk, \"text\") and chunk.text:\n                content_parts.append(chunk.text)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_google\n    @pytest.mark.integration\n    def test_sync_streaming_chunk_structure(self, registered_google_client):\n        stream = registered_google_client.models.generate_content_stream(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        for chunk in stream:\n            assert hasattr(chunk, \"candidates\") or hasattr(chunk, \"text\")\n\n\nclass TestAsyncStreaming:\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_returns_chunks(self, registered_google_client):\n        stream = await registered_google_client.aio.models.generate_content_stream(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        chunks = []\n        async for chunk in stream:\n            chunks.append(chunk)\n\n        assert len(chunks) > 0\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_assembles_content(self, registered_google_client):\n        stream = await registered_google_client.aio.models.generate_content_stream(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        content_parts = []\n        async for chunk in stream:\n            if hasattr(chunk, \"text\") and chunk.text:\n                content_parts.append(chunk.text)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_chunk_structure(self, registered_google_client):\n        stream = await registered_google_client.aio.models.generate_content_stream(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        async for chunk in stream:\n            assert hasattr(chunk, \"candidates\") or hasattr(chunk, \"text\")\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_with_usage_info(self, registered_google_client):\n        stream = await registered_google_client.aio.models.generate_content_stream(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        last_chunk = None\n        async for chunk in stream:\n            last_chunk = chunk\n\n        assert last_chunk is not None\n        if hasattr(last_chunk, \"usage_metadata\") and last_chunk.usage_metadata:\n            usage = last_chunk.usage_metadata\n            assert hasattr(usage, \"prompt_token_count\") or hasattr(\n                usage, \"candidates_token_count\"\n            )\n\n\nclass TestErrorHandling:\n    @pytest.mark.integration\n    def test_invalid_api_key_raises_error(self, memori_instance):\n        from google import genai\n        from google.genai import errors\n\n        client = genai.Client(api_key=\"invalid-key-12345\")\n        memori_instance.llm.register(client)\n\n        with pytest.raises((errors.APIError, Exception)):\n            client.models.generate_content(\n                model=MODEL,\n                contents=TEST_PROMPT,\n            )\n\n        client.close()\n\n    @requires_google\n    @pytest.mark.integration\n    def test_invalid_model_raises_error(self, registered_google_client):\n        from google.genai import errors\n\n        with pytest.raises((errors.APIError, Exception)):\n            registered_google_client.models.generate_content(\n                model=\"nonexistent-model-xyz\",\n                contents=TEST_PROMPT,\n            )\n\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invalid_api_key_raises_error(self, memori_instance):\n        from google import genai\n        from google.genai import errors\n\n        client = genai.Client(api_key=\"invalid-key-12345\")\n        memori_instance.llm.register(client)\n\n        with pytest.raises((errors.APIError, Exception)):\n            await client.aio.models.generate_content(\n                model=MODEL,\n                contents=TEST_PROMPT,\n            )\n\n        client.close()\n\n\nclass TestResponseFormatValidation:\n    @requires_google\n    @pytest.mark.integration\n    def test_response_contains_usage_metadata(self, registered_google_client):\n        response = registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert hasattr(response, \"usage_metadata\") or hasattr(response, \"candidates\")\n\n    @requires_google\n    @pytest.mark.integration\n    def test_response_finish_reason_is_valid(self, registered_google_client):\n        response = registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert len(response.candidates) > 0\n        candidate = response.candidates[0]\n        assert hasattr(candidate, \"finish_reason\")\n\n    @requires_google\n    @pytest.mark.integration\n    def test_response_model_info_is_present(self, registered_google_client):\n        response = registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert hasattr(response, \"model_version\") or hasattr(response, \"candidates\")\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_response_contains_usage_metadata(\n        self, registered_google_client\n    ):\n        response = await registered_google_client.aio.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert hasattr(response, \"usage_metadata\") or hasattr(response, \"candidates\")\n\n\nclass TestMemoriIntegration:\n    @requires_google\n    @pytest.mark.integration\n    def test_memori_wrapper_does_not_modify_response_type(\n        self, google_api_key, memori_instance\n    ):\n        from google import genai\n\n        unwrapped_client = genai.Client(api_key=google_api_key)\n\n        wrapped_client = genai.Client(api_key=google_api_key)\n        memori_instance.llm.register(wrapped_client)\n        memori_instance.attribution(entity_id=\"test\", process_id=\"test\")\n\n        unwrapped_response = unwrapped_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n        wrapped_response = wrapped_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert type(unwrapped_response) is type(wrapped_response)\n\n        unwrapped_client.close()\n        wrapped_client.close()\n\n    @requires_google\n    @pytest.mark.integration\n    def test_config_captures_provider_info(self, memori_instance, google_api_key):\n        from google import genai\n\n        client = genai.Client(api_key=google_api_key)\n        memori_instance.llm.register(client)\n\n        assert memori_instance.config.llm.provider_sdk_version is not None\n\n        client.close()\n\n    @requires_google\n    @pytest.mark.integration\n    def test_attribution_is_preserved_across_calls(\n        self, registered_google_client, memori_instance\n    ):\n        memori_instance.attribution(entity_id=\"user-123\", process_id=\"process-456\")\n\n        registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n        registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n\nclass TestStorageVerification:\n    @requires_google\n    @pytest.mark.integration\n    def test_conversation_stored_after_sync_call(\n        self, registered_google_client, memori_instance\n    ):\n        registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_google\n    @pytest.mark.integration\n    def test_messages_stored_with_content(\n        self, registered_google_client, memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=test_query,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        messages = memori_instance.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 1\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n    @requires_google\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_conversation_stored_after_async_call(\n        self, registered_google_client, memori_instance\n    ):\n        await registered_google_client.aio.models.generate_content(\n            model=MODEL,\n            contents=TEST_PROMPT,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n    @requires_google\n    @pytest.mark.integration\n    def test_multiple_calls_accumulate_messages(\n        self, registered_google_client, memori_instance\n    ):\n        registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=\"First question\",\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        messages_after_first = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_first = len(messages_after_first)\n\n        registered_google_client.models.generate_content(\n            model=MODEL,\n            contents=\"Second question\",\n        )\n\n        messages_after_second = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_second = len(messages_after_second)\n\n        assert count_after_second > count_after_first\n"
  },
  {
    "path": "tests/integration/providers/test_openai.py",
    "content": "import pytest\nfrom openai import (\n    AsyncOpenAI,\n    AuthenticationError,\n    BadRequestError,\n    NotFoundError,\n    OpenAI,\n)\n\nfrom tests.integration.conftest import requires_openai\n\nMODEL = \"gpt-4o-mini\"\nMAX_TOKENS = 50\nMAX_OUTPUT_TOKENS = 50\nTEST_PROMPT = \"Say 'hello' in one word.\"\n\n\nclass TestClientRegistration:\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_client_registration_marks_installed(\n        self, memori_instance, openai_api_key\n    ):\n        client = OpenAI(api_key=openai_api_key)\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_async_client_registration_marks_installed(\n        self, memori_instance, openai_api_key\n    ):\n        client = AsyncOpenAI(api_key=openai_api_key)\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_registrations_are_idempotent(\n        self, memori_instance, openai_api_key\n    ):\n        client = OpenAI(api_key=openai_api_key)\n\n        memori_instance.llm.register(client)\n        original_create = client.chat.completions.create\n\n        memori_instance.llm.register(client)\n\n        assert client.chat.completions.create is original_create\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_registration_preserves_original_methods(\n        self, memori_instance, openai_api_key\n    ):\n        client = OpenAI(api_key=openai_api_key)\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client.chat, \"_completions_create\")\n        assert hasattr(client.beta, \"_chat_completions_parse\")\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_client_registration_wraps_responses(\n        self, memori_instance, openai_api_key\n    ):\n        client = OpenAI(api_key=openai_api_key)\n\n        assert not hasattr(client, \"_memori_installed\")\n        assert not hasattr(client, \"_responses_create\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert hasattr(client, \"_responses_create\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_async_client_registration_wraps_responses(\n        self, memori_instance, openai_api_key\n    ):\n        client = AsyncOpenAI(api_key=openai_api_key)\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert hasattr(client, \"_responses_create\")\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_registrations_preserve_responses_wrapper(\n        self, memori_instance, openai_api_key\n    ):\n        client = OpenAI(api_key=openai_api_key)\n\n        memori_instance.llm.register(client)\n        original_responses_create = client.responses.create\n\n        memori_instance.llm.register(client)\n\n        assert client.responses.create is original_responses_create\n\n\nclass TestSyncChatCompletions:\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_chat_completion_returns_response(self, registered_openai_client):\n        response = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"choices\")\n        assert len(response.choices) > 0\n        assert response.choices[0].message.content is not None\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_chat_completion_response_structure(self, registered_openai_client):\n        response = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"choices\")\n        assert hasattr(response, \"usage\")\n\n        choice = response.choices[0]\n        assert hasattr(choice, \"message\")\n        assert hasattr(choice, \"finish_reason\")\n        assert hasattr(choice.message, \"role\")\n        assert hasattr(choice.message, \"content\")\n        assert choice.message.role == \"assistant\"\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_chat_completion_with_system_message(self, registered_openai_client):\n        response = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n                {\"role\": \"user\", \"content\": TEST_PROMPT},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_chat_completion_multi_turn(self, registered_openai_client):\n        response = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"user\", \"content\": \"My name is Alice.\"},\n                {\"role\": \"assistant\", \"content\": \"Nice to meet you, Alice!\"},\n                {\"role\": \"user\", \"content\": \"What is my name?\"},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        content = response.choices[0].message.content.lower()\n        assert \"alice\" in content\n\n\nclass TestAsyncChatCompletions:\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_chat_completion_returns_response(\n        self, registered_async_openai_client\n    ):\n        response = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"choices\")\n        assert len(response.choices) > 0\n        assert response.choices[0].message.content is not None\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_chat_completion_response_structure(\n        self, registered_async_openai_client\n    ):\n        response = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"choices\")\n        assert hasattr(response, \"usage\")\n\n        choice = response.choices[0]\n        assert hasattr(choice, \"message\")\n        assert choice.message.role == \"assistant\"\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_chat_completion_with_system_message(\n        self, registered_async_openai_client\n    ):\n        response = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n                {\"role\": \"user\", \"content\": TEST_PROMPT},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n\nclass TestSyncStreaming:\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_returns_chunks(self, registered_openai_client):\n        stream = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        chunks = list(stream)\n\n        assert len(chunks) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_assembles_content(self, registered_openai_client):\n        stream = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        content_parts = []\n        for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                content_parts.append(chunk.choices[0].delta.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_chunk_structure(self, registered_openai_client):\n        stream = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        for chunk in stream:\n            assert hasattr(chunk, \"choices\")\n            if chunk.choices:\n                assert hasattr(chunk.choices[0], \"delta\")\n\n\nclass TestAsyncStreaming:\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_returns_chunks(self, registered_async_openai_client):\n        stream = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        chunks = []\n        async for chunk in stream:\n            chunks.append(chunk)\n\n        assert len(chunks) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_assembles_content(\n        self, registered_async_openai_client\n    ):\n        stream = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        content_parts = []\n        async for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                content_parts.append(chunk.choices[0].delta.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_chunk_structure(\n        self, registered_async_openai_client\n    ):\n        stream = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        async for chunk in stream:\n            assert hasattr(chunk, \"choices\")\n            assert hasattr(chunk, \"id\")\n            assert hasattr(chunk, \"model\")\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_with_usage_info(\n        self, registered_async_openai_client\n    ):\n        stream = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n            stream_options={\"include_usage\": True},\n        )\n\n        last_chunk = None\n        async for chunk in stream:\n            last_chunk = chunk\n\n        assert last_chunk is not None\n\n\nclass TestErrorHandling:\n    @pytest.mark.integration\n    def test_invalid_api_key_raises_authentication_error(self, memori_instance):\n        client = OpenAI(api_key=\"invalid-key-12345\")\n        memori_instance.llm.register(client)\n\n        with pytest.raises(AuthenticationError):\n            client.chat.completions.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_invalid_model_raises_error(self, registered_openai_client):\n        with pytest.raises(NotFoundError):\n            registered_openai_client.chat.completions.create(\n                model=\"nonexistent-model-xyz\",\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invalid_api_key_raises_error(self, memori_instance):\n        client = AsyncOpenAI(api_key=\"invalid-key-12345\")\n        memori_instance.llm.register(client)\n\n        with pytest.raises(AuthenticationError):\n            await client.chat.completions.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n\nclass TestResponseFormatValidation:\n    @requires_openai\n    @pytest.mark.integration\n    def test_response_contains_usage_metadata(self, registered_openai_client):\n        response = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response.usage is not None\n        assert response.usage.prompt_tokens > 0\n        assert response.usage.completion_tokens > 0\n        assert response.usage.total_tokens > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_response_model_matches_request(self, registered_openai_client):\n        response = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert \"gpt-4o-mini\" in response.model\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_response_finish_reason_is_valid(self, registered_openai_client):\n        response = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        valid_reasons = {\n            \"stop\",\n            \"length\",\n            \"content_filter\",\n            \"tool_calls\",\n            \"function_call\",\n        }\n        assert response.choices[0].finish_reason in valid_reasons\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_response_contains_usage_metadata(\n        self, registered_async_openai_client\n    ):\n        response = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response.usage is not None\n        assert response.usage.prompt_tokens > 0\n        assert response.usage.completion_tokens > 0\n\n\nclass TestMemoriIntegration:\n    @requires_openai\n    @pytest.mark.integration\n    def test_memori_wrapper_does_not_modify_response_type(\n        self, openai_api_key, memori_instance\n    ):\n        unwrapped_client = OpenAI(api_key=openai_api_key)\n\n        wrapped_client = OpenAI(api_key=openai_api_key)\n        memori_instance.llm.register(wrapped_client)\n        memori_instance.attribution(entity_id=\"test\", process_id=\"test\")\n\n        unwrapped_response = unwrapped_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        wrapped_response = wrapped_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert type(unwrapped_response) is type(wrapped_response)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_config_captures_provider_info(self, memori_instance, openai_api_key):\n        client = OpenAI(api_key=openai_api_key)\n        memori_instance.llm.register(client)\n\n        assert memori_instance.config.llm.provider_sdk_version is not None\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_attribution_is_preserved_across_calls(\n        self, registered_openai_client, memori_instance\n    ):\n        memori_instance.attribution(entity_id=\"user-123\", process_id=\"process-456\")\n\n        registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n        registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n\nclass TestBetaApi:\n    @requires_openai\n    @pytest.mark.integration\n    def test_beta_parse_registration(self, memori_instance, openai_api_key):\n        client = OpenAI(api_key=openai_api_key)\n        memori_instance.llm.register(client)\n\n        assert hasattr(client.beta, \"_chat_completions_parse\")\n\n\nclass TestStorageVerification:\n    @requires_openai\n    @pytest.mark.integration\n    def test_conversation_stored_after_sync_call(\n        self, registered_openai_client, memori_instance\n    ):\n        registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_conversation_stored_after_async_call(\n        self, registered_async_openai_client, memori_instance\n    ):\n        await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_messages_stored_with_content(\n        self, registered_openai_client, memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_query}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        messages = memori_instance.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n        assert len(assistant_messages[0][\"content\"]) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_calls_accumulate_messages(\n        self, registered_openai_client, memori_instance\n    ):\n        registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"First question\"}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        messages_after_first = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_first = len(messages_after_first)\n\n        registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"Second question\"}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        messages_after_second = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_second = len(messages_after_second)\n\n        assert count_after_second > count_after_first\n\n\n# Responses API Tests\n\n\nclass TestSyncResponses:\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_responses_returns_response(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"output\")\n        assert hasattr(response, \"output_text\")\n        assert len(response.output_text) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_responses_response_structure(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"output\")\n        assert hasattr(response, \"status\")\n        assert response.status == \"completed\"\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_responses_with_instructions(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=\"What is 2+2?\",\n            instructions=\"You are a math tutor. Be very brief.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response is not None\n        assert len(response.output_text) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_responses_simple_math(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=\"What is 5 + 3? Answer with just the number.\",\n            instructions=\"Be very brief. Answer with just the number.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response is not None\n        assert \"8\" in response.output_text\n\n\nclass TestAsyncResponses:\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_responses_returns_response(\n        self, registered_async_openai_client\n    ):\n        response = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"output\")\n        assert hasattr(response, \"output_text\")\n        assert len(response.output_text) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_responses_response_structure(\n        self, registered_async_openai_client\n    ):\n        response = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"output\")\n        assert hasattr(response, \"status\")\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_responses_with_instructions(\n        self, registered_async_openai_client\n    ):\n        response = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=\"What is the capital of France?\",\n            instructions=\"Be brief.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response is not None\n        assert \"paris\" in response.output_text.lower()\n\n\nclass TestResponsesConversationStorage:\n    @requires_openai\n    @pytest.mark.integration\n    def test_conversation_id_created_after_call(self, memori_instance, openai_api_key):\n        client = OpenAI(api_key=openai_api_key)\n        memori_instance.llm.register(client)\n        memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n\n        assert memori_instance.config.cache.conversation_id is None\n\n        client.responses.create(\n            model=MODEL,\n            input=\"Hello\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert memori_instance.config.cache.conversation_id is not None\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_conversation_continuity(self, memori_instance, openai_api_key):\n        client = OpenAI(api_key=openai_api_key)\n        memori_instance.llm.register(client)\n        memori_instance.attribution(entity_id=\"test-entity\", process_id=\"test-process\")\n\n        client.responses.create(\n            model=MODEL,\n            input=\"My favorite color is blue.\",\n            instructions=\"Remember what the user tells you.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        response = client.responses.create(\n            model=MODEL,\n            input=\"What is my favorite color?\",\n            instructions=\"Answer based on what the user previously told you.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert \"blue\" in response.output_text.lower()\n\n\nclass TestResponsesErrorHandling:\n    @pytest.mark.integration\n    def test_invalid_api_key_raises_error(self, memori_instance):\n        client = OpenAI(api_key=\"invalid-key-12345\")\n        memori_instance.llm.register(client)\n\n        with pytest.raises(AuthenticationError):\n            client.responses.create(\n                model=MODEL,\n                input=TEST_PROMPT,\n                max_output_tokens=MAX_OUTPUT_TOKENS,\n            )\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_invalid_model_raises_error(self, registered_openai_client):\n        with pytest.raises(BadRequestError):\n            registered_openai_client.responses.create(\n                model=\"nonexistent-model-xyz\",\n                input=TEST_PROMPT,\n                max_output_tokens=MAX_OUTPUT_TOKENS,\n            )\n\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invalid_api_key_raises_error(self, memori_instance):\n        client = AsyncOpenAI(api_key=\"invalid-key-12345\")\n        memori_instance.llm.register(client)\n\n        with pytest.raises(AuthenticationError):\n            await client.responses.create(\n                model=MODEL,\n                input=TEST_PROMPT,\n                max_output_tokens=MAX_OUTPUT_TOKENS,\n            )\n\n\nclass TestResponsesWithChatCompletionsCoexistence:\n    @requires_openai\n    @pytest.mark.integration\n    def test_both_apis_work_with_same_client(self, registered_openai_client):\n        responses_result = registered_openai_client.responses.create(\n            model=MODEL,\n            input=\"Say 'responses' in one word.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n        assert responses_result is not None\n        assert len(responses_result.output_text) > 0\n\n        chat_result = registered_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"Say 'chat' in one word.\"}],\n            max_tokens=MAX_OUTPUT_TOKENS,\n        )\n        assert chat_result is not None\n        assert len(chat_result.choices[0].message.content) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_both_apis_work_with_same_client(\n        self, registered_async_openai_client\n    ):\n        responses_result = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=\"Say 'async-responses' in one word.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n        assert responses_result is not None\n        assert len(responses_result.output_text) > 0\n\n        chat_result = await registered_async_openai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"Say 'async-chat' in one word.\"}],\n            max_tokens=MAX_OUTPUT_TOKENS,\n        )\n        assert chat_result is not None\n        assert len(chat_result.choices[0].message.content) > 0\n\n\nclass TestResponsesStreaming:\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_returns_events(self, registered_openai_client):\n        stream = registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        )\n\n        events = list(stream)\n\n        assert len(events) > 0\n        event_types = [getattr(e, \"type\", None) for e in events]\n        assert \"response.completed\" in event_types\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_final_response_structure(self, registered_openai_client):\n        stream = registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        )\n\n        events = list(stream)\n        completed_events = [\n            e for e in events if hasattr(e, \"type\") and e.type == \"response.completed\"\n        ]\n\n        assert len(completed_events) == 1\n        completed_event = completed_events[0]\n\n        assert hasattr(completed_event, \"response\")\n        response = completed_event.response\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"output\")\n        assert hasattr(response, \"output_text\")\n        assert hasattr(response, \"status\")\n        assert response.status == \"completed\"\n        assert len(response.output_text) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_yields_text_deltas(self, registered_openai_client):\n        stream = registered_openai_client.responses.create(\n            model=MODEL,\n            input=\"Count from 1 to 3.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        )\n\n        delta_events = []\n        for event in stream:\n            if hasattr(event, \"type\") and \"delta\" in event.type:\n                delta_events.append(event)\n\n        assert len(delta_events) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_context_manager(self, registered_openai_client):\n        with registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        ) as stream:\n            events = list(stream)\n\n        assert len(events) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_returns_events(self, registered_async_openai_client):\n        stream = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        )\n\n        events = []\n        async for event in stream:\n            events.append(event)\n\n        assert len(events) > 0\n        event_types = [getattr(e, \"type\", None) for e in events]\n        assert \"response.completed\" in event_types\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_final_response_structure(\n        self, registered_async_openai_client\n    ):\n        stream = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        )\n\n        events = []\n        async for event in stream:\n            events.append(event)\n\n        completed_events = [\n            e for e in events if hasattr(e, \"type\") and e.type == \"response.completed\"\n        ]\n\n        assert len(completed_events) == 1\n        completed_event = completed_events[0]\n\n        assert hasattr(completed_event, \"response\")\n        response = completed_event.response\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"output\")\n        assert hasattr(response, \"output_text\")\n        assert hasattr(response, \"status\")\n        assert response.status == \"completed\"\n        assert len(response.output_text) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_context_manager(\n        self, registered_async_openai_client\n    ):\n        async with await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        ) as stream:\n            events = []\n            async for event in stream:\n                events.append(event)\n\n        assert len(events) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_event_structure(\n        self, registered_async_openai_client\n    ):\n        stream = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        )\n\n        async for event in stream:\n            assert hasattr(event, \"type\")\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_yields_text_deltas(\n        self, registered_async_openai_client\n    ):\n        stream = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=\"Count from 1 to 3.\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n            stream=True,\n        )\n\n        delta_events = []\n        async for event in stream:\n            if hasattr(event, \"type\") and \"delta\" in event.type:\n                delta_events.append(event)\n\n        assert len(delta_events) > 0\n\n\nclass TestResponsesInputFormats:\n    @requires_openai\n    @pytest.mark.integration\n    def test_string_input(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=\"Hello, world!\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response is not None\n        assert len(response.output_text) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_list_input_with_messages(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=[\n                {\"role\": \"user\", \"content\": \"My name is Alice.\"},\n                {\"role\": \"assistant\", \"content\": \"Hello Alice!\"},\n                {\"role\": \"user\", \"content\": \"What is my name?\"},\n            ],\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response is not None\n        assert \"alice\" in response.output_text.lower()\n\n\nclass TestResponsesFormatValidation:\n    @requires_openai\n    @pytest.mark.integration\n    def test_responses_contains_usage_metadata(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response.usage is not None\n        assert response.usage.input_tokens > 0\n        assert response.usage.output_tokens > 0\n        assert response.usage.total_tokens > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_responses_model_matches_request(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert \"gpt-4o-mini\" in response.model\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_responses_status_is_valid(self, registered_openai_client):\n        response = registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        valid_statuses = {\"completed\", \"failed\", \"incomplete\", \"in_progress\"}\n        assert response.status in valid_statuses\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_responses_contains_usage_metadata(\n        self, registered_async_openai_client\n    ):\n        response = await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert response.usage is not None\n        assert response.usage.input_tokens > 0\n        assert response.usage.output_tokens > 0\n\n\nclass TestResponsesMemoriIntegration:\n    @requires_openai\n    @pytest.mark.integration\n    def test_memori_wrapper_does_not_modify_response_type(\n        self, openai_api_key, memori_instance\n    ):\n        unwrapped_client = OpenAI(api_key=openai_api_key)\n\n        wrapped_client = OpenAI(api_key=openai_api_key)\n        memori_instance.llm.register(wrapped_client)\n        memori_instance.attribution(entity_id=\"test\", process_id=\"test\")\n\n        unwrapped_response = unwrapped_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        wrapped_response = wrapped_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert type(unwrapped_response) is type(wrapped_response)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_config_captures_provider_info(self, memori_instance, openai_api_key):\n        client = OpenAI(api_key=openai_api_key)\n        memori_instance.llm.register(client)\n\n        assert memori_instance.config.llm.provider_sdk_version is not None\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_attribution_is_preserved_across_calls(\n        self, registered_openai_client, memori_instance\n    ):\n        memori_instance.attribution(entity_id=\"user-123\", process_id=\"process-456\")\n\n        registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n        registered_openai_client.responses.create(\n            model=MODEL,\n            input=\"Another question\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n\nclass TestResponsesStorageVerification:\n    @requires_openai\n    @pytest.mark.integration\n    def test_conversation_stored_after_sync_call(\n        self, registered_openai_client, memori_instance\n    ):\n        registered_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_conversation_stored_after_async_call(\n        self, registered_async_openai_client, memori_instance\n    ):\n        await registered_async_openai_client.responses.create(\n            model=MODEL,\n            input=TEST_PROMPT,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_messages_stored_with_content(\n        self, registered_openai_client, memori_instance\n    ):\n        test_query = \"What is 2 + 2?\"\n\n        registered_openai_client.responses.create(\n            model=MODEL,\n            input=test_query,\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        messages = memori_instance.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n        assert len(assistant_messages[0][\"content\"]) > 0\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_calls_accumulate_messages(\n        self, registered_openai_client, memori_instance\n    ):\n        registered_openai_client.responses.create(\n            model=MODEL,\n            input=\"First question\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        messages_after_first = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_first = len(messages_after_first)\n\n        registered_openai_client.responses.create(\n            model=MODEL,\n            input=\"Second question\",\n            max_output_tokens=MAX_OUTPUT_TOKENS,\n        )\n\n        messages_after_second = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_second = len(messages_after_second)\n\n        assert count_after_second > count_after_first\n"
  },
  {
    "path": "tests/integration/providers/test_xai.py",
    "content": "import pytest\nfrom openai import (\n    AsyncOpenAI,\n    AuthenticationError,\n    BadRequestError,\n    NotFoundError,\n    OpenAI,\n)\n\nfrom tests.integration.conftest import requires_xai\n\nMODEL = \"grok-beta\"\nMAX_TOKENS = 50\nTEST_PROMPT = \"Say 'hello' in one word.\"\nXAI_BASE_URL = \"https://api.x.ai/v1\"\n\n\nclass TestClientRegistration:\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_client_registration_marks_installed(\n        self, memori_instance, xai_api_key\n    ):\n        client = OpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_async_client_registration_marks_installed(\n        self, memori_instance, xai_api_key\n    ):\n        client = AsyncOpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n\n        assert not hasattr(client, \"_memori_installed\")\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client, \"_memori_installed\")\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_multiple_registrations_are_idempotent(self, memori_instance, xai_api_key):\n        client = OpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n\n        memori_instance.llm.register(client)\n        original_create = client.chat.completions.create\n\n        memori_instance.llm.register(client)\n\n        assert client.chat.completions.create is original_create\n        assert getattr(client, \"_memori_installed\", False) is True\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_registration_preserves_original_methods(\n        self, memori_instance, xai_api_key\n    ):\n        client = OpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n\n        memori_instance.llm.register(client)\n\n        assert hasattr(client.chat, \"_completions_create\")\n\n\nclass TestSyncChatCompletions:\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_chat_completion_returns_response(self, registered_xai_client):\n        response = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"choices\")\n        assert len(response.choices) > 0\n        assert response.choices[0].message.content is not None\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_chat_completion_response_structure(self, registered_xai_client):\n        response = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"choices\")\n        assert hasattr(response, \"usage\")\n\n        choice = response.choices[0]\n        assert hasattr(choice, \"message\")\n        assert hasattr(choice, \"finish_reason\")\n        assert hasattr(choice.message, \"role\")\n        assert hasattr(choice.message, \"content\")\n        assert choice.message.role == \"assistant\"\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_chat_completion_with_system_message(self, registered_xai_client):\n        response = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n                {\"role\": \"user\", \"content\": TEST_PROMPT},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_chat_completion_multi_turn(self, registered_xai_client):\n        response = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"user\", \"content\": \"My name is Alice.\"},\n                {\"role\": \"assistant\", \"content\": \"Nice to meet you, Alice!\"},\n                {\"role\": \"user\", \"content\": \"What is my name?\"},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        content = response.choices[0].message.content.lower()\n        assert \"alice\" in content\n\n\nclass TestAsyncChatCompletions:\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_chat_completion_returns_response(\n        self, registered_async_xai_client\n    ):\n        response = await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert hasattr(response, \"choices\")\n        assert len(response.choices) > 0\n        assert response.choices[0].message.content is not None\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_chat_completion_response_structure(\n        self, registered_async_xai_client\n    ):\n        response = await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert hasattr(response, \"id\")\n        assert hasattr(response, \"model\")\n        assert hasattr(response, \"choices\")\n        assert hasattr(response, \"usage\")\n\n        choice = response.choices[0]\n        assert hasattr(choice, \"message\")\n        assert choice.message.role == \"assistant\"\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_chat_completion_with_system_message(\n        self, registered_async_xai_client\n    ):\n        response = await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[\n                {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n                {\"role\": \"user\", \"content\": TEST_PROMPT},\n            ],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n\nclass TestSyncStreaming:\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_streaming_returns_chunks(self, registered_xai_client):\n        stream = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        chunks = list(stream)\n\n        assert len(chunks) > 0\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_streaming_assembles_content(self, registered_xai_client):\n        stream = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        content_parts = []\n        for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                content_parts.append(chunk.choices[0].delta.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_sync_streaming_chunk_structure(self, registered_xai_client):\n        stream = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        for chunk in stream:\n            assert hasattr(chunk, \"choices\")\n            if chunk.choices:\n                assert hasattr(chunk.choices[0], \"delta\")\n\n\nclass TestAsyncStreaming:\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_returns_chunks(self, registered_async_xai_client):\n        stream = await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        chunks = []\n        async for chunk in stream:\n            chunks.append(chunk)\n\n        assert len(chunks) > 0\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_assembles_content(self, registered_async_xai_client):\n        stream = await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        content_parts = []\n        async for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                content_parts.append(chunk.choices[0].delta.content)\n\n        full_content = \"\".join(content_parts)\n        assert len(full_content) > 0\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_chunk_structure(self, registered_async_xai_client):\n        stream = await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        async for chunk in stream:\n            assert hasattr(chunk, \"choices\")\n            assert hasattr(chunk, \"id\")\n            assert hasattr(chunk, \"model\")\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_with_usage_info(self, registered_async_xai_client):\n        stream = await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n            stream_options={\"include_usage\": True},\n        )\n\n        last_chunk = None\n        async for chunk in stream:\n            last_chunk = chunk\n\n        assert last_chunk is not None\n\n\nclass TestErrorHandling:\n    @pytest.mark.integration\n    def test_invalid_api_key_raises_error(self, memori_instance):\n        client = OpenAI(api_key=\"invalid-key-12345\", base_url=XAI_BASE_URL)\n        memori_instance.llm.register(client)\n\n        with pytest.raises((AuthenticationError, BadRequestError)):\n            client.chat.completions.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_invalid_model_raises_error(self, registered_xai_client):\n        with pytest.raises(NotFoundError):\n            registered_xai_client.chat.completions.create(\n                model=\"nonexistent-model-xyz\",\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_invalid_api_key_raises_error(self, memori_instance):\n        client = AsyncOpenAI(api_key=\"invalid-key-12345\", base_url=XAI_BASE_URL)\n        memori_instance.llm.register(client)\n\n        with pytest.raises((AuthenticationError, BadRequestError)):\n            await client.chat.completions.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n                max_tokens=MAX_TOKENS,\n            )\n\n\nclass TestResponseFormatValidation:\n    @requires_xai\n    @pytest.mark.integration\n    def test_response_contains_usage_metadata(self, registered_xai_client):\n        response = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response.usage is not None\n        assert response.usage.prompt_tokens > 0\n        assert response.usage.completion_tokens > 0\n        assert response.usage.total_tokens > 0\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_response_model_matches_request(self, registered_xai_client):\n        response = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert \"grok\" in response.model.lower()\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_response_finish_reason_is_valid(self, registered_xai_client):\n        response = registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        valid_reasons = {\n            \"stop\",\n            \"length\",\n            \"content_filter\",\n            \"tool_calls\",\n            \"function_call\",\n        }\n        assert response.choices[0].finish_reason in valid_reasons\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_response_contains_usage_metadata(\n        self, registered_async_xai_client\n    ):\n        response = await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response.usage is not None\n        assert response.usage.prompt_tokens > 0\n        assert response.usage.completion_tokens > 0\n\n\nclass TestMemoriIntegration:\n    @requires_xai\n    @pytest.mark.integration\n    def test_memori_wrapper_does_not_modify_response_type(\n        self, xai_api_key, memori_instance\n    ):\n        unwrapped_client = OpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n\n        wrapped_client = OpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n        memori_instance.llm.register(wrapped_client)\n        memori_instance.attribution(entity_id=\"test\", process_id=\"test\")\n\n        unwrapped_response = unwrapped_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        wrapped_response = wrapped_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert type(unwrapped_response) is type(wrapped_response)\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_config_captures_provider_info(self, memori_instance, xai_api_key):\n        client = OpenAI(api_key=xai_api_key, base_url=XAI_BASE_URL)\n        memori_instance.llm.register(client)\n\n        assert memori_instance.config.llm.provider_sdk_version is not None\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_attribution_is_preserved_across_calls(\n        self, registered_xai_client, memori_instance\n    ):\n        memori_instance.attribution(entity_id=\"user-123\", process_id=\"process-456\")\n\n        registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n        registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert memori_instance.config.entity_id == \"user-123\"\n        assert memori_instance.config.process_id == \"process-456\"\n\n\nclass TestStorageVerification:\n    @requires_xai\n    @pytest.mark.integration\n    def test_conversation_stored_after_sync_call(\n        self, registered_xai_client, memori_instance\n    ):\n        registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n        assert conversation[\"id\"] == conversation_id\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_messages_stored_with_content(self, registered_xai_client, memori_instance):\n        test_query = \"What is 2 + 2?\"\n\n        registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": test_query}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        messages = memori_instance.config.storage.driver.conversation.messages.read(\n            conversation_id\n        )\n\n        assert len(messages) >= 2\n\n        user_messages = [m for m in messages if m[\"role\"] == \"user\"]\n        assert len(user_messages) >= 1\n        assert test_query in user_messages[0][\"content\"]\n\n        assistant_messages = [m for m in messages if m[\"role\"] == \"assistant\"]\n        assert len(assistant_messages) >= 1\n        assert len(assistant_messages[0][\"content\"]) > 0\n\n    @requires_xai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_conversation_stored_after_async_call(\n        self, registered_async_xai_client, memori_instance\n    ):\n        await registered_async_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        assert conversation_id is not None\n\n        conversation = memori_instance.config.storage.driver.conversation.read(\n            conversation_id\n        )\n        assert conversation is not None\n\n    @requires_xai\n    @pytest.mark.integration\n    def test_multiple_calls_accumulate_messages(\n        self, registered_xai_client, memori_instance\n    ):\n        registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"First question\"}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        conversation_id = memori_instance.config.cache.conversation_id\n        messages_after_first = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_first = len(messages_after_first)\n\n        registered_xai_client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": \"Second question\"}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        messages_after_second = (\n            memori_instance.config.storage.driver.conversation.messages.read(\n                conversation_id\n            )\n        )\n        count_after_second = len(messages_after_second)\n\n        assert count_after_second > count_after_first\n"
  },
  {
    "path": "tests/integration/test_aa_payload.py",
    "content": "import asyncio\nimport os\nimport time\nfrom typing import cast\n\nimport pytest\n\nfrom tests.integration.conftest import requires_openai\n\nos.environ.setdefault(\"MEMORI_TEST_MODE\", \"1\")\n\nMODEL = \"gpt-4o-mini\"\nMAX_TOKENS = 50\nTEST_PROMPT = \"Say 'hello' in one word.\"\n\n\nclass TestSyncAAIntegration:\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_call_triggers_aa_pipeline(self, memori_instance, openai_api_key):\n        from openai import OpenAI\n\n        mem = memori_instance\n\n        client = OpenAI(api_key=openai_api_key)\n        mem.llm.register(client)\n        mem.attribution(entity_id=\"test-user\", process_id=\"test-process\")\n\n        response = client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n        mem.config.augmentation.wait(timeout=5.0)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_sync_streaming_triggers_aa_pipeline(self, memori_instance, openai_api_key):\n        from openai import OpenAI\n\n        mem = memori_instance\n\n        client = OpenAI(api_key=openai_api_key)\n        mem.llm.register(client)\n        mem.attribution(entity_id=\"stream-user\", process_id=\"stream-process\")\n\n        stream = client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        chunks = []\n        for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                chunks.append(chunk.choices[0].delta.content)\n\n        assert len(chunks) > 0\n\n        mem.config.augmentation.wait(timeout=5.0)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_multi_turn_conversation_triggers_aa(self, memori_instance, openai_api_key):\n        from openai import OpenAI\n\n        mem = memori_instance\n\n        client = OpenAI(api_key=openai_api_key)\n        mem.llm.register(client)\n        mem.attribution(entity_id=\"multi-turn-user\", process_id=\"multi-turn-proc\")\n\n        from openai.types.chat import ChatCompletionMessageParam\n\n        messages = cast(\n            list[ChatCompletionMessageParam],\n            [\n                {\"role\": \"system\", \"content\": \"You are helpful.\"},\n                {\"role\": \"user\", \"content\": \"My name is Alice.\"},\n                {\"role\": \"assistant\", \"content\": \"Nice to meet you, Alice!\"},\n                {\"role\": \"user\", \"content\": \"What is my name?\"},\n            ],\n        )\n\n        response = client.chat.completions.create(\n            model=MODEL,\n            messages=messages,\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        mem.config.augmentation.wait(timeout=5.0)\n\n\nclass TestAsyncAAIntegration:\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_call_triggers_aa_pipeline(\n        self, memori_instance, openai_api_key\n    ):\n        from openai import AsyncOpenAI\n\n        mem = memori_instance\n\n        client = AsyncOpenAI(api_key=openai_api_key)\n        mem.llm.register(client)\n        mem.attribution(entity_id=\"async-user\", process_id=\"async-process\")\n\n        response = await client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        assert response.choices[0].message.content is not None\n\n        await asyncio.sleep(0.5)\n        mem.config.augmentation.wait(timeout=5.0)\n\n    @requires_openai\n    @pytest.mark.integration\n    @pytest.mark.asyncio\n    async def test_async_streaming_triggers_aa_pipeline(\n        self, memori_instance, openai_api_key\n    ):\n        from openai import AsyncOpenAI\n\n        mem = memori_instance\n\n        client = AsyncOpenAI(api_key=openai_api_key)\n        mem.llm.register(client)\n        mem.attribution(entity_id=\"async-stream-user\", process_id=\"async-stream-proc\")\n\n        stream = await client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n            stream=True,\n        )\n\n        chunks = []\n        async for chunk in stream:\n            if chunk.choices and chunk.choices[0].delta.content:\n                chunks.append(chunk.choices[0].delta.content)\n\n        assert len(chunks) > 0\n\n        await asyncio.sleep(0.5)\n        mem.config.augmentation.wait(timeout=5.0)\n\n\nclass TestAAEdgeCases:\n    @requires_openai\n    @pytest.mark.integration\n    def test_no_aa_without_attribution(self, memori_instance, openai_api_key):\n        from openai import OpenAI\n\n        mem = memori_instance\n\n        client = OpenAI(api_key=openai_api_key)\n        mem.llm.register(client)\n\n        response = client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        time.sleep(0.5)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_aa_with_entity_only(self, memori_instance, openai_api_key):\n        from openai import OpenAI\n\n        mem = memori_instance\n\n        client = OpenAI(api_key=openai_api_key)\n        mem.llm.register(client)\n        mem.attribution(entity_id=\"entity-only-user\")\n\n        response = client.chat.completions.create(\n            model=MODEL,\n            messages=[{\"role\": \"user\", \"content\": TEST_PROMPT}],\n            max_tokens=MAX_TOKENS,\n        )\n\n        assert response is not None\n        mem.config.augmentation.wait(timeout=5.0)\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_multiple_calls_same_session(self, memori_instance, openai_api_key):\n        from openai import OpenAI\n\n        mem = memori_instance\n\n        client = OpenAI(api_key=openai_api_key)\n        mem.llm.register(client)\n        mem.attribution(entity_id=\"multi-call-user\", process_id=\"multi-call-proc\")\n\n        for i in range(3):\n            response = client.chat.completions.create(\n                model=MODEL,\n                messages=[{\"role\": \"user\", \"content\": f\"Say the number {i}\"}],\n                max_tokens=MAX_TOKENS,\n            )\n            assert response is not None\n\n        mem.config.augmentation.wait(timeout=10.0)\n\n\nclass TestTestModeConfiguration:\n    def test_memori_test_mode_is_enabled(self):\n        assert os.environ.get(\"MEMORI_TEST_MODE\") == \"1\"\n\n    @requires_openai\n    @pytest.mark.integration\n    def test_memori_instance_in_test_mode(self, memori_instance):\n        assert os.environ.get(\"MEMORI_TEST_MODE\") is not None\n        assert memori_instance is not None\n        assert memori_instance.config is not None\n"
  },
  {
    "path": "tests/llm/adapters/anthropic/test_llm_adapters_anthropic_adapter.py",
    "content": "from memori.llm.adapters.anthropic._adapter import Adapter\n\n\ndef test_get_formatted_query():\n    assert Adapter().get_formatted_query({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"query\": {}}}) == []\n\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"messages\": [\n                        {\"content\": \"abc\", \"role\": \"user\"},\n                        {\"content\": \"def\", \"role\": \"assistant\"},\n                    ]\n                }\n            }\n        }\n    ) == [{\"content\": \"abc\", \"role\": \"user\"}, {\"content\": \"def\", \"role\": \"assistant\"}]\n\n\ndef test_get_formatted_response_unstreamed():\n    assert Adapter().get_formatted_response({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"response\": {}}}) == []\n\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"response\": {\n                    \"content\": [\n                        {\"text\": \"abc\", \"type\": \"text\"},\n                        {\"text\": \"def\", \"type\": \"text\"},\n                    ],\n                    \"role\": \"user\",\n                }\n            }\n        }\n    ) == [\n        {\"role\": \"user\", \"text\": \"abc\", \"type\": \"text\"},\n        {\"role\": \"user\", \"text\": \"def\", \"type\": \"text\"},\n    ]\n\n\ndef test_get_formatted_query_with_injected_messages():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"_memori_injected_count\": 2,\n                    \"messages\": [\n                        {\"content\": \"injected 1\", \"role\": \"user\"},\n                        {\"content\": \"injected 2\", \"role\": \"assistant\"},\n                        {\"content\": \"new message\", \"role\": \"user\"},\n                        {\"content\": \"new response\", \"role\": \"assistant\"},\n                    ],\n                }\n            }\n        }\n    ) == [\n        {\"content\": \"new message\", \"role\": \"user\"},\n        {\"content\": \"new response\", \"role\": \"assistant\"},\n    ]\n"
  },
  {
    "path": "tests/llm/adapters/bedrock/test_llm_adapters_bedrock_adapter.py",
    "content": "from memori.llm.adapters.bedrock._adapter import Adapter\n\n\ndef test_get_formatted_query():\n    assert Adapter().get_formatted_query({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"query\": {}}}) == []\n\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"body\": {\n                        \"messages\": [\n                            {\"content\": \"abc\", \"role\": \"user\"},\n                            {\"content\": \"def\", \"role\": \"system\"},\n                        ]\n                    }\n                }\n            }\n        }\n    ) == [{\"content\": \"abc\", \"role\": \"user\"}, {\"content\": \"def\", \"role\": \"system\"}]\n\n\ndef test_get_formatted_response_streamed():\n    assert Adapter().get_formatted_response({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"response\": {}}}) == []\n\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"response\": [\n                    {\"chunk\": {\"bytes\": {\"message\": {\"role\": \"assistant\"}}}},\n                    {\"chunk\": {\"bytes\": {\"delta\": {\"text\": \"abc\"}}}},\n                    {\"chunk\": {\"bytes\": {\"delta\": {\"text\": \"def\"}}}},\n                ]\n            }\n        }\n    ) == [{\"role\": \"assistant\", \"text\": \"abcdef\", \"type\": \"text\"}]\n\n\ndef test_get_formatted_query_with_injected_messages():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"_memori_injected_count\": 1,\n                    \"body\": {\n                        \"messages\": [\n                            {\"content\": \"injected 1\", \"role\": \"user\"},\n                            {\"content\": \"new message\", \"role\": \"user\"},\n                        ]\n                    },\n                }\n            }\n        }\n    ) == [{\"content\": \"new message\", \"role\": \"user\"}]\n"
  },
  {
    "path": "tests/llm/adapters/google/test_llm_adapters_google_adapter.py",
    "content": "from memori.llm.adapters.google._adapter import Adapter\n\n\ndef test_get_formatted_query():\n    assert Adapter().get_formatted_query({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"query\": {}}}) == []\n\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"contents\": [\n                        {\"parts\": [{\"text\": \"abc\"}, {\"text\": \"def\"}], \"role\": \"user\"},\n                        {\"parts\": [{\"text\": \"ghi\"}], \"role\": \"system\"},\n                    ]\n                }\n            }\n        }\n    ) == [{\"content\": \"abc def\", \"role\": \"user\"}, {\"content\": \"ghi\", \"role\": \"system\"}]\n\n\ndef test_get_formatted_response_streamed():\n    assert Adapter().get_formatted_response({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"response\": {}}}) == []\n\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"response\": [\n                    {\n                        \"candidates\": [\n                            {\n                                \"content\": {\n                                    \"parts\": [{\"text\": \"abc\"}, {\"text\": \"def\"}],\n                                    \"role\": \"model\",\n                                }\n                            },\n                        ]\n                    },\n                    {\n                        \"candidates\": [\n                            {\"content\": {\"parts\": [{\"text\": \"ghi\"}], \"role\": \"model\"}}\n                        ]\n                    },\n                ]\n            }\n        }\n    ) == [{\"role\": \"model\", \"text\": \"abcdefghi\", \"type\": \"text\"}]\n\n\ndef test_get_formatted_response_unstreamed():\n    assert Adapter().get_formatted_response({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"response\": {}}}) == []\n\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"response\": {\n                    \"candidates\": [\n                        {\"content\": {\"parts\": [{\"text\": \"abc\"}], \"role\": \"model\"}},\n                        {\"content\": {\"parts\": [{\"text\": \"def\"}], \"role\": \"model\"}},\n                    ]\n                }\n            }\n        }\n    ) == [\n        {\"role\": \"model\", \"text\": \"abc\", \"type\": \"text\"},\n        {\"role\": \"model\", \"text\": \"def\", \"type\": \"text\"},\n    ]\n\n\ndef test_get_formatted_query_with_injected_messages():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"_memori_injected_count\": 2,\n                    \"contents\": [\n                        {\"parts\": [{\"text\": \"injected 1\"}], \"role\": \"user\"},\n                        {\"parts\": [{\"text\": \"injected 2\"}], \"role\": \"model\"},\n                        {\"parts\": [{\"text\": \"new message\"}], \"role\": \"user\"},\n                    ],\n                }\n            }\n        }\n    ) == [{\"content\": \"new message\", \"role\": \"user\"}]\n"
  },
  {
    "path": "tests/llm/adapters/openai/test_llm_adapters_openai_adapter.py",
    "content": "from unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.llm._base import BaseInvoke\nfrom memori.llm._invoke import Invoke, InvokeAsync\nfrom memori.llm._iterator import AsyncIterator, Iterator\nfrom memori.llm.adapters.openai._adapter import Adapter\n\n\nclass MockEvent:\n    def __init__(self, event_type: str, response=None):\n        self.type = event_type\n        if response is not None:\n            self.response = response\n\n\nclass MockResponse:\n    def __init__(self, output_text: str):\n        self.output_text = output_text\n        self.output = [\n            {\n                \"type\": \"message\",\n                \"content\": [{\"type\": \"output_text\", \"text\": output_text}],\n            }\n        ]\n\n    def model_dump(self):\n        return {\"output_text\": self.output_text, \"output\": self.output}\n\n\nclass MockResponsesResponse:\n    def __init__(self):\n        self.output = []\n        self.output_text = \"Test response\"\n\n    def model_dump(self):\n        return {\"output\": self.output, \"output_text\": self.output_text}\n\n\ndef test_get_formatted_query():\n    assert Adapter().get_formatted_query({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"query\": {}}}) == []\n\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"messages\": [\n                        {\"content\": \"abc\", \"role\": \"user\"},\n                        {\"content\": \"def\", \"role\": \"assistant\"},\n                    ]\n                }\n            }\n        }\n    ) == [{\"content\": \"abc\", \"role\": \"user\"}, {\"content\": \"def\", \"role\": \"assistant\"}]\n\n\ndef test_get_formatted_response_streamed():\n    assert Adapter().get_formatted_response({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"response\": {}}}) == []\n\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"query\": {\"stream\": True},\n                \"response\": {\n                    \"choices\": [\n                        {\n                            \"delta\": {\n                                \"content\": \"abc\",\n                                \"role\": \"assistant\",\n                            }\n                        },\n                        {\n                            \"delta\": {\n                                \"content\": \"def\",\n                                \"role\": \"assistant\",\n                            }\n                        },\n                    ]\n                },\n            }\n        }\n    ) == [{\"role\": \"assistant\", \"text\": \"abcdef\", \"type\": \"text\"}]\n\n\ndef test_get_formatted_response_unstreamed():\n    assert Adapter().get_formatted_response({}) == []\n    assert Adapter().get_formatted_query({\"conversation\": {\"response\": {}}}) == []\n\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"query\": {},\n                \"response\": {\n                    \"choices\": [\n                        {\"message\": {\"content\": \"abc\", \"role\": \"assistant\"}},\n                        {\"message\": {\"content\": \"def\", \"role\": \"assistant\"}},\n                    ]\n                },\n            }\n        }\n    ) == [\n        {\"role\": \"assistant\", \"text\": \"abc\", \"type\": \"text\"},\n        {\"role\": \"assistant\", \"text\": \"def\", \"type\": \"text\"},\n    ]\n\n\ndef test_get_formatted_query_with_injected_messages():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"_memori_injected_count\": 2,\n                    \"messages\": [\n                        {\"content\": \"injected 1\", \"role\": \"user\"},\n                        {\"content\": \"injected 2\", \"role\": \"assistant\"},\n                        {\"content\": \"new message\", \"role\": \"user\"},\n                        {\"content\": \"new response\", \"role\": \"assistant\"},\n                    ],\n                }\n            }\n        }\n    ) == [\n        {\"content\": \"new message\", \"role\": \"user\"},\n        {\"content\": \"new response\", \"role\": \"assistant\"},\n    ]\n\n\ndef test_responses_get_formatted_query_string_input():\n    payload = {\n        \"conversation\": {\n            \"query\": {\n                \"input\": \"Hello, how are you?\",\n                \"instructions\": \"You are a helpful assistant.\",\n            }\n        }\n    }\n    result = Adapter().get_formatted_query(payload)\n    assert len(result) == 2\n    assert result[0] == {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}\n    assert result[1] == {\"role\": \"user\", \"content\": \"Hello, how are you?\"}\n\n\ndef test_responses_get_formatted_query_list_input():\n    payload = {\n        \"conversation\": {\n            \"query\": {\n                \"input\": [\n                    {\"role\": \"user\", \"content\": \"First message\"},\n                    {\"role\": \"assistant\", \"content\": \"First response\"},\n                    {\"role\": \"user\", \"content\": \"Second message\"},\n                ]\n            }\n        }\n    }\n    result = Adapter().get_formatted_query(payload)\n    assert len(result) == 3\n    assert result[0] == {\"role\": \"user\", \"content\": \"First message\"}\n\n\ndef test_responses_get_formatted_query_strips_memori_context():\n    payload = {\n        \"conversation\": {\n            \"query\": {\n                \"input\": \"Hello\",\n                \"instructions\": \"Be helpful.\\n\\n<memori_context>\\nUser likes cats.\\n</memori_context>\",\n            }\n        }\n    }\n    result = Adapter().get_formatted_query(payload)\n    assert len(result) == 2\n    assert result[0][\"content\"] == \"Be helpful.\"\n\n\ndef test_responses_get_formatted_query_with_injected_messages():\n    payload = {\n        \"conversation\": {\n            \"query\": {\n                \"_memori_injected_count\": 2,\n                \"input\": [\n                    {\"role\": \"user\", \"content\": \"Injected 1\"},\n                    {\"role\": \"assistant\", \"content\": \"Injected 2\"},\n                    {\"role\": \"user\", \"content\": \"Actual query\"},\n                ],\n            }\n        }\n    }\n    result = Adapter().get_formatted_query(payload)\n    assert len(result) == 1\n    assert result[0] == {\"role\": \"user\", \"content\": \"Actual query\"}\n\n\ndef test_responses_get_formatted_response_with_output_message():\n    payload = {\n        \"conversation\": {\n            \"query\": {},\n            \"response\": {\n                \"output\": [\n                    {\n                        \"type\": \"message\",\n                        \"content\": [{\"type\": \"output_text\", \"text\": \"Hello!\"}],\n                    }\n                ]\n            },\n        }\n    }\n    result = Adapter().get_formatted_response(payload)\n    assert len(result) == 1\n    assert result[0] == {\"role\": \"assistant\", \"text\": \"Hello!\", \"type\": \"text\"}\n\n\ndef test_responses_get_formatted_response_fallback_to_output_text():\n    payload = {\n        \"conversation\": {\n            \"query\": {},\n            \"response\": {\"output\": [], \"output_text\": \"Fallback text\"},\n        }\n    }\n    result = Adapter().get_formatted_response(payload)\n    assert len(result) == 1\n    assert result[0] == {\"role\": \"assistant\", \"text\": \"Fallback text\", \"type\": \"text\"}\n\n\ndef test_iterator_iter_returns_self():\n    config = Config()\n    iterator = Iterator(config, iter([]))\n    assert iterator.__iter__() is iterator\n\n\n@patch(\"memori.llm._iterator.MemoryManager\")\ndef test_iterator_yields_all_events(mock_memory_manager):\n    config = Config()\n    events = [\n        MockEvent(\"response.created\"),\n        MockEvent(\"response.completed\", MockResponse(\"Hello\")),\n    ]\n    iterator = Iterator(config, iter(events))\n\n    mock_invoke = MagicMock()\n    mock_invoke._uses_protobuf = False\n    mock_invoke._format_payload.return_value = {}\n    mock_invoke._format_kwargs.return_value = {}\n    mock_invoke._format_response.return_value = {}\n    iterator.configure_invoke(mock_invoke)\n    iterator.configure_request({\"input\": \"test\"}, 0)\n\n    collected = list(iterator)\n    assert len(collected) == 2\n\n\n@patch(\"memori.llm._iterator.MemoryManager\")\ndef test_iterator_captures_response_on_completed_event(mock_memory_manager):\n    config = Config()\n    mock_response = MockResponse(\"Test output\")\n    events = [MockEvent(\"response.completed\", mock_response)]\n    iterator = Iterator(config, iter(events))\n\n    mock_invoke = MagicMock()\n    mock_invoke._uses_protobuf = False\n    mock_invoke._format_payload.return_value = {}\n    mock_invoke._format_kwargs.return_value = {}\n    mock_invoke._format_response.return_value = {}\n    iterator.configure_invoke(mock_invoke)\n    iterator.configure_request({\"input\": \"test\"}, 0)\n\n    list(iterator)\n    assert iterator.raw_response == mock_response.model_dump()\n\n\ndef test_async_iterator_aiter_returns_self():\n    config = Config()\n    mock_source = MagicMock()\n    mock_source.__aiter__.return_value = mock_source\n    iterator = AsyncIterator(config, mock_source)\n    assert iterator.__aiter__() is iterator\n\n\n@pytest.mark.asyncio\n@patch(\"memori.llm._iterator.MemoryManager\")\nasync def test_async_iterator_yields_all_events(mock_memory_manager):\n    config = Config()\n    events = [\n        MockEvent(\"response.created\"),\n        MockEvent(\"response.completed\", MockResponse(\"Hello\")),\n    ]\n\n    async def async_gen():\n        for event in events:\n            yield event\n\n    iterator = AsyncIterator(config, async_gen())\n\n    mock_invoke = MagicMock()\n    mock_invoke._uses_protobuf = False\n    mock_invoke._format_payload.return_value = {}\n    mock_invoke._format_kwargs.return_value = {}\n    mock_invoke._format_response.return_value = {}\n    iterator.configure_invoke(mock_invoke)\n    iterator.configure_request({\"input\": \"test\"}, 0)\n    iterator.__aiter__()\n\n    collected = []\n    async for event in iterator:\n        collected.append(event)\n    assert len(collected) == 2\n\n\n@pytest.mark.asyncio\nasync def test_async_iterator_raises_runtime_error_if_not_initialized():\n    config = Config()\n    iterator = AsyncIterator(config, MagicMock())\n    with pytest.raises(RuntimeError, match=\"Iterator not initialized\"):\n        await iterator.__anext__()\n\n\ndef test_extract_user_query_from_string_input():\n    config = Config()\n    invoke = BaseInvoke(config, lambda **kwargs: None)\n    assert invoke._extract_user_query({\"input\": \"What is 2+2?\"}) == \"What is 2+2?\"\n\n\ndef test_extract_user_query_from_list_input():\n    config = Config()\n    invoke = BaseInvoke(config, lambda **kwargs: None)\n    kwargs = {\n        \"input\": [\n            {\"role\": \"user\", \"content\": \"First\"},\n            {\"role\": \"user\", \"content\": \"Second\"},\n        ]\n    }\n    assert invoke._extract_user_query(kwargs) == \"Second\"\n\n\ndef test_extract_user_query_from_missing_input():\n    config = Config()\n    invoke = BaseInvoke(config, lambda **kwargs: None)\n    assert invoke._extract_user_query({}) == \"\"\n\n\ndef test_inject_recalled_facts_returns_kwargs_when_no_storage():\n    config = Config()\n    config.storage = None\n    invoke = BaseInvoke(config, lambda **kwargs: None)\n    kwargs = {\"input\": \"test\", \"instructions\": \"Be helpful\"}\n    assert invoke.inject_recalled_facts(kwargs) == kwargs\n\n\ndef test_inject_recalled_facts_appends_facts_to_instructions():\n    config = Config()\n    config.storage = MagicMock()\n    config.storage.driver = MagicMock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    config.recall_relevance_threshold = 0.1\n    config.llm.provider = \"openai_responses\"\n\n    invoke = BaseInvoke(config, lambda **kwargs: None)\n    invoke.set_client(None, \"openai_responses\", \"1.0.0\")\n\n    mock_facts = [{\"content\": \"User likes Python\", \"similarity\": 0.8}]\n\n    with patch(\"memori.memory.recall.Recall\") as MockRecall:\n        MockRecall.return_value.search_facts.return_value = mock_facts\n        kwargs = {\"input\": \"Test\", \"instructions\": \"Be helpful.\"}\n        result = invoke.inject_recalled_facts(kwargs)\n        assert \"<memori_context>\" in result[\"instructions\"]\n        assert \"User likes Python\" in result[\"instructions\"]\n\n\ndef test_inject_conversation_messages_returns_kwargs_when_no_conversation_id():\n    config = Config()\n    config.cache.conversation_id = None\n    invoke = BaseInvoke(config, lambda **kwargs: None)\n    kwargs = {\"input\": \"test\"}\n    assert invoke.inject_conversation_messages(kwargs) == kwargs\n\n\ndef test_inject_conversation_messages_converts_string_input_to_list():\n    config = Config()\n    config.cache.conversation_id = 1\n    config.storage = MagicMock()\n    config.storage.driver = MagicMock()\n    config.storage.driver.conversation.messages.read.return_value = [\n        {\"role\": \"user\", \"content\": \"Previous\"},\n    ]\n    config.llm.provider = \"openai_responses\"\n\n    invoke = BaseInvoke(config, lambda **kwargs: None)\n    invoke.set_client(None, \"openai_responses\", \"1.0.0\")\n\n    result = invoke.inject_conversation_messages({\"input\": \"New\"})\n    assert isinstance(result[\"input\"], list)\n    assert len(result[\"input\"]) == 2\n\n\ndef test_invoke_calls_method():\n    config = Config()\n    config.storage = None\n\n    mock_response = MockResponsesResponse()\n    mock_method = MagicMock(return_value=mock_response)\n\n    invoke = Invoke(config, mock_method)\n    invoke.set_client(None, \"openai_responses\", \"1.0.0\")\n\n    result = invoke.invoke(model=\"gpt-4o\", input=\"test\")\n    mock_method.assert_called_once()\n    assert result == mock_response\n\n\n@pytest.mark.asyncio\nasync def test_async_invoke_calls_method():\n    config = Config()\n    config.storage = None\n\n    mock_response = MockResponsesResponse()\n\n    async def mock_method(**kwargs):\n        return mock_response\n\n    invoke = InvokeAsync(config, mock_method)\n    invoke.set_client(None, \"openai_responses\", \"1.0.0\")\n\n    result = await invoke.invoke(model=\"gpt-4o\", input=\"test\")\n    assert result == mock_response\n"
  },
  {
    "path": "tests/llm/adapters/xai/test_llm_adapters_xai_adapter.py",
    "content": "from memori.llm.adapters.xai._adapter import Adapter\n\n\ndef test_get_formatted_query_empty_payload():\n    assert Adapter().get_formatted_query({}) == []\n\n\ndef test_get_formatted_query_missing_messages():\n    assert Adapter().get_formatted_query({\"conversation\": {\"query\": {}}}) == []\n\n\ndef test_get_formatted_query_with_user_message():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"messages\": [\n                        {\n                            \"role\": \"ROLE_USER\",\n                            \"content\": [{\"text\": \"Hello\"}],\n                        }\n                    ]\n                }\n            }\n        }\n    ) == [{\"role\": \"user\", \"content\": \"Hello\"}]\n\n\ndef test_get_formatted_query_with_assistant_message():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"messages\": [\n                        {\n                            \"role\": \"ROLE_ASSISTANT\",\n                            \"content\": [{\"text\": \"Hi there\"}],\n                        }\n                    ]\n                }\n            }\n        }\n    ) == [{\"role\": \"assistant\", \"content\": \"Hi there\"}]\n\n\ndef test_get_formatted_query_with_system_message():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"messages\": [\n                        {\n                            \"role\": \"ROLE_SYSTEM\",\n                            \"content\": [{\"text\": \"You are helpful\"}],\n                        }\n                    ]\n                }\n            }\n        }\n    ) == [{\"role\": \"system\", \"content\": \"You are helpful\"}]\n\n\ndef test_get_formatted_query_with_multiple_messages():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"messages\": [\n                        {\n                            \"role\": \"ROLE_USER\",\n                            \"content\": [{\"text\": \"Hello\"}],\n                        },\n                        {\n                            \"role\": \"ROLE_ASSISTANT\",\n                            \"content\": [{\"text\": \"Hi\"}],\n                        },\n                        {\n                            \"role\": \"ROLE_USER\",\n                            \"content\": [{\"text\": \"How are you?\"}],\n                        },\n                    ]\n                }\n            }\n        }\n    ) == [\n        {\"role\": \"user\", \"content\": \"Hello\"},\n        {\"role\": \"assistant\", \"content\": \"Hi\"},\n        {\"role\": \"user\", \"content\": \"How are you?\"},\n    ]\n\n\ndef test_get_formatted_query_with_multiple_content_parts():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"messages\": [\n                        {\n                            \"role\": \"ROLE_USER\",\n                            \"content\": [\n                                {\"text\": \"Hello\"},\n                                {\"text\": \"world\"},\n                                {\"text\": \"!\"},\n                            ],\n                        }\n                    ]\n                }\n            }\n        }\n    ) == [{\"role\": \"user\", \"content\": \"Hello world !\"}]\n\n\ndef test_get_formatted_query_with_missing_role():\n    assert (\n        Adapter().get_formatted_query(\n            {\n                \"conversation\": {\n                    \"query\": {\n                        \"messages\": [\n                            {\n                                \"content\": [{\"text\": \"Hello\"}],\n                            }\n                        ]\n                    }\n                }\n            }\n        )\n        == []\n    )\n\n\ndef test_get_formatted_query_with_unknown_role():\n    assert (\n        Adapter().get_formatted_query(\n            {\n                \"conversation\": {\n                    \"query\": {\n                        \"messages\": [\n                            {\n                                \"role\": \"ROLE_UNKNOWN\",\n                                \"content\": [{\"text\": \"Hello\"}],\n                            }\n                        ]\n                    }\n                }\n            }\n        )\n        == []\n    )\n\n\ndef test_get_formatted_query_with_empty_content():\n    assert (\n        Adapter().get_formatted_query(\n            {\n                \"conversation\": {\n                    \"query\": {\n                        \"messages\": [\n                            {\n                                \"role\": \"ROLE_USER\",\n                                \"content\": [],\n                            }\n                        ]\n                    }\n                }\n            }\n        )\n        == []\n    )\n\n\ndef test_get_formatted_query_with_content_without_text():\n    assert (\n        Adapter().get_formatted_query(\n            {\n                \"conversation\": {\n                    \"query\": {\n                        \"messages\": [\n                            {\n                                \"role\": \"ROLE_USER\",\n                                \"content\": [{\"image\": \"data\"}],\n                            }\n                        ]\n                    }\n                }\n            }\n        )\n        == []\n    )\n\n\ndef test_get_formatted_query_with_injected_messages():\n    assert Adapter().get_formatted_query(\n        {\n            \"conversation\": {\n                \"query\": {\n                    \"_memori_injected_count\": 2,\n                    \"messages\": [\n                        {\n                            \"role\": \"ROLE_USER\",\n                            \"content\": [{\"text\": \"injected 1\"}],\n                        },\n                        {\n                            \"role\": \"ROLE_ASSISTANT\",\n                            \"content\": [{\"text\": \"injected 2\"}],\n                        },\n                        {\n                            \"role\": \"ROLE_USER\",\n                            \"content\": [{\"text\": \"new message\"}],\n                        },\n                        {\n                            \"role\": \"ROLE_ASSISTANT\",\n                            \"content\": [{\"text\": \"new response\"}],\n                        },\n                    ],\n                }\n            }\n        }\n    ) == [\n        {\"role\": \"user\", \"content\": \"new message\"},\n        {\"role\": \"assistant\", \"content\": \"new response\"},\n    ]\n\n\ndef test_get_formatted_response_empty_payload():\n    assert Adapter().get_formatted_response({}) == []\n\n\ndef test_get_formatted_response_missing_response():\n    assert Adapter().get_formatted_response({\"conversation\": {\"response\": {}}}) == []\n\n\ndef test_get_formatted_response_with_string_content():\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"response\": {\n                    \"role\": \"assistant\",\n                    \"content\": \"Hello world\",\n                }\n            }\n        }\n    ) == [{\"role\": \"assistant\", \"text\": \"Hello world\", \"type\": \"text\"}]\n\n\ndef test_get_formatted_response_with_list_of_dicts():\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"response\": {\n                    \"role\": \"assistant\",\n                    \"content\": [\n                        {\"text\": \"Hello\"},\n                        {\"text\": \"world\"},\n                    ],\n                }\n            }\n        }\n    ) == [\n        {\"role\": \"assistant\", \"text\": \"Hello\", \"type\": \"text\"},\n        {\"role\": \"assistant\", \"text\": \"world\", \"type\": \"text\"},\n    ]\n\n\ndef test_get_formatted_response_with_list_of_strings():\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"response\": {\n                    \"role\": \"assistant\",\n                    \"content\": [\"Hello\", \"world\"],\n                }\n            }\n        }\n    ) == [\n        {\"role\": \"assistant\", \"text\": \"Hello\", \"type\": \"text\"},\n        {\"role\": \"assistant\", \"text\": \"world\", \"type\": \"text\"},\n    ]\n\n\ndef test_get_formatted_response_with_mixed_list():\n    assert Adapter().get_formatted_response(\n        {\n            \"conversation\": {\n                \"response\": {\n                    \"role\": \"assistant\",\n                    \"content\": [\n                        {\"text\": \"Hello\"},\n                        \"world\",\n                        {\"text\": \"!\"},\n                    ],\n                }\n            }\n        }\n    ) == [\n        {\"role\": \"assistant\", \"text\": \"Hello\", \"type\": \"text\"},\n        {\"role\": \"assistant\", \"text\": \"world\", \"type\": \"text\"},\n        {\"role\": \"assistant\", \"text\": \"!\", \"type\": \"text\"},\n    ]\n\n\ndef test_get_formatted_response_with_missing_role():\n    assert (\n        Adapter().get_formatted_response(\n            {\n                \"conversation\": {\n                    \"response\": {\n                        \"content\": \"Hello world\",\n                    }\n                }\n            }\n        )\n        == []\n    )\n\n\ndef test_get_formatted_response_with_missing_content():\n    assert (\n        Adapter().get_formatted_response(\n            {\n                \"conversation\": {\n                    \"response\": {\n                        \"role\": \"assistant\",\n                    }\n                }\n            }\n        )\n        == []\n    )\n\n\ndef test_get_formatted_response_with_dict_without_text():\n    assert (\n        Adapter().get_formatted_response(\n            {\n                \"conversation\": {\n                    \"response\": {\n                        \"role\": \"assistant\",\n                        \"content\": [{\"image\": \"data\"}],\n                    }\n                }\n            }\n        )\n        == []\n    )\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/anthropic_async.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.anthropic import Claude\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"ANTHROPIC_API_KEY\", None) is None:\n    raise RuntimeError(\"ANTHROPIC_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    model = Claude(id=\"claude-3-5-haiku-20241022\")\n\n    mem = Memori(conn=session).llm.register(claude=model)\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    agent = Agent(\n        model=model,\n        instructions=[\"Be helpful and concise\"],\n        markdown=True,\n    )\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    session_id = \"test-anthropic-async-session\"\n    response = await agent.arun(query, session_id=session_id)\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = await agent.arun(query, session_id=session_id)\n\n    print(\"-\" * 25)\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/anthropic_sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.anthropic import Claude\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"ANTHROPIC_API_KEY\", None) is None:\n    raise RuntimeError(\"ANTHROPIC_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nmodel = Claude(id=\"claude-sonnet-4-20250514\")\n\nmem = Memori(conn=session).llm.register(claude=model)\n\nmem.llm.register(claude=model)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nresponse = agent.run(query)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = agent.run(query)\n\nprint(\"-\" * 25)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/gemini_async.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.google import Gemini\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    model = Gemini(id=\"gemini-2.0-flash\")\n\n    mem = Memori(conn=session).llm.register(gemini=model)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    agent = Agent(\n        model=model,\n        instructions=[\"Be helpful and concise\"],\n        markdown=True,\n    )\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    session_id = \"test-gemini-async-session\"\n    response = await agent.arun(query, session_id=session_id)\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = await agent.arun(query, session_id=session_id)\n\n    print(\"-\" * 25)\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/gemini_streaming.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.google import Gemini\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nmodel = Gemini(id=\"gemini-2.0-flash\")\n\nmem = Memori(conn=session).llm.register(gemini=model)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nsession_id = \"test-gemini-streaming-session\"\n\nstream = agent.run(query, session_id=session_id, stream=True)\n\nprint(\"llm: \", end=\"\", flush=True)\n\nfor chunk in stream:\n    if hasattr(chunk, \"content\") and chunk.content:\n        print(chunk.content, end=\"\", flush=True)\n\nprint()\nprint()\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nprint(\"llm: \", end=\"\", flush=True)\n\nfor chunk in agent.run(query, session_id=session_id, stream=True):\n    if hasattr(chunk, \"content\") and chunk.content:\n        print(chunk.content, end=\"\", flush=True)\n\nprint(\"\\n\")\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/gemini_sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.google import Gemini\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nmodel = Gemini(id=\"gemini-2.0-flash-exp\")\n\nmem = Memori(conn=session).llm.register(gemini=model)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nsession_id = \"test-gemini-session\"\nresponse = agent.run(query, session_id=session_id)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = agent.run(query, session_id=session_id)\n\nprint(\"-\" * 25)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/openai_async.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    model = OpenAIChat(id=\"gpt-4o-mini\")\n\n    mem = Memori(conn=session).llm.register(openai_chat=model)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    agent = Agent(\n        model=model,\n        instructions=[\"Be helpful and concise\"],\n        markdown=True,\n    )\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    session_id = \"test-openai-async-session\"\n    response = await agent.arun(query, session_id=session_id)\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = await agent.arun(query, session_id=session_id)\n\n    print(\"-\" * 25)\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/openai_streaming.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori(conn=session).llm.register(openai_chat=model)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nsession_id = \"test-openai-streaming-session\"\n\nstream = agent.run(query, session_id=session_id, stream=True)\n\nprint(\"llm: \", end=\"\", flush=True)\n\nfor chunk in stream:\n    if hasattr(chunk, \"content\") and chunk.content:\n        print(chunk.content, end=\"\", flush=True)\n\nprint()\nprint()\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nprint(\"llm: \", end=\"\", flush=True)\n\nfor chunk in agent.run(query, session_id=session_id, stream=True):\n    if hasattr(chunk, \"content\") and chunk.content:\n        print(chunk.content, end=\"\", flush=True)\n\nprint(\"\\n\")\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/openai_sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nmodel = OpenAIChat(id=\"gpt-4o-mini\")\n\nmem = Memori(conn=session).llm.register(openai_chat=model)\n\nmem.llm.register(openai_chat=model)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nresponse = agent.run(query)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = agent.run(query)\n\nprint(\"-\" * 25)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/xai_async.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.xai import xAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"XAI_API_KEY\", None) is None:\n    raise RuntimeError(\"XAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    model = xAI(id=\"grok-3\")\n\n    mem = Memori(conn=session).llm.register(xai=model)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    agent = Agent(\n        model=model,\n        instructions=[\"Be helpful and concise\"],\n        markdown=True,\n    )\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    session_id = \"test-xai-async-session\"\n    response = await agent.arun(query, session_id=session_id)\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = await agent.arun(query, session_id=session_id)\n\n    print(\"-\" * 25)\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/xai_streaming.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.xai import xAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"XAI_API_KEY\", None) is None:\n    raise RuntimeError(\"XAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nmodel = xAI(id=\"grok-4\")\n\nmem = Memori(conn=session).llm.register(xai=model)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nsession_id = \"test-xai-streaming-session\"\n\nstream = agent.run(query, session_id=session_id, stream=True)\n\nprint(\"llm: \", end=\"\", flush=True)\n\nfor chunk in stream:\n    if hasattr(chunk, \"content\") and chunk.content:\n        print(chunk.content, end=\"\", flush=True)\n\nprint()\nprint()\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nprint(\"llm: \", end=\"\", flush=True)\n\nfor chunk in agent.run(query, session_id=session_id, stream=True):\n    if hasattr(chunk, \"content\") and chunk.content:\n        print(chunk.content, end=\"\", flush=True)\n\nprint(\"\\n\")\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/agno/xai_sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom agno.agent import Agent\nfrom agno.models.xai import xAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"XAI_API_KEY\", None) is None:\n    raise RuntimeError(\"XAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nmodel = xAI(id=\"grok-3\")\n\nmem = Memori(conn=session).llm.register(xai=model)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\n\nagent = Agent(\n    model=model,\n    instructions=[\"Be helpful and concise\"],\n    markdown=True,\n)\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nresponse = agent.run(query)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = agent.run(query)\n\nprint(\"-\" * 25)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/anthropic/async.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nimport anthropic\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"ANTHROPIC_API_KEY\", None) is None:\n    raise RuntimeError(\"ANTHROPIC_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def run():\n    session = TestDBSession\n    client = anthropic.AsyncAnthropic()\n\n    mem = Memori(conn=session).llm.register(client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    response = await client.messages.create(\n        model=\"claude-sonnet-4-20250514\",\n        max_tokens=1024,\n        messages=[{\"role\": \"user\", \"content\": query}],\n    )\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response.content[0].text}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = await client.messages.create(\n        model=\"claude-sonnet-4-20250514\",\n        max_tokens=1024,\n        messages=[{\"role\": \"user\", \"content\": query}],\n    )\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response.content[0].text}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n"
  },
  {
    "path": "tests/llm/clients/oss/anthropic/sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nimport anthropic\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"ANTHROPIC_API_KEY\", None) is None:\n    raise RuntimeError(\"ANTHROPIC_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nclient = anthropic.Anthropic()\n\nmem = Memori(conn=session).llm.register(client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nresponse = client.messages.create(\n    model=\"claude-sonnet-4-20250514\",\n    max_tokens=1024,\n    messages=[{\"role\": \"user\", \"content\": query}],\n)\n\nprint(\"-\" * 25)\n\nprint(f\"llm: {response.content[0].text}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = client.messages.create(\n    model=\"claude-sonnet-4-20250514\",\n    max_tokens=1024,\n    messages=[{\"role\": \"user\", \"content\": query}],\n)\n\nprint(\"-\" * 25)\n\nprint(f\"llm: {response.content[0].text}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/gemini/async.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom google import genai\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    client = genai.Client(api_key=os.environ[\"GEMINI_API_KEY\"])\n\n    mem = Memori(conn=session).llm.register(client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n    response = await client.aio.models.generate_content(\n        model=\"gemini-2.0-flash\",\n        contents=[{\"role\": \"user\", \"parts\": [{\"text\": query}]}],\n    )\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response.candidates[0].content.parts[0].text}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = await client.aio.models.generate_content(\n        model=\"gemini-2.0-flash\",\n        contents=[{\"role\": \"user\", \"parts\": [{\"text\": query}]}],\n    )\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response.candidates[0].content.parts[0].text}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/gemini/async_streaming.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom google import genai\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    client = genai.Client(api_key=os.environ[\"GEMINI_API_KEY\"])\n\n    mem = Memori(conn=session).llm.register(client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    print(\"llm: \", end=\"\", flush=True)\n    response = \"\"\n    async for chunk in await client.aio.models.generate_content_stream(\n        model=\"gemini-2.0-flash\",\n        contents=[{\"role\": \"user\", \"parts\": [{\"text\": query}]}],\n    ):\n        # Handle streaming chunks - chunks have candidates with content parts\n        if hasattr(chunk, \"candidates\") and chunk.candidates:\n            for candidate in chunk.candidates:\n                if hasattr(candidate, \"content\") and hasattr(\n                    candidate.content, \"parts\"\n                ):\n                    for part in candidate.content.parts:\n                        if hasattr(part, \"text\") and part.text:\n                            print(part.text, end=\"\", flush=True)\n                            response += part.text\n\n    print()\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = \"\"\n    async for chunk in await client.aio.models.generate_content_stream(\n        model=\"gemini-2.0-flash\",\n        contents=[{\"role\": \"user\", \"parts\": [{\"text\": query}]}],\n    ):\n        # Handle streaming chunks\n        if hasattr(chunk, \"candidates\") and chunk.candidates:\n            for candidate in chunk.candidates:\n                if hasattr(candidate, \"content\") and hasattr(\n                    candidate.content, \"parts\"\n                ):\n                    for part in candidate.content.parts:\n                        if hasattr(part, \"text\") and part.text:\n                            response += part.text\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/gemini/sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom google import genai\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nclient = genai.Client(api_key=os.environ[\"GEMINI_API_KEY\"])\n\nmem = Memori(conn=session).llm.register(client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\nresponse = client.models.generate_content(\n    model=\"gemini-2.0-flash\",\n    contents=[{\"role\": \"user\", \"parts\": [{\"text\": query}]}],\n)\n\nprint(\"-\" * 25)\n\nprint(f\"llm: {response.candidates[0].content.parts[0].text}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = client.models.generate_content(\n    model=\"gemini-2.0-flash\",\n    contents=[{\"role\": \"user\", \"parts\": [{\"text\": query}]}],\n)\n\nprint(\"-\" * 25)\n\nprint(f\"llm: {response.candidates[0].content.parts[0].text}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatbedrock/async_runnable.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom database.core import TestDBSession\nfrom langchain_aws import ChatBedrock\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\n\nfrom memori import Memori\n\nif os.environ.get(\"AWS_BEARER_TOKEN_BEDROCK\", None) is None:\n    raise RuntimeError(\"AWS_BEARER_TOKEN_BEDROCK is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession()\n    client = ChatBedrock(\n        model_id=\"anthropic.claude-3-5-sonnet-20240620-v1:0\", region_name=\"us-east-1\"\n    )\n    prompt = ChatPromptTemplate.from_messages(\n        [\n            (\"human\", \"{question}\"),\n        ]\n    )\n    chain = prompt | client | StrOutputParser()\n\n    mem = Memori(conn=session).llm.register(chatbedrock=client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(chatbedrock=client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    print(\"llm: \", end=\"\")\n    async for chunk in chain.astream({\"question\": query}):\n        print(chunk, end=\"\", flush=True)\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = \"\"\n    async for chunk in chain.astream({\"question\": query}):\n        response += chunk\n\n    print(\"-\" * 25)\n    print(f\"llm: {response}\", end=\"\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatgooglegenai/async_runnable.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_google_genai import ChatGoogleGenerativeAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    client = ChatGoogleGenerativeAI(\n        model=\"gemini-2.0-flash\", google_api_key=os.environ[\"GEMINI_API_KEY\"]\n    )\n    prompt = ChatPromptTemplate.from_messages(\n        [\n            (\"human\", \"{question}\"),\n        ]\n    )\n    chain = prompt | client | StrOutputParser()\n\n    mem = Memori(conn=session).llm.register(chatgooglegenai=client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(chatgooglegenai=client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    print(\"llm: \", end=\"\")\n    async for chunk in chain.astream({\"question\": query}):\n        print(chunk, end=\"\", flush=True)\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = \"\"\n    async for chunk in chain.astream({\"question\": query}):\n        response += chunk\n\n    print(\"-\" * 25)\n    print(f\"llm: {response}\", end=\"\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatgooglegenai/async_streaming.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_google_genai import ChatGoogleGenerativeAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    client = ChatGoogleGenerativeAI(\n        model=\"gemini-2.0-flash\", google_api_key=os.environ[\"GEMINI_API_KEY\"]\n    )\n\n    mem = Memori(conn=session).llm.register(chatgooglegenai=client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(chatgooglegenai=client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    generator = client.astream([HumanMessage(content=query)])\n    async for chunk in generator:\n        print(chunk.text, end=\"\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = \"\"\n    generator = client.astream([HumanMessage(content=query)])\n    async for chunk in generator:\n        response += chunk.text\n\n    print(\"-\" * 25)\n    print(f\"llm: {response}\", end=\"\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatgooglegenai/sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom langchain_google_genai import ChatGoogleGenerativeAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nclient = ChatGoogleGenerativeAI(\n    model=\"gemini-2.0-flash\", google_api_key=os.environ[\"GEMINI_API_KEY\"]\n)\n\nmem = Memori(conn=session).llm.register(chatgooglegenai=client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(chatgooglegenai=client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nresponse = client.invoke(query)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = client.invoke(query)\n\nprint(\"-\" * 25)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatgooglegenai/sync_runnable.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_google_genai import ChatGoogleGenerativeAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nsession = TestDBSession\nclient = ChatGoogleGenerativeAI(\n    model=\"gemini-2.0-flash\", google_api_key=os.environ[\"GEMINI_API_KEY\"]\n)\nprompt = ChatPromptTemplate.from_messages(\n    [\n        (\"human\", \"{question}\"),\n    ]\n)\nchain = prompt | client | StrOutputParser()\n\nmem = Memori(conn=session).llm.register(chatgooglegenai=client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(chatgooglegenai=client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nprint(\"llm: \", end=\"\")\nprint(chain.invoke({\"question\": query}))\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = chain.invoke({\"question\": query})\n\nprint(\"-\" * 25)\nprint(f\"llm: {response}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatgooglegenai/sync_runnable_structured_output.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_google_genai import ChatGoogleGenerativeAI\nfrom pydantic import BaseModel\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GEMINI_API_KEY\", None) is None:\n    raise RuntimeError(\"GEMINI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nclass Color(BaseModel):\n    color: str\n\n\nclass Order(BaseModel):\n    order: str\n\n\nsession = TestDBSession\nclient = ChatGoogleGenerativeAI(\n    model=\"gemini-2.0-flash\", google_api_key=os.environ[\"GEMINI_API_KEY\"]\n)\n\nmem = Memori(conn=session).llm.register(chatgooglegenai=client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(chatgooglegenai=client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars: {color}\"\nprint(f\"me: {query}\")\n\nprompt = ChatPromptTemplate.from_messages([(\"user\", query)])\nchain = prompt | client.with_structured_output(Color)\n\nprint(\"-\" * 25)\n\nprint(\"llm: \", end=\"\")\nprint(chain.invoke({\"color\": \"red\"}))\n\nprint(\"-\" * 25)\n\nquery = \"What order number is the planet we're talking about from the sun: {order}\"\nprint(f\"me: {query}\")\n\nprompt = ChatPromptTemplate.from_messages([(\"user\", query)])\nchain = prompt | client.with_structured_output(Order)\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = chain.invoke({\"order\": \"4th\"})\n\nprint(\"-\" * 25)\nprint(f\"llm: {response}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatopenai/async_ainvoke.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def run():\n    session = TestDBSession\n    client = ChatOpenAI(model=\"gpt-4.1\", streaming=True)\n    message = HumanMessage(content=\"What color is the planet Mars?\")\n\n    mem = Memori(conn=session).llm.register(chatopenai=client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(chatopenai=client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n    print(\"me: What color is the planet Mars?\")\n\n    response = await client.ainvoke([message])\n\n    print(\"-\" * 25)\n    print(f\"llm: {response}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatopenai/async_runnable.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_openai import ChatOpenAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    client = ChatOpenAI(model=\"gpt-4o\", streaming=True)\n    prompt = ChatPromptTemplate.from_messages(\n        [\n            (\"human\", \"{question}\"),\n        ]\n    )\n    chain = prompt | client | StrOutputParser()\n\n    mem = Memori(conn=session).llm.register(chatopenai=client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(chatopenai=client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    print(\"llm: \", end=\"\")\n    async for chunk in chain.astream({\"question\": query}):\n        print(chunk, end=\"\", flush=True)\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = \"\"\n    async for chunk in chain.astream({\"question\": query}):\n        response += chunk\n\n    print(\"-\" * 25)\n    print(f\"llm: {response}\", end=\"\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatopenai/async_streaming.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def main():\n    session = TestDBSession\n    client = ChatOpenAI(model=\"gpt-4.1\", streaming=True)\n\n    mem = Memori(conn=session).llm.register(chatopenai=client)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(chatopenai=client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    generator = client.astream([HumanMessage(content=query)])\n    async for chunk in generator:\n        print(chunk.text, end=\"\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = \"\"\n    generator = client.astream([HumanMessage(content=query)])\n    async for chunk in generator:\n        response += chunk.text\n\n    print(\"-\" * 25)\n    print(f\"llm: {response}\", end=\"\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatopenai/sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom langchain_community.chat_models import ChatOpenAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nclient = ChatOpenAI(model=\"gpt-4o-mini\")\n\nmem = Memori(conn=session).llm.register(chatopenai=client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(chatopenai=client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nresponse = client.invoke(query)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = client.invoke(query)\n\nprint(\"-\" * 25)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatopenai/sync_runnable.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom langchain_community.chat_models import ChatOpenAI\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nsession = TestDBSession\nclient = ChatOpenAI(model=\"gpt-4o\", streaming=True)\nprompt = ChatPromptTemplate.from_messages(\n    [\n        (\"human\", \"{question}\"),\n    ]\n)\nchain = prompt | client | StrOutputParser()\n\nmem = Memori(conn=session).llm.register(chatopenai=client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(chatopenai=client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nprint(\"llm: \", end=\"\")\nprint(chain.invoke({\"question\": query}))\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = chain.invoke({\"question\": query})\n\nprint(\"-\" * 25)\nprint(f\"llm: {response}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatopenai/sync_runnable_structured_output.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_openai import ChatOpenAI\nfrom pydantic import BaseModel\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nclass Color(BaseModel):\n    color: str\n\n\nclass Order(BaseModel):\n    order: str\n\n\nsession = TestDBSession\nclient = ChatOpenAI(model=\"gpt-4o\")\n\nmem = Memori(conn=session).llm.register(chatopenai=client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(chatopenai=client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars: {color}\"\nprint(f\"me: {query}\")\n\nprompt = ChatPromptTemplate.from_messages([(\"user\", query)])\nchain = prompt | client.with_structured_output(Color)\n\nprint(\"-\" * 25)\n\nprint(\"llm: \", end=\"\")\nprint(chain.invoke({\"color\": \"red\"}))\n\nprint(\"-\" * 25)\n\nquery = \"What order number is the planet we're talking about from the sun: {order}\"\nprint(f\"me: {query}\")\n\nprompt = ChatPromptTemplate.from_messages([(\"user\", query)])\nchain = prompt | client.with_structured_output(Order)\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = chain.invoke({\"order\": \"4th\"})\n\nprint(\"-\" * 25)\nprint(f\"llm: {response}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/langchain/chatvertexai/sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom langchain_google_vertexai import ChatVertexAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"GOOGLE_APPLICATION_CREDENTIALS\", None) is None:\n    raise RuntimeError(\"GOOGLE_APPLICATION_CREDENTIALS is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nclient = ChatVertexAI(\n    model_name=\"gemini-2.0-flash\",\n    temperature=0,\n    seed=42,\n)\n\nmem = Memori(conn=session).llm.register(chatvertexai=client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(chatvertexai=client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\n\nresponse = client.invoke(query)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = client.invoke(query)\n\nprint(\"-\" * 25)\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/openai/async.py",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport asyncio\nimport os\n\nfrom openai import AsyncOpenAI\n\nfrom memori import Memori\nfrom tests.database.core import (\n    MongoTestDBSession,\n    MySQLTestDBSession,\n    OracleTestDBSession,\n    SQLiteTestDBSession,\n    TestDBSession,\n)\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def run(db_backend: str = \"default\"):\n    # Select database session based on backend\n    if db_backend == \"mongodb\":\n        session = MongoTestDBSession\n    elif db_backend == \"mysql\":\n        session = MySQLTestDBSession\n    elif db_backend == \"oracle\":\n        session = OracleTestDBSession\n    elif db_backend == \"sqlite\":\n        session = SQLiteTestDBSession\n    else:\n        session = TestDBSession\n\n    client = AsyncOpenAI()\n\n    mem = Memori(conn=session).llm.register(client)\n\n    # Initialize database schema\n    mem.config.storage.build()\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n    response = await client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": query}],\n    )\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response.choices[0].message.content}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    response = await client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": query}],\n    )\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response.choices[0].message.content}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        description=\"Test OpenAI async client with various database backends\"\n    )\n    parser.add_argument(\n        \"--db\",\n        choices=[\"default\", \"postgres\", \"mysql\", \"oracle\", \"mongodb\", \"sqlite\"],\n        default=\"default\",\n        help=\"Database backend to use (default: uses DATABASE_URL env var)\",\n    )\n    args = parser.parse_args()\n\n    asyncio.run(run(args.db))\n"
  },
  {
    "path": "tests/llm/clients/oss/openai/async_streaming.py",
    "content": "#!/usr/bin/env python3\n\nimport asyncio\nimport os\n\nfrom openai import AsyncOpenAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def run():\n    session = TestDBSession\n    client = AsyncOpenAI()\n\n    mem = Memori(conn=session).llm.register(client, stream=True)\n\n    # Multiple registrations should not cause an issue.\n    mem.llm.register(client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n\n    response = \"\"\n    async for chunk in client.chat.completions.create(\n        model=\"gpt-4o-mini\", messages=[{\"role\": \"user\", \"content\": query}], stream=True\n    ):\n        try:\n            if chunk.choices[0].delta.content is not None:\n                response += chunk.choices[0].delta.content\n        except IndexError:\n            pass\n\n    print(f\"llm: {response}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    response = \"\"\n    async for chunk in client.chat.completions.create(\n        model=\"gpt-4o-mini\", messages=[{\"role\": \"user\", \"content\": query}], stream=True\n    ):\n        try:\n            if chunk.choices[0].delta.content is not None:\n                response += chunk.choices[0].delta.content\n        except IndexError:\n            pass\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n"
  },
  {
    "path": "tests/llm/clients/oss/openai/sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom openai import OpenAI\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"OPENAI_API_KEY\", None) is None:\n    raise RuntimeError(\"OPENAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nclient = OpenAI()\n\nmem = Memori(conn=session).llm.register(client)\n\n# Multiple registrations should not cause an issue.\nmem.llm.register(client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": query}],\n)\n\nprint(\"-\" * 25)\n\nprint(f\"llm: {response.choices[0].message.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nresponse = client.chat.completions.create(\n    model=\"gpt-4o-mini\",\n    messages=[{\"role\": \"user\", \"content\": query}],\n)\n\nprint(\"-\" * 25)\n\nprint(f\"llm: {response.choices[0].message.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/clients/oss/xai/async.py",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport asyncio\nimport os\n\nfrom xai_sdk import AsyncClient\nfrom xai_sdk.chat import user\n\nfrom memori import Memori\nfrom tests.database.core import (\n    MongoTestDBSession,\n    MySQLTestDBSession,\n    OracleTestDBSession,\n    PostgresTestDBSession,\n    SQLiteTestDBSession,\n    TestDBSession,\n)\n\nif os.environ.get(\"XAI_API_KEY\", None) is None:\n    raise RuntimeError(\"XAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def run(db_backend: str = \"default\"):\n    if db_backend == \"mongodb\":\n        session = MongoTestDBSession\n    elif db_backend == \"mysql\":\n        session = MySQLTestDBSession\n    elif db_backend == \"oracle\":\n        session = OracleTestDBSession\n    elif db_backend == \"postgres\":\n        session = PostgresTestDBSession\n    elif db_backend == \"sqlite\":\n        session = SQLiteTestDBSession\n    else:\n        session = TestDBSession\n\n    client = AsyncClient(api_key=os.environ.get(\"XAI_API_KEY\"))\n\n    mem = Memori(conn=session).llm.register(client)\n\n    mem.config.storage.build()\n\n    mem.llm.register(client)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n    chat = client.chat.create(\n        model=\"grok-4\",\n        messages=[user(query)],\n    )\n    response = await chat.sample()\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    chat = client.chat.create(\n        model=\"grok-4\",\n        messages=[user(query)],\n    )\n    response = await chat.sample()\n\n    print(\"-\" * 25)\n\n    print(f\"llm: {response.content}\")\n\n    print(\"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        description=\"Test XAI async client with various database backends\"\n    )\n    parser.add_argument(\n        \"--db\",\n        choices=[\"default\", \"postgres\", \"mysql\", \"oracle\", \"mongodb\", \"sqlite\"],\n        default=\"default\",\n        help=\"Database backend to use (default: uses DATABASE_URL env var)\",\n    )\n    args = parser.parse_args()\n\n    asyncio.run(run(args.db))\n"
  },
  {
    "path": "tests/llm/clients/oss/xai/async_stream.py",
    "content": "#!/usr/bin/env python3\n\nimport argparse\nimport asyncio\nimport os\n\nfrom xai_sdk import AsyncClient\nfrom xai_sdk.chat import user\n\nfrom memori import Memori\nfrom tests.database.core import (\n    MongoTestDBSession,\n    MySQLTestDBSession,\n    OracleTestDBSession,\n    PostgresTestDBSession,\n    SQLiteTestDBSession,\n    TestDBSession,\n)\n\nif os.environ.get(\"XAI_API_KEY\", None) is None:\n    raise RuntimeError(\"XAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n\nasync def run(db_backend: str = \"default\"):\n    if db_backend == \"mongodb\":\n        session = MongoTestDBSession\n    elif db_backend == \"mysql\":\n        session = MySQLTestDBSession\n    elif db_backend == \"oracle\":\n        session = OracleTestDBSession\n    elif db_backend == \"postgres\":\n        session = PostgresTestDBSession\n    elif db_backend == \"sqlite\":\n        session = SQLiteTestDBSession\n    else:\n        session = TestDBSession\n\n    client = AsyncClient(api_key=os.environ.get(\"XAI_API_KEY\"))\n\n    mem = Memori(conn=session).llm.register(client, stream=True)\n\n    mem.config.storage.build()\n\n    mem.llm.register(client, stream=True)\n\n    mem.attribution(entity_id=\"123\", process_id=\"456\")\n\n    print(\"-\" * 25)\n\n    query = \"What color is the planet Mars?\"\n    print(f\"me: {query}\")\n    chat = client.chat.create(\n        model=\"grok-4\",\n        messages=[user(query)],\n    )\n\n    print(\"-\" * 25)\n    print(\"llm: \", end=\"\", flush=True)\n\n    full_response = []\n    async for item in chat.stream():\n        if isinstance(item, tuple) and len(item) == 2:\n            _, delta = item\n            if hasattr(delta, \"content\") and delta.content:\n                print(delta.content, end=\"\", flush=True)\n                full_response.append(delta.content)\n        elif hasattr(item, \"content\") and item.content:\n            print(item.content, end=\"\", flush=True)\n            full_response.append(item.content)\n\n    print(\"\\n\" + \"-\" * 25)\n\n    query = \"That planet we're talking about, in order from the sun which one is it?\"\n    print(f\"me: {query}\")\n\n    print(\"-\" * 25)\n    print(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\n    chat = client.chat.create(\n        model=\"grok-4\",\n        messages=[user(query)],\n    )\n\n    print(\"-\" * 25)\n    print(\"llm: \", end=\"\", flush=True)\n\n    full_response = []\n    async for item in chat.stream():\n        if isinstance(item, tuple) and len(item) == 2:\n            _, delta = item\n            if hasattr(delta, \"content\") and delta.content:\n                print(delta.content, end=\"\", flush=True)\n                full_response.append(delta.content)\n        elif hasattr(item, \"content\") and item.content:\n            print(item.content, end=\"\", flush=True)\n            full_response.append(item.content)\n\n    print(\"\\n\" + \"-\" * 25)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        description=\"Test XAI async streaming client with various database backends\"\n    )\n    parser.add_argument(\n        \"--db\",\n        choices=[\"default\", \"postgres\", \"mysql\", \"oracle\", \"mongodb\", \"sqlite\"],\n        default=\"default\",\n        help=\"Database backend to use (default: uses DATABASE_URL env var)\",\n    )\n    args = parser.parse_args()\n\n    asyncio.run(run(args.db))\n"
  },
  {
    "path": "tests/llm/clients/oss/xai/sync.py",
    "content": "#!/usr/bin/env python3\n\nimport os\n\nfrom xai_sdk import Client\nfrom xai_sdk.chat import user\n\nfrom memori import Memori\nfrom tests.database.core import TestDBSession\n\nif os.environ.get(\"XAI_API_KEY\", None) is None:\n    raise RuntimeError(\"XAI_API_KEY is not set\")\n\nos.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\nsession = TestDBSession\nclient = Client(api_key=os.environ.get(\"XAI_API_KEY\"))\n\nmem = Memori(conn=session).llm.register(client)\n\nmem.llm.register(client)\n\nmem.attribution(entity_id=\"123\", process_id=\"456\")\n\nprint(\"-\" * 25)\n\nquery = \"What color is the planet Mars?\"\nprint(f\"me: {query}\")\nchat = client.chat.create(\n    model=\"grok-4\",\n    messages=[user(query)],\n)\nresponse = chat.sample()\n\nprint(\"-\" * 25)\n\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n\nquery = \"That planet we're talking about, in order from the sun which one is it?\"\nprint(f\"me: {query}\")\n\nprint(\"-\" * 25)\nprint(\"CONVERSATION INJECTION OCCURRED HERE!\\n\")\n\nchat = client.chat.create(\n    model=\"grok-4\",\n    messages=[user(query)],\n)\nresponse = chat.sample()\n\nprint(\"-\" * 25)\n\nprint(f\"llm: {response.content}\")\n\nprint(\"-\" * 25)\n"
  },
  {
    "path": "tests/llm/providers/__init__.py",
    "content": ""
  },
  {
    "path": "tests/llm/providers/azure_openai/__init__.py",
    "content": ""
  },
  {
    "path": "tests/llm/providers/azure_openai/test_azure_openai.py",
    "content": "\"\"\"\nTests for AzureOpenAI client detection and registration.\n\nThis ensures that AzureOpenAI clients are properly detected by Memori's\nclient registration system, which uses `.startswith(\"openai\")` to match\nboth OpenAI and AzureOpenAI clients.\n\"\"\"\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.llm._clients import OpenAi\n\n\n@pytest.fixture\ndef config():\n    return Config()\n\n\n@pytest.fixture\ndef openai_handler(config):\n    return OpenAi(config)\n\n\nclass TestAzureOpenAIModuleDetection:\n    \"\"\"Tests for AzureOpenAI module path detection.\"\"\"\n\n    def test_openai_module_path(self):\n        \"\"\"Verify OpenAI class module is 'openai'.\"\"\"\n        from openai import OpenAI\n\n        assert OpenAI.__module__ == \"openai\"\n\n    def test_azure_openai_module_path(self):\n        \"\"\"Verify AzureOpenAI class module is 'openai.lib.azure'.\"\"\"\n        from openai import AzureOpenAI\n\n        assert AzureOpenAI.__module__ == \"openai.lib.azure\"\n\n    def test_azure_openai_startswith_openai(self):\n        \"\"\"Verify AzureOpenAI module starts with 'openai'.\"\"\"\n        from openai import AzureOpenAI\n\n        assert AzureOpenAI.__module__.startswith(\"openai\")\n\n    def test_openai_startswith_openai(self):\n        \"\"\"Verify OpenAI module starts with 'openai'.\"\"\"\n        from openai import OpenAI\n\n        assert OpenAI.__module__.startswith(\"openai\")\n\n\nclass TestAzureOpenAIInheritance:\n    \"\"\"Tests for AzureOpenAI class inheritance.\"\"\"\n\n    def test_azure_openai_is_subclass_of_openai(self):\n        \"\"\"Verify AzureOpenAI inherits from OpenAI.\"\"\"\n        from openai import AzureOpenAI, OpenAI\n\n        assert issubclass(AzureOpenAI, OpenAI)\n\n    def test_azure_openai_has_chat_attribute(self):\n        \"\"\"Verify AzureOpenAI has the required 'chat' attribute.\"\"\"\n        from openai import AzureOpenAI\n\n        assert hasattr(AzureOpenAI, \"chat\")\n\n    def test_azure_openai_has_beta_attribute(self):\n        \"\"\"Verify AzureOpenAI has the required 'beta' attribute.\"\"\"\n        from openai import AzureOpenAI\n\n        assert hasattr(AzureOpenAI, \"beta\")\n\n\nclass TestAzureOpenAIRegistryDetection:\n    \"\"\"Tests for AzureOpenAI detection by Memori's Registry.\"\"\"\n\n    def test_registry_detects_openai_client(self):\n        \"\"\"Verify Registry detects standard OpenAI clients.\"\"\"\n        from openai import OpenAI\n\n        # The actual detection happens via type(client).__module__\n        assert OpenAI.__module__.startswith(\"openai\")\n\n    def test_registry_detects_azure_openai_client(self):\n        \"\"\"Verify Registry detects AzureOpenAI clients.\"\"\"\n        from openai import AzureOpenAI\n\n        # The detection should match AzureOpenAI module\n        assert AzureOpenAI.__module__.startswith(\"openai\")\n\n    def test_registry_does_not_detect_anthropic(self):\n        \"\"\"Verify Registry does not falsely detect Anthropic as OpenAI.\"\"\"\n        import anthropic\n\n        assert not anthropic.Anthropic.__module__.startswith(\"openai\")\n\n    def test_registry_does_not_detect_langchain_openai(self):\n        \"\"\"Verify Registry does not falsely detect langchain_openai as OpenAI.\"\"\"\n        # langchain_openai module starts with 'langchain', not 'openai'\n        module_name = \"langchain_openai\"\n        assert not module_name.startswith(\"openai\")\n\n\nclass TestAzureOpenAIClientRegistration:\n    \"\"\"Tests for AzureOpenAI client registration with Memori.\"\"\"\n\n    def test_openai_handler_registers_openai_client(self, openai_handler, mocker):\n        \"\"\"Verify OpenAi handler can register standard OpenAI clients.\"\"\"\n        mock_client = mocker.MagicMock()\n        mock_client._version = \"1.0.0\"\n        mock_client.chat.completions.create = mocker.MagicMock()\n        mock_client.beta.chat.completions.parse = mocker.MagicMock()\n        del mock_client._memori_installed\n\n        mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n        result = openai_handler.register(mock_client)\n\n        assert result is openai_handler\n        assert hasattr(mock_client, \"_memori_installed\")\n        assert mock_client._memori_installed is True\n\n    def test_openai_handler_registers_azure_openai_client(self, openai_handler, mocker):\n        \"\"\"Verify OpenAi handler can register AzureOpenAI clients.\n\n        AzureOpenAI has the same API structure as OpenAI, so the same\n        handler should work for both.\n        \"\"\"\n        # Mock an AzureOpenAI-like client\n        mock_azure_client = mocker.MagicMock()\n        mock_azure_client._version = \"1.0.0\"\n        mock_azure_client.chat.completions.create = mocker.MagicMock()\n        mock_azure_client.beta.chat.completions.parse = mocker.MagicMock()\n        del mock_azure_client._memori_installed\n\n        mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n        result = openai_handler.register(mock_azure_client)\n\n        assert result is openai_handler\n        assert hasattr(mock_azure_client, \"_memori_installed\")\n        assert mock_azure_client._memori_installed is True\n\n    def test_openai_handler_wraps_chat_completions_create(self, openai_handler, mocker):\n        \"\"\"Verify handler wraps chat.completions.create for AzureOpenAI.\"\"\"\n        mock_client = mocker.MagicMock()\n        mock_client._version = \"1.0.0\"\n        mock_client.beta.chat.completions.parse = mocker.MagicMock()\n        del mock_client._memori_installed\n\n        mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n        openai_handler.register(mock_client)\n\n        # Verify the original method was stored as backup\n        assert hasattr(mock_client.chat, \"_completions_create\")\n\n    def test_openai_handler_wraps_beta_parse(self, openai_handler, mocker):\n        \"\"\"Verify handler wraps beta.chat.completions.parse for AzureOpenAI.\"\"\"\n        mock_client = mocker.MagicMock()\n        mock_client._version = \"1.0.0\"\n        mock_client.chat.completions.create = mocker.MagicMock()\n        del mock_client._memori_installed\n\n        mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n        openai_handler.register(mock_client)\n\n        # Verify the original method was stored as backup\n        assert hasattr(mock_client.beta, \"_chat_completions_parse\")\n\n\nclass TestAzureOpenAINoFalsePositives:\n    \"\"\"Tests to ensure no false positives in client detection.\"\"\"\n\n    def test_anthropic_not_matched(self):\n        \"\"\"Anthropic module should not match OpenAI detection.\"\"\"\n        assert not \"anthropic\".startswith(\"openai\")\n\n    def test_google_not_matched(self):\n        \"\"\"Google module should not match OpenAI detection.\"\"\"\n        assert not \"google.generativeai\".startswith(\"openai\")\n\n    def test_langchain_openai_not_matched(self):\n        \"\"\"langchain_openai module should not match (uses underscore).\"\"\"\n        assert not \"langchain_openai\".startswith(\"openai\")\n\n    def test_pydantic_ai_not_matched(self):\n        \"\"\"pydantic_ai module should not match OpenAI detection.\"\"\"\n        assert not \"pydantic_ai\".startswith(\"openai\")\n\n    def test_xai_not_matched(self):\n        \"\"\"xai module should not match OpenAI detection.\"\"\"\n        assert not \"xai_sdk\".startswith(\"openai\")\n\n    def test_openai_submodules_matched(self):\n        \"\"\"All openai submodules should match detection.\"\"\"\n        openai_modules = [\n            \"openai\",\n            \"openai.lib.azure\",\n            \"openai.resources\",\n            \"openai.types\",\n            \"openai._client\",\n        ]\n        for module in openai_modules:\n            assert module.startswith(\"openai\"), f\"{module} should match\"\n"
  },
  {
    "path": "tests/llm/providers/google_genai/__init__.py",
    "content": ""
  },
  {
    "path": "tests/llm/providers/google_genai/test_google_genai.py",
    "content": "\"\"\"\nTests for google-genai SDK support.\n\nThis ensures that the new google-genai SDK format (without _pb protobuf)\nis properly handled for both streaming and non-streaming responses.\n\n\"\"\"\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.llm._base import BaseInvoke, BaseIterator\n\n\n@pytest.fixture\ndef config():\n    return Config()\n\n\nclass MockGoogleGenaiPart:\n    \"\"\"Mock google-genai Part object.\"\"\"\n\n    def __init__(self, text):\n        self.text = text\n\n\nclass MockGoogleGenaiContent:\n    \"\"\"Mock google-genai Content object.\"\"\"\n\n    def __init__(self, parts, role=\"model\"):\n        self.parts = parts\n        self.role = role\n\n\nclass MockGoogleGenaiCandidate:\n    \"\"\"Mock google-genai Candidate object.\"\"\"\n\n    def __init__(self, content):\n        self.content = content\n\n\nclass MockGoogleGenaiResponse:\n    \"\"\"Mock google-genai GenerateContentResponse (non-streaming).\"\"\"\n\n    def __init__(self, text, role=\"model\"):\n        part = MockGoogleGenaiPart(text)\n        content = MockGoogleGenaiContent([part], role)\n        self.candidates = [MockGoogleGenaiCandidate(content)]\n        # Note: No _pb attribute - this is the new format\n\n\nclass MockGoogleGenaiChunk:\n    \"\"\"Mock google-genai streaming chunk.\"\"\"\n\n    def __init__(self, text, role=\"model\"):\n        part = MockGoogleGenaiPart(text)\n        content = MockGoogleGenaiContent([part], role)\n        self.candidates = [MockGoogleGenaiCandidate(content)]\n        # Note: No _pb attribute - this is the new format\n\n\nclass TestGoogleGenaiFormatDetection:\n    \"\"\"Tests for detecting google-genai format (no _pb attribute).\"\"\"\n\n    def test_response_has_no_pb_attribute(self):\n        \"\"\"Verify mock response doesn't have _pb (like real google-genai).\"\"\"\n        response = MockGoogleGenaiResponse(\"Hello\")\n        assert not hasattr(response, \"_pb\")\n        assert \"_pb\" not in response.__dict__\n\n    def test_response_has_candidates_attribute(self):\n        \"\"\"Verify mock response has candidates (like real google-genai).\"\"\"\n        response = MockGoogleGenaiResponse(\"Hello\")\n        assert hasattr(response, \"candidates\")\n        assert len(response.candidates) == 1\n\n    def test_chunk_has_no_pb_attribute(self):\n        \"\"\"Verify mock chunk doesn't have _pb.\"\"\"\n        chunk = MockGoogleGenaiChunk(\"Hi\")\n        assert not hasattr(chunk, \"_pb\")\n        assert \"_pb\" not in chunk.__dict__\n\n    def test_chunk_has_candidates_attribute(self):\n        \"\"\"Verify mock chunk has candidates.\"\"\"\n        chunk = MockGoogleGenaiChunk(\"Hi\")\n        assert hasattr(chunk, \"candidates\")\n\n\nclass TestGoogleGenaiNonStreamingFormat:\n    \"\"\"Tests for non-streaming google-genai response formatting.\"\"\"\n\n    def test_format_response_with_google_genai_format(self, config):\n        \"\"\"Test _format_response handles google-genai format.\"\"\"\n        invoke = BaseInvoke(config, lambda **kwargs: None)\n        invoke._uses_protobuf = True\n\n        response = MockGoogleGenaiResponse(\"Hello world\", role=\"model\")\n        formatted = invoke._format_response(response)\n\n        assert \"candidates\" in formatted\n        assert len(formatted[\"candidates\"]) == 1\n        assert \"content\" in formatted[\"candidates\"][0]\n        assert \"parts\" in formatted[\"candidates\"][0][\"content\"]\n        assert (\n            formatted[\"candidates\"][0][\"content\"][\"parts\"][0][\"text\"] == \"Hello world\"\n        )\n        assert formatted[\"candidates\"][0][\"content\"][\"role\"] == \"model\"\n\n    def test_format_response_with_empty_candidates(self, config):\n        \"\"\"Test _format_response handles empty candidates.\"\"\"\n        invoke = BaseInvoke(config, lambda **kwargs: None)\n        invoke._uses_protobuf = True\n\n        response = MockGoogleGenaiResponse(\"Test\")\n        response.candidates = []\n        formatted = invoke._format_response(response)\n\n        # Empty candidates returns empty dict (no content to save)\n        assert formatted == {}\n\n    def test_format_response_preserves_role(self, config):\n        \"\"\"Test that role is preserved in formatted response.\"\"\"\n        invoke = BaseInvoke(config, lambda **kwargs: None)\n        invoke._uses_protobuf = True\n\n        response = MockGoogleGenaiResponse(\"Hello\", role=\"model\")\n        formatted = invoke._format_response(response)\n\n        assert formatted[\"candidates\"][0][\"content\"][\"role\"] == \"model\"\n\n\nclass TestGoogleGenaiStreamingFormat:\n    \"\"\"Tests for streaming google-genai chunk processing.\"\"\"\n\n    def test_process_chunk_with_google_genai_format(self, config):\n        \"\"\"Test process_chunk handles google-genai chunk format.\"\"\"\n        invoke = BaseInvoke(config, lambda **kwargs: None)\n        invoke._uses_protobuf = True\n\n        iterator = BaseIterator(config, iter([]))\n        iterator.invoke = invoke\n        iterator.raw_response = []\n\n        chunk = MockGoogleGenaiChunk(\"Hello\", role=\"model\")\n        iterator.process_chunk(chunk)\n\n        assert len(iterator.raw_response) == 1\n        assert \"candidates\" in iterator.raw_response[0]\n        assert (\n            iterator.raw_response[0][\"candidates\"][0][\"content\"][\"parts\"][0][\"text\"]\n            == \"Hello\"\n        )\n\n    def test_process_multiple_chunks(self, config):\n        \"\"\"Test processing multiple streaming chunks.\"\"\"\n        invoke = BaseInvoke(config, lambda **kwargs: None)\n        invoke._uses_protobuf = True\n\n        iterator = BaseIterator(config, iter([]))\n        iterator.invoke = invoke\n        iterator.raw_response = []\n\n        chunks = [\n            MockGoogleGenaiChunk(\"Hello\"),\n            MockGoogleGenaiChunk(\" \"),\n            MockGoogleGenaiChunk(\"World\"),\n        ]\n\n        for chunk in chunks:\n            iterator.process_chunk(chunk)\n\n        assert len(iterator.raw_response) == 3\n        assert (\n            iterator.raw_response[0][\"candidates\"][0][\"content\"][\"parts\"][0][\"text\"]\n            == \"Hello\"\n        )\n        assert (\n            iterator.raw_response[1][\"candidates\"][0][\"content\"][\"parts\"][0][\"text\"]\n            == \" \"\n        )\n        assert (\n            iterator.raw_response[2][\"candidates\"][0][\"content\"][\"parts\"][0][\"text\"]\n            == \"World\"\n        )\n\n    def test_process_chunk_preserves_role(self, config):\n        \"\"\"Test that role is preserved in processed chunk.\"\"\"\n        invoke = BaseInvoke(config, lambda **kwargs: None)\n        invoke._uses_protobuf = True\n\n        iterator = BaseIterator(config, iter([]))\n        iterator.invoke = invoke\n        iterator.raw_response = []\n\n        chunk = MockGoogleGenaiChunk(\"Test\", role=\"model\")\n        iterator.process_chunk(chunk)\n\n        assert iterator.raw_response[0][\"candidates\"][0][\"content\"][\"role\"] == \"model\"\n\n\nclass TestGoogleGenaiBackwardsCompatibility:\n    \"\"\"Tests to ensure old google-generativeai format still works.\"\"\"\n\n    def test_format_response_with_pb_format(self, config):\n        \"\"\"Test _format_response still handles _pb format.\"\"\"\n\n        class MockPbResponse:\n            def __init__(self):\n                self._pb = None  # Would be protobuf in real usage\n\n        invoke = BaseInvoke(config, lambda **kwargs: None)\n        invoke._uses_protobuf = True\n\n        # When _pb exists but is None, it should try protobuf parsing\n        # This test just verifies the _pb path is still checked first\n        response = MockPbResponse()\n        assert \"_pb\" in response.__dict__\n\n    def test_non_protobuf_response_unchanged(self, config):\n        \"\"\"Test non-protobuf responses pass through unchanged.\"\"\"\n        invoke = BaseInvoke(config, lambda **kwargs: None)\n        invoke._uses_protobuf = False\n\n        response = {\"choices\": [{\"message\": {\"content\": \"Hello\"}}]}\n        formatted = invoke._format_response(response)\n\n        assert formatted == response\n"
  },
  {
    "path": "tests/llm/test_llm_base.py",
    "content": "import json\nfrom unittest.mock import Mock, patch\n\nfrom memori._config import Config\nfrom memori.llm._base import BaseInvoke, BaseLlmAdaptor\nfrom memori.llm._constants import (\n    LANGCHAIN_FRAMEWORK_PROVIDER,\n    LANGCHAIN_OPENAI_LLM_PROVIDER,\n    OPENAI_LLM_PROVIDER,\n)\n\n\ndef test_dict_to_json_dict():\n    assert BaseInvoke(Config(), \"abc\").dict_to_json({\"a\": \"b\", \"c\": \"d\"}) == {\n        \"a\": \"b\",\n        \"c\": \"d\",\n    }\n\n\ndef test_dist_to_json_dict_has_dict():\n    assert BaseInvoke(Config(), \"abc\").dict_to_json(\n        {\"a\": {\"b\": {\"c\": \"d\"}, \"e\": 123}}\n    ) == {\"a\": {\"b\": {\"c\": \"d\"}, \"e\": 123}}\n\n\ndef test_configure_for_streaming_usage_openai():\n    invoke = BaseInvoke(Config(), \"abc\")\n    invoke.config.llm.provider = OPENAI_LLM_PROVIDER\n\n    assert invoke.configure_for_streaming_usage({\"abc\": \"def\", \"stream\": True}) == {\n        \"abc\": \"def\",\n        \"stream\": True,\n        \"stream_options\": {\"include_usage\": True},\n    }\n\n    assert invoke.configure_for_streaming_usage(\n        {\"abc\": \"def\", \"stream\": True, \"stream_options\": {}}\n    ) == {\"abc\": \"def\", \"stream\": True, \"stream_options\": {\"include_usage\": True}}\n\n    assert invoke.configure_for_streaming_usage(\n        {\"abc\": \"def\", \"stream\": True, \"stream_options\": {\"include_usage\": False}}\n    ) == {\"abc\": \"def\", \"stream\": True, \"stream_options\": {\"include_usage\": True}}\n\n\ndef test_configure_for_streaming_usage_streaming_options_is_not_dict_openai():\n    invoke = BaseInvoke(Config(), \"abc\")\n    invoke.config.llm.provider = OPENAI_LLM_PROVIDER\n\n    assert invoke.configure_for_streaming_usage(\n        {\"abc\": \"def\", \"stream\": True, \"stream_options\": 123}\n    ) == {\n        \"abc\": \"def\",\n        \"stream\": True,\n        \"stream_options\": {\"include_usage\": True},\n    }\n\n\ndef test_configure_for_streaming_usage_only_if_stream_is_true_openai():\n    invoke = BaseInvoke(Config(), \"abc\")\n    invoke.config.llm.provider = OPENAI_LLM_PROVIDER\n\n    assert invoke.configure_for_streaming_usage({\"abc\": \"def\"}) == {\"abc\": \"def\"}\n\n\ndef test_configure_for_streaming_usage_langchain_openai():\n    invoke = BaseInvoke(Config(), \"abc\")\n    invoke.config.framework.provider = LANGCHAIN_FRAMEWORK_PROVIDER\n    invoke.config.llm.provider = OPENAI_LLM_PROVIDER\n\n    assert invoke.configure_for_streaming_usage({\"abc\": \"def\", \"stream\": True}) == {\n        \"abc\": \"def\",\n        \"stream\": True,\n        \"stream_options\": {\"include_usage\": True},\n    }\n\n    assert invoke.configure_for_streaming_usage(\n        {\"abc\": \"def\", \"stream\": True, \"stream_options\": {}}\n    ) == {\"abc\": \"def\", \"stream\": True, \"stream_options\": {\"include_usage\": True}}\n\n    assert invoke.configure_for_streaming_usage(\n        {\"abc\": \"def\", \"stream\": True, \"stream_options\": {\"include_usage\": False}}\n    ) == {\"abc\": \"def\", \"stream\": True, \"stream_options\": {\"include_usage\": True}}\n\n\ndef test_configure_for_streaming_usage_streaming_opts_is_not_dict_langchain_openai():\n    invoke = BaseInvoke(Config(), \"abc\")\n    invoke.config.framework.provider = LANGCHAIN_FRAMEWORK_PROVIDER\n    invoke.config.llm.provider = LANGCHAIN_OPENAI_LLM_PROVIDER\n\n    assert invoke.configure_for_streaming_usage(\n        {\"abc\": \"def\", \"stream\": True, \"stream_options\": 123}\n    ) == {\n        \"abc\": \"def\",\n        \"stream\": True,\n        \"stream_options\": {\"include_usage\": True},\n    }\n\n\ndef test_configure_for_streaming_usage_only_if_stream_is_true_langchain_openai():\n    invoke = BaseInvoke(Config(), \"abc\")\n    invoke.config.framework.provider = LANGCHAIN_FRAMEWORK_PROVIDER\n    invoke.config.llm.provider = LANGCHAIN_OPENAI_LLM_PROVIDER\n\n    assert invoke.configure_for_streaming_usage({\"abc\": \"def\"}) == {\"abc\": \"def\"}\n\n\ndef test_get_response_content():\n    invoke = BaseInvoke(Config(), \"abc\")\n\n    assert invoke.get_response_content({\"abc\": \"def\"}) == {\"abc\": \"def\"}\n\n    class MockLegacyAPIResponse:\n        def __init__(self):\n            self.text = json.dumps({\"abc\": \"def\"})\n\n    legacy_api_response = MockLegacyAPIResponse()\n    legacy_api_response.__class__.__name__ = \"LegacyAPIResponse\"\n    legacy_api_response.__class__.__module__ = \"openai._legacy_response\"\n\n    assert invoke.get_response_content(legacy_api_response) == {\"abc\": \"def\"}\n\n\ndef test_exclude_injected_messages():\n    adapter = BaseLlmAdaptor()\n\n    # No injected count - returns all messages\n    messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n    payload = {\"conversation\": {\"query\": {}}}\n    assert adapter._exclude_injected_messages(messages, payload) == messages\n\n    # Injected count of 2 - slices off first 2 messages\n    messages = [\n        {\"role\": \"user\", \"content\": \"injected 1\"},\n        {\"role\": \"assistant\", \"content\": \"injected 2\"},\n        {\"role\": \"user\", \"content\": \"new message\"},\n    ]\n    payload = {\"conversation\": {\"query\": {\"_memori_injected_count\": 2}}}\n    assert adapter._exclude_injected_messages(messages, payload) == [\n        {\"role\": \"user\", \"content\": \"new message\"}\n    ]\n\n    # Safe navigation - missing keys don't cause errors\n    assert adapter._exclude_injected_messages(messages, {}) == messages\n\n\ndef test_handle_post_response_without_augmentation():\n    config = Config()\n    invoke = BaseInvoke(config, \"test_method\")\n    invoke.set_client(\"test_provider\", \"test_title\", \"1.0.0\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    start_time = 1234567890.0\n    raw_response = {\"choices\": [{\"message\": {\"content\": \"Hi\"}}]}\n\n    with patch(\"memori.memory._manager.Manager\") as mock_memory_manager:\n        mock_manager_instance = Mock()\n        mock_memory_manager.return_value = mock_manager_instance\n\n        with patch(\n            \"memori.memory._conversation_messages.parse_payload_conversation_messages\"\n        ) as mock_parse:\n            mock_parse.return_value = [{\"role\": \"user\", \"type\": None, \"text\": \"Hello\"}]\n\n            invoke.handle_post_response(kwargs, start_time, raw_response)\n\n            mock_memory_manager.assert_called_once_with(config)\n            mock_manager_instance.execute.assert_called_once()\n\n\ndef test_handle_post_response_with_augmentation_no_conversation():\n    config = Config()\n    config.augmentation = Mock()\n    config.entity_id = \"test-entity\"\n    invoke = BaseInvoke(config, \"test_method\")\n    invoke.set_client(\"test_provider\", \"test_title\", \"1.0.0\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    start_time = 1234567890.0\n    raw_response = {\"choices\": [{\"message\": {\"content\": \"Hi\"}}]}\n\n    with patch(\"memori.memory._manager.Manager\") as mock_memory_manager:\n        mock_manager_instance = Mock()\n        mock_memory_manager.return_value = mock_manager_instance\n\n        with patch(\n            \"memori.memory._conversation_messages.parse_payload_conversation_messages\"\n        ) as mock_parse:\n            mock_parse.return_value = [{\"role\": \"user\", \"type\": None, \"text\": \"Hello\"}]\n\n            invoke.handle_post_response(kwargs, start_time, raw_response)\n\n            mock_memory_manager.assert_called_once_with(config)\n            mock_manager_instance.execute.assert_called_once()\n            config.augmentation.enqueue.assert_called_once()\n            call_args = config.augmentation.enqueue.call_args[0][0]\n            assert call_args.conversation_id is None\n            assert call_args.entity_id == \"test-entity\"\n            assert call_args.conversation_messages[0].role == \"user\"\n            assert call_args.conversation_messages[0].content == \"Hello\"\n\n\ndef test_handle_post_response_with_augmentation_and_conversation():\n    config = Config()\n    config.augmentation = Mock()\n    config.entity_id = \"test-entity\"\n    config.cache.conversation_id = 123\n    invoke = BaseInvoke(config, \"test_method\")\n    invoke.set_client(\"test_provider\", \"test_title\", \"1.0.0\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    start_time = 1234567890.0\n    raw_response = {\"choices\": [{\"message\": {\"content\": \"Hi\"}}]}\n\n    with patch(\"memori.memory._manager.Manager\") as mock_memory_manager:\n        mock_manager_instance = Mock()\n        mock_memory_manager.return_value = mock_manager_instance\n\n        with patch(\n            \"memori.memory._conversation_messages.parse_payload_conversation_messages\"\n        ) as mock_parse:\n            mock_parse.return_value = [{\"role\": \"user\", \"type\": None, \"text\": \"Hello\"}]\n\n            invoke.handle_post_response(kwargs, start_time, raw_response)\n\n            mock_memory_manager.assert_called_once_with(config)\n            mock_manager_instance.execute.assert_called_once()\n            config.augmentation.enqueue.assert_called_once()\n            call_args = config.augmentation.enqueue.call_args[0][0]\n            assert call_args.conversation_id == 123\n            assert call_args.entity_id == \"test-entity\"\n            assert call_args.conversation_messages[0].role == \"user\"\n            assert call_args.conversation_messages[0].content == \"Hello\"\n\n\ndef test_extract_user_query_with_user_message():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    kwargs = {\n        \"messages\": [\n            {\"role\": \"system\", \"content\": \"You are helpful\"},\n            {\"role\": \"user\", \"content\": \"What is the weather?\"},\n        ]\n    }\n    assert invoke._extract_user_query(kwargs) == \"What is the weather?\"\n\n\ndef test_extract_user_query_with_multiple_user_messages():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    kwargs = {\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"First question\"},\n            {\"role\": \"assistant\", \"content\": \"First answer\"},\n            {\"role\": \"user\", \"content\": \"Second question\"},\n        ]\n    }\n    assert invoke._extract_user_query(kwargs) == \"Second question\"\n\n\ndef test_extract_user_query_no_messages():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    assert invoke._extract_user_query({}) == \"\"\n    assert invoke._extract_user_query({\"messages\": []}) == \"\"\n\n\ndef test_extract_user_query_no_user_messages():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    kwargs = {\n        \"messages\": [\n            {\"role\": \"system\", \"content\": \"You are helpful\"},\n            {\"role\": \"assistant\", \"content\": \"I can help\"},\n        ]\n    }\n    assert invoke._extract_user_query(kwargs) == \"\"\n\n\ndef test_extract_user_query_google_contents_string():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    kwargs = {\"contents\": \"What is the weather?\"}\n    assert invoke._extract_user_query(kwargs) == \"What is the weather?\"\n\n\ndef test_extract_user_query_google_contents_list_of_strings():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    kwargs = {\"contents\": [\"First message\", \"Second message\"]}\n    assert invoke._extract_user_query(kwargs) == \"Second message\"\n\n\ndef test_extract_user_query_google_contents_list_of_dicts():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    kwargs = {\n        \"contents\": [\n            {\"role\": \"user\", \"parts\": [{\"text\": \"First question\"}]},\n            {\"role\": \"model\", \"parts\": [{\"text\": \"Answer\"}]},\n            {\"role\": \"user\", \"parts\": [{\"text\": \"Second question\"}]},\n        ]\n    }\n    assert invoke._extract_user_query(kwargs) == \"Second question\"\n\n\ndef test_extract_user_query_google_contents_with_string_parts():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    kwargs = {\n        \"contents\": [\n            {\"role\": \"user\", \"parts\": [\"Hello\", \"World\"]},\n        ]\n    }\n    assert invoke._extract_user_query(kwargs) == \"Hello World\"\n\n\ndef test_extract_user_query_google_contents_empty():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    assert invoke._extract_user_query({\"contents\": []}) == \"\"\n    assert invoke._extract_user_query({\"contents\": \"\"}) == \"\"\n\n\ndef test_extract_text_from_parts_with_strings():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    parts = [\"Hello\", \"World\"]\n    assert invoke._extract_text_from_parts(parts) == \"Hello World\"\n\n\ndef test_extract_text_from_parts_with_dicts():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    parts = [{\"text\": \"Hello\"}, {\"text\": \"World\"}]\n    assert invoke._extract_text_from_parts(parts) == \"Hello World\"\n\n\ndef test_extract_text_from_parts_mixed():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    parts = [\"Hello\", {\"text\": \"World\"}]\n    assert invoke._extract_text_from_parts(parts) == \"Hello World\"\n\n\ndef test_extract_text_from_parts_empty():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    assert invoke._extract_text_from_parts([]) == \"\"\n\n\ndef test_extract_from_contents_string():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    assert invoke._extract_from_contents(\"Hello\") == \"Hello\"\n\n\ndef test_extract_from_contents_list_strings():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    assert invoke._extract_from_contents([\"First\", \"Second\"]) == \"Second\"\n\n\ndef test_extract_from_contents_list_dicts():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    contents = [\n        {\"role\": \"user\", \"parts\": [{\"text\": \"Question\"}]},\n    ]\n    assert invoke._extract_from_contents(contents) == \"Question\"\n\n\ndef test_inject_recalled_facts_no_storage():\n    config = Config()\n    config.storage = None\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    result = invoke.inject_recalled_facts(kwargs)\n\n    assert result == kwargs\n\n\ndef test_inject_recalled_facts_no_entity_id():\n    config = Config()\n    config.storage = Mock()\n    config.entity_id = None\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    result = invoke.inject_recalled_facts(kwargs)\n\n    assert result == kwargs\n\n\ndef test_inject_recalled_facts_no_user_query():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"system\", \"content\": \"You are helpful\"}]}\n    result = invoke.inject_recalled_facts(kwargs)\n\n    assert result == kwargs\n\n\ndef test_inject_recalled_facts_no_facts_found():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = []\n        result = invoke.inject_recalled_facts(kwargs)\n\n    assert result == kwargs\n    assert len(kwargs[\"messages\"]) == 1\n\n\ndef test_inject_recalled_facts_no_relevant_facts():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = [\n            {\"content\": \"Irrelevant fact\", \"similarity\": 0.05}\n        ]\n        result = invoke.inject_recalled_facts(kwargs)\n\n    assert result == kwargs\n    assert len(kwargs[\"messages\"]) == 1\n\n\ndef test_inject_recalled_facts_success():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"What do I like?\"}]}\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = [\n            {\n                \"content\": \"User likes pizza\",\n                \"similarity\": 0.9,\n                \"date_created\": \"2026-01-01 10:30:00\",\n            },\n            {\n                \"content\": \"User likes coding\",\n                \"similarity\": 0.85,\n                \"date_created\": \"2026-01-02 11:15:00\",\n            },\n        ]\n        result = invoke.inject_recalled_facts(kwargs)\n\n    assert len(result[\"messages\"]) == 2\n    assert result[\"messages\"][0][\"role\"] == \"system\"\n    assert \"User likes pizza\" in result[\"messages\"][0][\"content\"]\n    assert (\n        \"User likes pizza. Stated at 2026-01-01 10:30\"\n        in result[\"messages\"][0][\"content\"]\n    )\n    assert \"User likes coding\" in result[\"messages\"][0][\"content\"]\n    assert result[\"messages\"][1][\"role\"] == \"user\"\n\n\ndef test_inject_recalled_facts_filters_by_relevance():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = [\n            {\"content\": \"Relevant fact\", \"similarity\": 0.9},\n            {\"content\": \"Irrelevant fact\", \"similarity\": 0.05},\n        ]\n        result = invoke.inject_recalled_facts(kwargs)\n\n    assert len(result[\"messages\"]) == 2\n    assert \"Relevant fact\" in result[\"messages\"][0][\"content\"]\n    assert \"Irrelevant fact\" not in result[\"messages\"][0][\"content\"]\n\n\ndef test_inject_recalled_facts_extends_existing_system_message():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\n        \"messages\": [\n            {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n            {\"role\": \"user\", \"content\": \"What do I like?\"},\n        ]\n    }\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = [\n            {\"content\": \"User likes pizza\", \"similarity\": 0.9},\n        ]\n        result = invoke.inject_recalled_facts(kwargs)\n\n    # Should still have 2 messages (not 3)\n    assert len(result[\"messages\"]) == 2\n    # First message should still be system role\n    assert result[\"messages\"][0][\"role\"] == \"system\"\n    # System message should contain both original content and recalled facts\n    assert \"You are a helpful assistant.\" in result[\"messages\"][0][\"content\"]\n    assert \"User likes pizza\" in result[\"messages\"][0][\"content\"]\n    assert \"Relevant context about the user\" in result[\"messages\"][0][\"content\"]\n\n\ndef test_inject_recalled_facts_creates_system_message_when_none_exists():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"What do I like?\"}]}\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = [\n            {\"content\": \"User likes pizza\", \"similarity\": 0.9},\n        ]\n        result = invoke.inject_recalled_facts(kwargs)\n\n    # Should have 2 messages now (system + user)\n    assert len(result[\"messages\"]) == 2\n    # First message should be system role\n    assert result[\"messages\"][0][\"role\"] == \"system\"\n    # System message should contain recalled facts\n    assert \"User likes pizza\" in result[\"messages\"][0][\"content\"]\n    assert \"Relevant context about the user\" in result[\"messages\"][0][\"content\"]\n\n\ndef test_inject_recalled_facts_google_creates_config():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    config.framework.provider = \"langchain\"\n    config.llm.provider = \"chatgooglegenai\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"contents\": \"What do I like?\"}\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = [\n            {\"content\": \"User likes pizza\", \"similarity\": 0.9},\n        ]\n        result = invoke.inject_recalled_facts(kwargs)\n\n    assert \"config\" in result\n    assert \"system_instruction\" in result[\"config\"]\n    assert \"User likes pizza\" in result[\"config\"][\"system_instruction\"]\n\n\ndef test_inject_recalled_facts_google_extends_existing_config():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    config.framework.provider = \"langchain\"\n    config.llm.provider = \"chatgooglegenai\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\n        \"contents\": \"What do I like?\",\n        \"config\": {\"system_instruction\": \"You are helpful.\"},\n    }\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = [\n            {\"content\": \"User likes pizza\", \"similarity\": 0.9},\n        ]\n        result = invoke.inject_recalled_facts(kwargs)\n\n    assert \"You are helpful.\" in result[\"config\"][\"system_instruction\"]\n    assert \"User likes pizza\" in result[\"config\"][\"system_instruction\"]\n\n\ndef test_inject_recalled_facts_google_with_contents_list():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    config.framework.provider = \"langchain\"\n    config.llm.provider = \"chatgooglegenai\"\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\n        \"contents\": [\n            {\"role\": \"user\", \"parts\": [{\"text\": \"What do I like?\"}]},\n        ]\n    }\n\n    with patch(\"memori.memory.recall.Recall\") as mock_recall:\n        mock_recall.return_value.search_facts.return_value = [\n            {\"content\": \"User likes pizza\", \"similarity\": 0.9},\n        ]\n        result = invoke.inject_recalled_facts(kwargs)\n\n    assert \"config\" in result\n    assert \"system_instruction\" in result[\"config\"]\n    assert \"User likes pizza\" in result[\"config\"][\"system_instruction\"]\n\n\ndef test_append_to_google_system_instruction_dict_empty():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    config = {}\n    invoke._append_to_google_system_instruction_dict(config, \"\\n\\ntest context\")\n    assert config[\"system_instruction\"] == \"test context\"\n\n\ndef test_append_to_google_system_instruction_dict_string():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    config = {\"system_instruction\": \"Existing.\"}\n    invoke._append_to_google_system_instruction_dict(config, \"\\n\\ntest context\")\n    assert config[\"system_instruction\"] == \"Existing.\\n\\ntest context\"\n\n\ndef test_append_to_google_system_instruction_dict_list_of_dicts():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    config = {\"system_instruction\": [{\"text\": \"Existing.\"}]}\n    invoke._append_to_google_system_instruction_dict(config, \"\\n\\ntest context\")\n    assert config[\"system_instruction\"][0][\"text\"] == \"Existing.\\n\\ntest context\"\n\n\ndef test_append_to_google_system_instruction_dict_list_of_strings():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    config = {\"system_instruction\": [\"Existing.\"]}\n    invoke._append_to_google_system_instruction_dict(config, \"\\n\\ntest context\")\n    assert config[\"system_instruction\"][0] == \"Existing.\\n\\ntest context\"\n\n\ndef test_append_to_list_empty():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    parent = {\"key\": []}\n    invoke._append_to_list(parent[\"key\"], \"\\n\\ntest\", parent, \"key\")\n    assert parent[\"key\"] == [{\"text\": \"test\"}]\n\n\ndef test_append_to_list_dict_with_text():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    lst = [{\"text\": \"Existing\"}]\n    parent = {\"key\": lst}\n    invoke._append_to_list(lst, \"\\n\\ntest\", parent, \"key\")\n    assert lst[0][\"text\"] == \"Existing\\n\\ntest\"\n\n\ndef test_append_to_list_strings():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    lst = [\"Existing\"]\n    parent = {\"key\": lst}\n    invoke._append_to_list(lst, \"\\n\\ntest\", parent, \"key\")\n    assert lst[0] == \"Existing\\n\\ntest\"\n\n\ndef test_append_to_content_dict_with_parts():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    content = {\"parts\": [{\"text\": \"Existing\"}]}\n    parent = {\"key\": content}\n    invoke._append_to_content_dict(content, \"\\n\\ntest\", parent, \"key\")\n    assert content[\"parts\"][0][\"text\"] == \"Existing\\n\\ntest\"\n\n\ndef test_append_to_content_dict_with_text():\n    invoke = BaseInvoke(Config(), \"test_method\")\n    content = {\"text\": \"Existing\"}\n    parent = {\"key\": content}\n    invoke._append_to_content_dict(content, \"\\n\\ntest\", parent, \"key\")\n    assert content[\"text\"] == \"Existing\\n\\ntest\"\n\n\ndef test_inject_conversation_messages_no_conversation_id():\n    config = Config()\n    config.cache.conversation_id = None\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    result = invoke.inject_conversation_messages(kwargs)\n\n    assert result == kwargs\n\n\ndef test_inject_conversation_messages_no_storage():\n    config = Config()\n    config.cache.conversation_id = 123\n    config.storage = None\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    result = invoke.inject_conversation_messages(kwargs)\n\n    assert result == kwargs\n\n\ndef test_inject_conversation_messages_no_messages():\n    config = Config()\n    config.cache.conversation_id = 123\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.conversation.messages.read.return_value = []\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    result = invoke.inject_conversation_messages(kwargs)\n\n    assert result == kwargs\n    assert invoke._injected_message_count == 0\n\n\ndef test_inject_conversation_messages_openai_success():\n    config = Config()\n    config.cache.conversation_id = 123\n    config.llm.provider = OPENAI_LLM_PROVIDER\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.conversation.messages.read.return_value = [\n        {\"role\": \"user\", \"content\": \"Previous question\"},\n        {\"role\": \"assistant\", \"content\": \"Previous answer\"},\n    ]\n    invoke = BaseInvoke(config, \"test_method\")\n\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"New question\"}]}\n    result = invoke.inject_conversation_messages(kwargs)\n\n    assert len(result[\"messages\"]) == 3\n    assert result[\"messages\"][0][\"content\"] == \"Previous question\"\n    assert result[\"messages\"][1][\"content\"] == \"Previous answer\"\n    assert result[\"messages\"][2][\"content\"] == \"New question\"\n    assert invoke._injected_message_count == 2\n\n\ndef test_inject_conversation_messages_cache_miss_loads_from_session(mocker):\n    config = Config()\n    config.session_id = \"session-uuid\"\n    config.llm.provider = OPENAI_LLM_PROVIDER\n\n    mock_driver = mocker.MagicMock()\n    mock_driver.session.read.return_value = 11\n    mock_driver.conversation.read_id_by_session_id.return_value = 22\n    mock_driver.conversation.messages.read.return_value = [\n        {\"role\": \"user\", \"content\": \"Previous question\"},\n        {\"role\": \"assistant\", \"content\": \"Previous answer\"},\n    ]\n\n    mock_storage = mocker.MagicMock()\n    mock_storage.driver = mock_driver\n    config.storage = mock_storage\n\n    invoke = BaseInvoke(config, \"test_method\")\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"New question\"}]}\n\n    result = invoke.inject_conversation_messages(kwargs)\n\n    assert config.cache.session_id == 11\n    assert config.cache.conversation_id == 22\n    mock_driver.session.read.assert_called_once_with(\"session-uuid\")\n    mock_driver.conversation.read_id_by_session_id.assert_called_once_with(11)\n    mock_driver.conversation.messages.read.assert_called_once_with(22)\n    assert [m[\"content\"] for m in result[\"messages\"]] == [\n        \"Previous question\",\n        \"Previous answer\",\n        \"New question\",\n    ]\n\n\ndef test_inject_conversation_messages_cloud_fetches_from_cloud(mocker):\n    config = Config()\n    config.cloud = True\n    config.session_id = \"session-uuid\"\n    config.entity_id = \"entity-id\"\n    config.llm.provider = OPENAI_LLM_PROVIDER\n\n    invoke = BaseInvoke(config, \"test_method\")\n    kwargs = {\"messages\": [{\"role\": \"user\", \"content\": \"New question\"}]}\n\n    mocker.patch(\n        \"memori.memory.recall.Recall._cloud_recall\",\n        autospec=True,\n        return_value={\n            \"facts\": [],\n            \"messages\": [\n                {\"role\": \"user\", \"content\": \"cloud previous question\"},\n                {\"role\": \"assistant\", \"content\": \"cloud previous answer\"},\n            ],\n        },\n    )\n\n    kwargs = invoke.inject_recalled_facts(kwargs)\n    result = invoke.inject_conversation_messages(kwargs)\n\n    assert [m[\"content\"] for m in result[\"messages\"]] == [\n        \"cloud previous question\",\n        \"cloud previous answer\",\n        \"New question\",\n    ]\n    assert invoke._injected_message_count == 2\n"
  },
  {
    "path": "tests/llm/test_llm_clients.py",
    "content": "import pytest\n\nfrom memori._config import Config\nfrom memori.llm._clients import (\n    Agno,\n    Anthropic,\n    Google,\n    LangChain,\n    OpenAi,\n    PydanticAi,\n    XAi,\n)\n\n\n@pytest.fixture\ndef config():\n    return Config()\n\n\n@pytest.fixture\ndef anthropic_client(config):\n    return Anthropic(config)\n\n\n@pytest.fixture\ndef google_client(config):\n    return Google(config)\n\n\n@pytest.fixture\ndef openai_client(config):\n    return OpenAi(config)\n\n\n@pytest.fixture\ndef pydantic_client(config):\n    return PydanticAi(config)\n\n\n@pytest.fixture\ndef langchain_client(config):\n    return LangChain(config)\n\n\n@pytest.fixture\ndef xai_client(config):\n    return XAi(config)\n\n\n@pytest.fixture\ndef agno_client(config):\n    return Agno(config)\n\n\ndef test_anthropic_register_adds_memori_wrappers_sync(anthropic_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.messages.create = mocker.MagicMock()\n    mock_client.beta.messages.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = anthropic_client.register(mock_client)\n\n    assert result is anthropic_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client, \"_messages_create\")\n    assert hasattr(mock_client.beta, \"_messages_create\")\n\n\ndef test_anthropic_register_wraps_real_client_and_injects_recall(config, mocker):\n    pytest.importorskip(\"anthropic\")\n\n    from anthropic import Anthropic as AnthropicSdk\n\n    config.cloud = False\n    config.entity_id = \"user-123\"\n    config.storage = mocker.MagicMock()\n    config.storage.driver = mocker.MagicMock()\n    config.storage.driver.entity.create.return_value = 1\n    config.storage.driver.session.read.return_value = None\n    config.storage.driver.session.create.return_value = None\n    config.storage.driver.conversation.create.return_value = None\n    config.storage.driver.conversation.read_id_by_session_id.return_value = None\n    config.storage.driver.conversation.messages.read.return_value = []\n\n    captured_kwargs = {}\n\n    def fake_messages_create(**kwargs):\n        captured_kwargs.update(kwargs)\n        return mocker.MagicMock(content=[])\n\n    client = AnthropicSdk(api_key=\"test-key\")\n    client.messages.create = fake_messages_create\n    client.beta.messages.create = fake_messages_create\n\n    recall_mock = mocker.patch(\n        \"memori.memory.recall.Recall.search_facts\",\n        return_value=[{\"content\": \"User likes tennis\", \"similarity\": 0.9}],\n    )\n    mocker.patch(\"memori.llm._base.BaseInvoke.handle_post_response\")\n\n    anthropic_client = Anthropic(config)\n    anthropic_client.register(client)\n\n    client.messages.create(\n        model=\"claude-3-5-haiku-latest\",\n        max_tokens=16,\n        messages=[{\"role\": \"user\", \"content\": \"What do I like?\"}],\n    )\n\n    assert hasattr(client, \"_messages_create\")\n    assert client._messages_create is fake_messages_create\n    recall_mock.assert_called_once_with(\"What do I like?\", entity_id=1, cloud=False)\n    assert \"system\" in captured_kwargs\n    assert \"User likes tennis\" in captured_kwargs[\"system\"]\n\n\n@pytest.mark.asyncio\nasync def test_anthropic_register_adds_memori_wrappers_async(anthropic_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.messages.create = mocker.MagicMock()\n    mock_client.beta.messages.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    result = anthropic_client.register(mock_client)\n\n    assert result is anthropic_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n\n\ndef test_anthropic_register_skips_if_already_installed(anthropic_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client._memori_installed = True\n    original_create = mock_client.messages.create\n\n    result = anthropic_client.register(mock_client)\n\n    assert result is anthropic_client\n    assert mock_client.messages.create == original_create\n\n\ndef test_anthropic_register_raises_without_messages_attr(anthropic_client, mocker):\n    mock_client = mocker.MagicMock(spec=[])\n\n    with pytest.raises(RuntimeError, match=\"not instance of Anthropic\"):\n        anthropic_client.register(mock_client)\n\n\ndef test_google_register_adds_memori_wrappers(google_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    result = google_client.register(mock_client)\n\n    assert result is google_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client.models, \"actual_generate_content\")\n\n\ndef test_google_register_wraps_real_google_genai_client_and_injects_recall(\n    config, mocker\n):\n    pytest.importorskip(\"google.genai\")\n\n    from google import genai\n    from google.genai.types import Content, Part\n\n    config.cloud = False\n    config.entity_id = \"user-123\"\n    config.storage = mocker.MagicMock()\n    config.storage.driver = mocker.MagicMock()\n    config.storage.driver.entity.create.return_value = 1\n    config.storage.driver.session.read.return_value = None\n    config.storage.driver.session.create.return_value = None\n    config.storage.driver.conversation.create.return_value = None\n    config.storage.driver.conversation.read_id_by_session_id.return_value = None\n    config.storage.driver.conversation.messages.read.return_value = []\n\n    captured_kwargs = {}\n\n    def fake_generate_content(**kwargs):\n        captured_kwargs.update(kwargs)\n        return mocker.MagicMock(candidates=[])\n\n    client = genai.Client(api_key=\"test-key\")\n    client.models.generate_content = fake_generate_content\n\n    recall_mock = mocker.patch(\n        \"memori.memory.recall.Recall.search_facts\",\n        return_value=[{\"content\": \"User likes tennis\", \"similarity\": 0.9}],\n    )\n    mocker.patch(\"memori.llm._base.BaseInvoke.handle_post_response\")\n\n    google_client = Google(config)\n    google_client.register(client)\n\n    client.models.generate_content(\n        model=\"gemini-2.0-flash\",\n        contents=[Content(role=\"user\", parts=[Part(text=\"What do I like?\")])],\n    )\n\n    assert hasattr(client.models, \"actual_generate_content\")\n    assert client.models.actual_generate_content is fake_generate_content\n    recall_mock.assert_called_once_with(\"What do I like?\", entity_id=1, cloud=False)\n    assert \"config\" in captured_kwargs\n    assert \"system_instruction\" in captured_kwargs[\"config\"]\n    assert \"User likes tennis\" in captured_kwargs[\"config\"][\"system_instruction\"]\n\n\ndef test_google_register_skips_if_already_installed(google_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client._memori_installed = True\n    original_generate = mock_client.models.generate_content\n\n    result = google_client.register(mock_client)\n\n    assert result is google_client\n    assert mock_client.models.generate_content == original_generate\n\n\ndef test_google_register_raises_without_models_attr(google_client, mocker):\n    mock_client = mocker.MagicMock(spec=[])\n\n    with pytest.raises(RuntimeError, match=\"not instance of genai.Client\"):\n        google_client.register(mock_client)\n\n\ndef test_openai_register_adds_memori_wrappers_sync(openai_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = openai_client.register(mock_client)\n\n    assert result is openai_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client.chat, \"_completions_create\")\n    assert hasattr(mock_client.beta, \"_chat_completions_parse\")\n\n\ndef test_openai_register_wraps_real_client_and_injects_recall(config, mocker):\n    pytest.importorskip(\"openai\")\n\n    from openai import OpenAI as OpenAISdk\n\n    config.cloud = False\n    config.entity_id = \"user-123\"\n    config.storage = mocker.MagicMock()\n    config.storage.driver = mocker.MagicMock()\n    config.storage.driver.entity.create.return_value = 1\n    config.storage.driver.session.read.return_value = None\n    config.storage.driver.session.create.return_value = None\n    config.storage.driver.conversation.create.return_value = None\n    config.storage.driver.conversation.read_id_by_session_id.return_value = None\n    config.storage.driver.conversation.messages.read.return_value = []\n\n    captured_kwargs = {}\n\n    def fake_chat_completions_create(**kwargs):\n        captured_kwargs.update(kwargs)\n        return mocker.MagicMock(choices=[])\n\n    def fake_chat_completions_parse(**kwargs):\n        return mocker.MagicMock(choices=[], **kwargs)\n\n    client = OpenAISdk(api_key=\"test-key\")\n    client.chat.completions.create = fake_chat_completions_create\n    client.beta.chat.completions.parse = fake_chat_completions_parse\n\n    recall_mock = mocker.patch(\n        \"memori.memory.recall.Recall.search_facts\",\n        return_value=[{\"content\": \"User likes tennis\", \"similarity\": 0.9}],\n    )\n    mocker.patch(\"memori.llm._base.BaseInvoke.handle_post_response\")\n\n    openai_client = OpenAi(config)\n    openai_client.register(client)\n\n    client.chat.completions.create(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"What do I like?\"}],\n    )\n\n    assert hasattr(client.chat, \"_completions_create\")\n    assert client.chat._completions_create is fake_chat_completions_create\n    recall_mock.assert_called_once_with(\"What do I like?\", entity_id=1, cloud=False)\n    assert \"messages\" in captured_kwargs\n    assert captured_kwargs[\"messages\"][0][\"role\"] == \"system\"\n    assert \"User likes tennis\" in captured_kwargs[\"messages\"][0][\"content\"]\n\n\ndef test_openai_register_with_streaming_sync(openai_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = openai_client.register(mock_client, stream=True)\n\n    assert result is openai_client\n    assert mock_client._memori_installed is True\n\n\n@pytest.mark.asyncio\nasync def test_openai_register_adds_memori_wrappers_async(openai_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    result = openai_client.register(mock_client)\n\n    assert result is openai_client\n    assert mock_client._memori_installed is True\n\n\n@pytest.mark.asyncio\nasync def test_openai_register_with_streaming_async(openai_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    result = openai_client.register(mock_client, stream=True)\n\n    assert result is openai_client\n    assert mock_client._memori_installed is True\n\n\ndef test_openai_register_skips_if_already_installed(openai_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client._memori_installed = True\n    original_create = mock_client.chat.completions.create\n\n    result = openai_client.register(mock_client)\n\n    assert result is openai_client\n    assert mock_client.chat.completions.create == original_create\n\n\ndef test_openai_register_raises_without_chat_attr(openai_client, mocker):\n    mock_client = mocker.MagicMock(spec=[])\n\n    with pytest.raises(RuntimeError, match=\"not instance of OpenAI\"):\n        openai_client.register(mock_client)\n\n\ndef test_pydantic_ai_register_adds_memori_wrappers(pydantic_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    result = pydantic_client.register(mock_client)\n\n    assert result is pydantic_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client.chat.completions, \"actual_chat_completions_create\")\n\n\ndef test_pydantic_ai_register_skips_if_already_installed(pydantic_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client._memori_installed = True\n    original_create = mock_client.chat.completions.create\n\n    result = pydantic_client.register(mock_client)\n\n    assert result is pydantic_client\n    assert mock_client.chat.completions.create == original_create\n\n\ndef test_pydantic_ai_register_raises_without_chat_attr(pydantic_client, mocker):\n    mock_client = mocker.MagicMock(spec=[])\n\n    with pytest.raises(RuntimeError, match=\"not instantiated using PydanticAi\"):\n        pydantic_client.register(mock_client)\n\n\ndef test_langchain_register_without_any_client_raises(langchain_client):\n    with pytest.raises(RuntimeError, match=\"called without client\"):\n        langchain_client.register()\n\n\ndef test_langchain_register_chatbedrock(langchain_client, mocker):\n    mock_chatbedrock = mocker.MagicMock()\n    mock_chatbedrock.client.invoke_model = mocker.MagicMock()\n    mock_chatbedrock.client.invoke_model_with_response_stream = mocker.MagicMock()\n    del mock_chatbedrock.client._memori_installed\n\n    result = langchain_client.register(chatbedrock=mock_chatbedrock)\n\n    assert result is langchain_client\n    assert hasattr(mock_chatbedrock.client, \"_memori_installed\")\n    assert mock_chatbedrock.client._memori_installed is True\n    assert hasattr(mock_chatbedrock.client, \"_invoke_model\")\n\n\ndef test_langchain_register_chatgooglegenai(langchain_client, mocker):\n    mock_chatgooglegenai = mocker.MagicMock()\n    mock_chatgooglegenai.client.generate_content = mocker.MagicMock()\n    mock_chatgooglegenai.async_client = None\n    del mock_chatgooglegenai.client._memori_installed\n\n    result = langchain_client.register(chatgooglegenai=mock_chatgooglegenai)\n\n    assert result is langchain_client\n    assert hasattr(mock_chatgooglegenai.client, \"_memori_installed\")\n    assert mock_chatgooglegenai.client._memori_installed is True\n\n\ndef test_langchain_register_chatgooglegenai_with_async_client(langchain_client, mocker):\n    mock_chatgooglegenai = mocker.MagicMock()\n    mock_chatgooglegenai.client.generate_content = mocker.MagicMock()\n    mock_chatgooglegenai.async_client.stream_generate_content = mocker.MagicMock()\n    del mock_chatgooglegenai.client._memori_installed\n\n    result = langchain_client.register(chatgooglegenai=mock_chatgooglegenai)\n\n    assert result is langchain_client\n    assert mock_chatgooglegenai.client._memori_installed is True\n\n\ndef test_langchain_register_chatgooglegenai_new_sdk(langchain_client, mocker):\n    \"\"\"Test LangChain adapter with new google.genai SDK (client.models.generate_content).\"\"\"\n    mock_chatgooglegenai = mocker.MagicMock()\n    # New SDK: client.models.generate_content instead of client.generate_content\n    mock_chatgooglegenai.client.models.generate_content = mocker.MagicMock()\n    mock_chatgooglegenai.async_client = None\n    del mock_chatgooglegenai.client._memori_installed\n    # Remove generate_content from client level to simulate new SDK\n    del mock_chatgooglegenai.client.generate_content\n\n    result = langchain_client.register(chatgooglegenai=mock_chatgooglegenai)\n\n    assert result is langchain_client\n    assert hasattr(mock_chatgooglegenai.client, \"_memori_installed\")\n    assert mock_chatgooglegenai.client._memori_installed is True\n    # Verify the models namespace was wrapped\n    assert hasattr(mock_chatgooglegenai.client.models, \"_generate_content\")\n\n\ndef test_langchain_register_chatgooglegenai_new_sdk_with_async(\n    langchain_client, mocker\n):\n    \"\"\"Test LangChain adapter with new google.genai SDK including async client.\"\"\"\n    mock_chatgooglegenai = mocker.MagicMock()\n    # New SDK structure\n    mock_chatgooglegenai.client.models.generate_content = mocker.MagicMock()\n    mock_chatgooglegenai.client.models.generate_content_stream = mocker.MagicMock()\n    mock_chatgooglegenai.async_client.models.generate_content = mocker.MagicMock()\n    mock_chatgooglegenai.async_client.models.generate_content_stream = (\n        mocker.MagicMock()\n    )\n    del mock_chatgooglegenai.client._memori_installed\n    # Remove generate_content from client level to simulate new SDK\n    del mock_chatgooglegenai.client.generate_content\n\n    result = langchain_client.register(chatgooglegenai=mock_chatgooglegenai)\n\n    assert result is langchain_client\n    assert mock_chatgooglegenai.client._memori_installed is True\n    # Verify both sync and async models were wrapped\n    assert hasattr(mock_chatgooglegenai.client.models, \"_generate_content\")\n    assert hasattr(mock_chatgooglegenai.async_client.models, \"_generate_content\")\n\n\ndef test_langchain_register_chatopenai(langchain_client, mocker):\n    mock_chatopenai = mocker.MagicMock()\n    mock_chatopenai.http_client = None\n    mock_chatopenai.async_http_client = None\n    mock_chatopenai.client._client.beta.chat.completions.create = mocker.MagicMock()\n    mock_chatopenai.client._client.beta.chat.completions.parse = mocker.MagicMock()\n    mock_chatopenai.client._client.chat.completions.create = mocker.MagicMock()\n    mock_chatopenai.client._client.chat.completions.parse = mocker.MagicMock()\n    del mock_chatopenai.client._client._memori_installed\n\n    mock_chatopenai.async_client._client.beta.chat.completions.create = (\n        mocker.MagicMock()\n    )\n    mock_chatopenai.async_client._client.beta.chat.completions.parse = (\n        mocker.MagicMock()\n    )\n    mock_chatopenai.async_client._client.chat.completions.create = mocker.MagicMock()\n    mock_chatopenai.async_client._client.chat.completions.parse = mocker.MagicMock()\n    del mock_chatopenai.async_client._client._memori_installed\n\n    result = langchain_client.register(chatopenai=mock_chatopenai)\n\n    assert result is langchain_client\n    assert mock_chatopenai.client._client._memori_installed is True\n    assert mock_chatopenai.async_client._client._memori_installed is True\n\n\ndef test_langchain_register_chatvertexai(langchain_client, mocker):\n    mock_chatvertexai = mocker.MagicMock()\n    mock_chatvertexai.prediction_client.generate_content = mocker.MagicMock()\n    del mock_chatvertexai.prediction_client._memori_installed\n\n    result = langchain_client.register(chatvertexai=mock_chatvertexai)\n\n    assert result is langchain_client\n    assert hasattr(mock_chatvertexai.prediction_client, \"_memori_installed\")\n    assert mock_chatvertexai.prediction_client._memori_installed is True\n\n\ndef test_langchain_register_chatbedrock_raises_without_client_attr(\n    langchain_client, mocker\n):\n    mock_chatbedrock = mocker.MagicMock(spec=[])\n\n    with pytest.raises(RuntimeError, match=\"not instance of ChatBedrock\"):\n        langchain_client.register(chatbedrock=mock_chatbedrock)\n\n\ndef test_langchain_register_chatgooglegenai_raises_without_client_attr(\n    langchain_client, mocker\n):\n    mock_chatgooglegenai = mocker.MagicMock(spec=[])\n\n    with pytest.raises(RuntimeError, match=\"not instance of ChatGoogleGenerativeAI\"):\n        langchain_client.register(chatgooglegenai=mock_chatgooglegenai)\n\n\ndef test_langchain_register_chatopenai_raises_without_client_attrs(\n    langchain_client, mocker\n):\n    mock_chatopenai = mocker.MagicMock(spec=[\"client\"])\n\n    with pytest.raises(RuntimeError, match=\"not instance of ChatOpenAI\"):\n        langchain_client.register(chatopenai=mock_chatopenai)\n\n\ndef test_langchain_register_chatvertexai_raises_without_prediction_client(\n    langchain_client, mocker\n):\n    mock_chatvertexai = mocker.MagicMock(spec=[])\n\n    with pytest.raises(RuntimeError, match=\"not instance of ChatVertexAI\"):\n        langchain_client.register(chatvertexai=mock_chatvertexai)\n\n\ndef test_xai_register_adds_memori_wrappers(xai_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    result = xai_client.register(mock_client)\n\n    assert result is xai_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client.chat, \"_create\")\n\n\ndef test_xai_register_skips_if_already_installed(xai_client, mocker):\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client._memori_installed = True\n    original_create = mock_client.chat.create\n\n    result = xai_client.register(mock_client)\n\n    assert result is xai_client\n    assert mock_client.chat.create == original_create\n\n\ndef test_xai_register_raises_without_chat_attr(xai_client, mocker):\n    mock_client = mocker.MagicMock(spec=[])\n\n    with pytest.raises(RuntimeError, match=\"not instance of xAI\"):\n        xai_client.register(mock_client)\n\n\ndef test_agno_register_openai_chat_sync(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.openai\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_model.get_client.return_value = mock_client\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = agno_client.register(openai_chat=mock_model)\n\n    assert result is agno_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client.chat, \"_completions_create\")\n    assert hasattr(mock_client.beta, \"_chat_completions_parse\")\n\n\n@pytest.mark.asyncio\nasync def test_agno_register_openai_chat_async(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.openai\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_model.get_client.return_value = mock_client\n\n    result = agno_client.register(openai_chat=mock_model)\n\n    assert result is agno_client\n    assert mock_client._memori_installed is True\n\n\ndef test_agno_register_claude_sync(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.anthropic\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.messages.create = mocker.MagicMock()\n    mock_client.beta.messages.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_model.get_client.return_value = mock_client\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = agno_client.register(claude=mock_model)\n\n    assert result is agno_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client, \"_messages_create\")\n    assert hasattr(mock_client.beta, \"_messages_create\")\n\n\ndef test_agno_register_gemini_sync(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.google\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.aio\n\n    mock_model.get_client.return_value = mock_client\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = agno_client.register(gemini=mock_model)\n\n    assert result is agno_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client.models, \"actual_generate_content\")\n\n\n@pytest.mark.asyncio\nasync def test_agno_register_gemini_async(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.google\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.aio\n\n    mock_model.get_client.return_value = mock_client\n\n    result = agno_client.register(gemini=mock_model)\n\n    assert result is agno_client\n    assert mock_client._memori_installed is True\n\n\ndef test_agno_register_skips_if_already_installed(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.openai\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client._memori_installed = True\n    original_create = mock_client.chat.completions.create\n\n    mock_model.get_client.return_value = mock_client\n\n    result = agno_client.register(openai_chat=mock_model)\n\n    assert result is agno_client\n    assert mock_client.chat.completions.create == original_create\n\n\ndef test_agno_register_raises_without_models(agno_client):\n    with pytest.raises(RuntimeError, match=\"Agno::register called without model\"):\n        agno_client.register()\n\n\ndef test_agno_register_raises_with_invalid_openai_model(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"invalid.module\"\n\n    with pytest.raises(\n        RuntimeError, match=\"not instance of agno.models.openai.OpenAIChat\"\n    ):\n        agno_client.register(openai_chat=mock_model)\n\n\ndef test_agno_register_raises_with_invalid_gemini_model(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"invalid.module\"\n\n    with pytest.raises(RuntimeError, match=\"not instance of agno.models.google.Gemini\"):\n        agno_client.register(gemini=mock_model)\n\n\ndef test_agno_register_xai_sync(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.xai\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_model.get_client.return_value = mock_client\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = agno_client.register(xai=mock_model)\n\n    assert result is agno_client\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n    assert hasattr(mock_client.chat, \"_completions_create\")\n    assert hasattr(mock_client.beta, \"_chat_completions_parse\")\n\n\n@pytest.mark.asyncio\nasync def test_agno_register_xai_async(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.xai\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_model.get_client.return_value = mock_client\n\n    result = agno_client.register(xai=mock_model)\n\n    assert result is agno_client\n    assert mock_client._memori_installed is True\n\n\ndef test_agno_register_raises_with_invalid_xai_model(agno_client, mocker):\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"invalid.module\"\n\n    with pytest.raises(RuntimeError, match=\"not instance of agno.models.xai.xAI\"):\n        agno_client.register(xai=mock_model)\n"
  },
  {
    "path": "tests/llm/test_llm_deprecation_warnings.py",
    "content": "import pytest\n\nfrom memori import Memori\n\n\n@pytest.fixture\ndef memori_instance(mocker):\n    \"\"\"Create a Memori instance with mocked storage.\"\"\"\n    mock_conn = mocker.MagicMock()\n    mocker.patch(\"memori.storage.Manager.start\", return_value=mocker.MagicMock())\n    mocker.patch(\n        \"memori.memory.augmentation.Manager.start\", return_value=mocker.MagicMock()\n    )\n    return Memori(conn=mock_conn)\n\n\ndef test_openai_register_shows_deprecation_warning(memori_instance, mocker):\n    \"\"\"Test that memori.openai.register() shows deprecation warning.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.base_url\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    with pytest.warns(\n        DeprecationWarning, match=\"memori.openai.register\\\\(\\\\) is deprecated\"\n    ):\n        memori_instance.openai.register(mock_client)\n\n\ndef test_anthropic_register_shows_deprecation_warning(memori_instance, mocker):\n    \"\"\"Test that memori.anthropic.register() shows deprecation warning.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.messages.create = mocker.MagicMock()\n    mock_client.beta.messages.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_anthropic_module = mocker.MagicMock()\n    mock_anthropic_module.__version__ = \"0.75.0\"\n    mocker.patch.dict(\"sys.modules\", {\"anthropic\": mock_anthropic_module})\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    with pytest.warns(\n        DeprecationWarning, match=\"memori.anthropic.register\\\\(\\\\) is deprecated\"\n    ):\n        memori_instance.anthropic.register(mock_client)\n\n\ndef test_google_register_shows_deprecation_warning(memori_instance, mocker):\n    \"\"\"Test that memori.google.register() shows deprecation warning.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.aio\n\n    mock_genai_module = mocker.MagicMock()\n    mock_genai_module.__version__ = \"1.52.0\"\n\n    mock_google_module = mocker.MagicMock()\n    mock_google_module.genai = mock_genai_module\n\n    mocker.patch.dict(\n        \"sys.modules\", {\"google\": mock_google_module, \"google.genai\": mock_genai_module}\n    )\n\n    with pytest.warns(\n        DeprecationWarning, match=\"memori.google.register\\\\(\\\\) is deprecated\"\n    ):\n        memori_instance.google.register(mock_client)\n\n\ndef test_xai_register_shows_deprecation_warning(memori_instance, mocker):\n    \"\"\"Test that memori.xai.register() shows deprecation warning.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.chat.create = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.chat.completions\n\n    mock_xai_sdk_module = mocker.MagicMock()\n    mock_xai_sdk_module.__version__ = \"1.4.1\"\n    mocker.patch.dict(\"sys.modules\", {\"xai_sdk\": mock_xai_sdk_module})\n\n    with pytest.warns(\n        DeprecationWarning, match=\"memori.xai.register\\\\(\\\\) is deprecated\"\n    ):\n        memori_instance.xai.register(mock_client)\n\n\ndef test_pydantic_ai_register_shows_deprecation_warning(memori_instance, mocker):\n    \"\"\"Test that memori.pydantic_ai.register() shows deprecation warning.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    with pytest.warns(\n        DeprecationWarning, match=\"memori.pydantic_ai.register\\\\(\\\\) is deprecated\"\n    ):\n        memori_instance.pydantic_ai.register(mock_client)\n\n\ndef test_langchain_register_shows_deprecation_warning(memori_instance, mocker):\n    \"\"\"Test that memori.langchain.register() shows deprecation warning.\"\"\"\n    mock_chatbedrock = mocker.MagicMock()\n    mock_chatbedrock.client.invoke_model = mocker.MagicMock()\n    mock_chatbedrock.client.invoke_model_with_response_stream = mocker.MagicMock()\n    del mock_chatbedrock.client._memori_installed\n\n    with pytest.warns(\n        DeprecationWarning, match=\"memori.langchain.register\\\\(\\\\) is deprecated\"\n    ):\n        memori_instance.langchain.register(chatbedrock=mock_chatbedrock)\n\n\ndef test_agno_register_shows_deprecation_warning(memori_instance, mocker):\n    \"\"\"Test that memori.agno.register() shows deprecation warning.\"\"\"\n    mock_model = mocker.MagicMock()\n    type(mock_model).__module__ = \"agno.models.openai\"\n\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_model.get_client.return_value = mock_client\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    with pytest.warns(\n        DeprecationWarning, match=\"memori.agno.register\\\\(\\\\) is deprecated\"\n    ):\n        memori_instance.agno.register(openai_chat=mock_model)\n\n\ndef test_llm_register_no_deprecation_warning(memori_instance, mocker):\n    \"\"\"Test that memori.llm.register() does NOT show deprecation warning.\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"openai\"\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.base_url\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    # This should NOT raise a DeprecationWarning\n    import warnings\n\n    with warnings.catch_warnings(record=True) as warning_list:\n        warnings.simplefilter(\"always\")\n        memori_instance.llm.register(mock_client)\n\n    # Filter for deprecation warnings related to register methods\n    deprecation_warnings = [\n        w\n        for w in warning_list\n        if issubclass(w.category, DeprecationWarning) and \"register()\" in str(w.message)\n    ]\n    assert len(deprecation_warnings) == 0, (\n        \"llm.register() should not emit deprecation warnings\"\n    )\n"
  },
  {
    "path": "tests/llm/test_llm_embeddings.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport struct\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nimport pytest\n\nimport memori.embeddings._sentence_transformers as st_core\nfrom memori._config import Config\nfrom memori.embeddings import TEI, embed_texts, format_embedding_for_db\n\n\ndef test_format_embedding_for_db_mysql():\n    embedding = [1.0, 2.0, 3.0]\n    result = format_embedding_for_db(embedding, \"mysql\")\n    assert isinstance(result, bytes)\n    unpacked = struct.unpack(\"<3f\", result)\n    assert list(unpacked) == pytest.approx(embedding)\n\n\ndef test_format_embedding_for_db_postgresql():\n    embedding = [1.0, 2.0, 3.0]\n    result = format_embedding_for_db(embedding, \"postgresql\")\n    assert isinstance(result, bytes)\n    unpacked = struct.unpack(\"<3f\", result)\n    assert list(unpacked) == pytest.approx(embedding)\n\n\ndef test_format_embedding_for_db_cockroachdb():\n    embedding = [1.0, 2.0, 3.0]\n    result = format_embedding_for_db(embedding, \"cockroachdb\")\n    assert isinstance(result, bytes)\n    unpacked = struct.unpack(\"<3f\", result)\n    assert list(unpacked) == pytest.approx(embedding)\n\n\ndef test_format_embedding_for_db_sqlite():\n    embedding = [1.0, 2.0, 3.0]\n    result = format_embedding_for_db(embedding, \"sqlite\")\n    assert isinstance(result, bytes)\n    unpacked = struct.unpack(\"<3f\", result)\n    assert list(unpacked) == pytest.approx(embedding)\n\n\ndef test_format_embedding_for_db_mongodb(mocker):\n    embedding = [1.0, 2.0, 3.0]\n    # Mock bson.Binary to test MongoDB path\n    mock_bson = mocker.MagicMock()\n    mock_binary = mocker.MagicMock()\n    mock_bson.Binary.return_value = mock_binary\n    mocker.patch.dict(\"sys.modules\", {\"bson\": mock_bson})\n\n    result = format_embedding_for_db(embedding, \"mongodb\")\n    # Should return bson.Binary wrapped bytes\n    assert result == mock_binary\n    # Verify bson.Binary was called with packed bytes\n    mock_bson.Binary.assert_called_once()\n    call_args = mock_bson.Binary.call_args[0][0]\n    assert isinstance(call_args, bytes)\n    unpacked = struct.unpack(\"<3f\", call_args)\n    assert list(unpacked) == pytest.approx(embedding)\n\n\ndef test_format_embedding_for_db_mongodb_no_bson():\n    \"\"\"Test MongoDB fallback when bson is not available.\"\"\"\n    embedding = [1.0, 2.0, 3.0]\n    # Don't mock bson, let ImportError happen\n    result = format_embedding_for_db(embedding, \"mongodb\")\n    # Should return raw bytes as fallback\n    assert isinstance(result, bytes)\n    unpacked = struct.unpack(\"<3f\", result)\n    assert list(unpacked) == pytest.approx(embedding)\n\n\ndef test_format_embedding_for_db_oceanbase_uses_pyobvector(mocker):\n    embedding = [1.0, 2.0, 3.0]\n    mock_vector = mocker.MagicMock()\n    mock_vector._to_db.return_value = \"vector-bytes\"\n    mock_util = mocker.MagicMock(Vector=mock_vector)\n    mock_pkg = mocker.MagicMock(util=mock_util)\n    mocker.patch.dict(\n        \"sys.modules\", {\"pyobvector\": mock_pkg, \"pyobvector.util\": mock_util}\n    )\n\n    result = format_embedding_for_db(embedding, \"oceanbase\")\n\n    assert result == \"vector-bytes\"\n    mock_vector._to_db.assert_called_once_with(embedding)\n\n\ndef test_format_embedding_for_db_unknown_dialect():\n    embedding = [1.0, 2.0, 3.0]\n    result = format_embedding_for_db(embedding, \"unknown_db\")\n    assert isinstance(result, bytes)\n    unpacked = struct.unpack(\"<3f\", result)\n    assert list(unpacked) == pytest.approx(embedding)\n\n\ndef test_format_embedding_for_db_high_dimensional():\n    embedding = [float(i) for i in range(768)]\n    result_mysql = format_embedding_for_db(embedding, \"mysql\")\n    assert isinstance(result_mysql, bytes)\n    unpacked_mysql = struct.unpack(\"<768f\", result_mysql)\n    assert list(unpacked_mysql) == pytest.approx(embedding)\n\n    result_postgres = format_embedding_for_db(embedding, \"postgresql\")\n    assert isinstance(result_postgres, bytes)\n    unpacked_postgres = struct.unpack(\"<768f\", result_postgres)\n    assert list(unpacked_postgres) == pytest.approx(embedding)\n\n\ndef test_get_model_caches_model():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformer\"\n    ) as mock_transformer:\n        mock_model = Mock()\n        mock_transformer.return_value = mock_model\n\n        embedder = st_core.get_sentence_transformers_embedder(\"test-model\")\n        model1 = embedder._get_model()\n        model2 = embedder._get_model()\n\n        assert model1 is model2\n        mock_transformer.assert_called_once_with(\"test-model\")\n\n\ndef test_get_model_different_models():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformer\"\n    ) as mock_transformer:\n        mock_model_1 = Mock()\n        mock_model_2 = Mock()\n        mock_transformer.side_effect = [mock_model_1, mock_model_2]\n\n        model1 = st_core.get_sentence_transformers_embedder(\"model-1\")._get_model()\n        model2 = st_core.get_sentence_transformers_embedder(\"model-2\")._get_model()\n\n        assert model1 is not model2\n        assert mock_transformer.call_count == 2\n\n\ndef test_embed_texts_single_string():\n    cfg = Config()\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_model = Mock()\n        mock_embeddings = np.array([[0.1, 0.2, 0.3]])\n        mock_model.encode.return_value = mock_embeddings\n        mock_get_model.return_value = mock_model\n\n        result = embed_texts(\n            \"Hello world\",\n            model=cfg.embeddings.model,\n        )\n\n        assert len(result) == 1\n        assert result[0] == pytest.approx([0.1, 0.2, 0.3])\n        mock_model.encode.assert_called_once()\n\n\ndef test_embed_texts_list_of_strings():\n    cfg = Config()\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_model = Mock()\n        mock_embeddings = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])\n        mock_model.encode.return_value = mock_embeddings\n        mock_get_model.return_value = mock_model\n\n        result = embed_texts(\n            [\"Hello\", \"World\"],\n            model=cfg.embeddings.model,\n        )\n\n        assert len(result) == 2\n        assert result[0] == pytest.approx([0.1, 0.2, 0.3])\n        assert result[1] == pytest.approx([0.4, 0.5, 0.6])\n\n\ndef test_embed_texts_empty_list():\n    cfg = Config()\n    st_core._EMBEDDER_CACHE.clear()\n    result = embed_texts(\n        [],\n        model=cfg.embeddings.model,\n    )\n    assert result == []\n\n\ndef test_embed_texts_empty_string():\n    cfg = Config()\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_model = Mock()\n        mock_embeddings = np.array([[0.1, 0.2, 0.3]])\n        mock_model.encode.return_value = mock_embeddings\n        mock_get_model.return_value = mock_model\n\n        result = embed_texts(\n            \"\",\n            model=cfg.embeddings.model,\n        )\n\n        assert len(result) == 1\n        mock_model.encode.assert_called_once_with(\n            [\"\"], convert_to_numpy=True, normalize_embeddings=True\n        )\n\n\ndef test_embed_texts_filters_empty_strings():\n    cfg = Config()\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_model = Mock()\n        mock_embeddings = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])\n        mock_model.encode.return_value = mock_embeddings\n        mock_get_model.return_value = mock_model\n\n        result = embed_texts(\n            [\"Hello\", \"\", \"World\", \"\"],\n            model=cfg.embeddings.model,\n        )\n\n        assert len(result) == 2\n        mock_model.encode.assert_called_once_with(\n            [\"Hello\", \"World\"], convert_to_numpy=True, normalize_embeddings=True\n        )\n\n\ndef test_embed_texts_chunks_long_text_and_pools(mocker):\n    st_core._EMBEDDER_CACHE.clear()\n    mock_model = mocker.Mock()\n    mock_model.get_max_seq_length.return_value = 6\n    mock_model.tokenizer = mocker.Mock()\n    mock_model.tokenizer.return_value = {\"input_ids\": list(range(20))}\n    mock_model.tokenizer.decode.side_effect = [\"c1\", \"c2\", \"c3\", \"c4\", \"c5\"]\n\n    # Chunk embeddings: unit vectors along two axes to make mean+renorm predictable.\n    mock_model.encode.return_value = np.array(\n        [[1.0, 0.0], [0.0, 1.0], [1.0, 0.0], [0.0, 1.0], [1.0, 0.0]],\n        dtype=np.float32,\n    )\n\n    mocker.patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\",\n        return_value=mock_model,\n    )\n\n    out = embed_texts([\"x\" * 10], model=\"test-model\")\n\n    assert len(out) == 1\n    # Mean is [0.6, 0.4], then renormalized to unit length.\n    assert out[0] == pytest.approx([0.832050, 0.554700], rel=1e-4)\n    mock_model.encode.assert_called_with(\n        [\"c1\", \"c2\", \"c3\", \"c4\", \"c5\"],\n        convert_to_numpy=True,\n        normalize_embeddings=True,\n    )\n\n\ndef test_embed_texts_custom_model():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\"memori.embeddings._api.get_sentence_transformers_embedder\") as mock_get:\n        mock_embedder = Mock()\n        mock_embedder.embed.return_value = [[0.1, 0.2, 0.3]]\n        mock_get.return_value = mock_embedder\n\n        result = embed_texts(\"test\", model=\"custom-model\")\n\n        mock_get.assert_called_once_with(\"custom-model\")\n        mock_embedder.embed.assert_called_once_with([\"test\"], fallback_dimension=768)\n        assert len(result) == 1\n\n\ndef test_embed_texts_model_load_failure():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_get_model.side_effect = OSError(\"Model not found\")\n\n        result = embed_texts([\"Hello\", \"World\"], model=\"missing-model\")\n\n        assert len(result) == 2\n        assert len(result[0]) == 768\n        assert len(result[1]) == 768\n        assert set(result[0]) == {0.0}\n        assert set(result[1]) == {0.0}\n\n\ndef test_embed_texts_encode_failure():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_model = Mock()\n        mock_model.encode.side_effect = RuntimeError(\"Encoding failed\")\n        mock_model.get_sentence_embedding_dimension.return_value = 384\n        mock_get_model.return_value = mock_model\n\n        result = embed_texts([\"Hello\"], model=\"test-model\")\n\n        assert len(result) == 1\n        assert result[0] == [0.0] * 384\n\n\ndef test_embed_texts_shape_error_retries_and_pools(mocker):\n    st_core._EMBEDDER_CACHE.clear()\n    mock_model = mocker.Mock()\n    # First call (convert_to_numpy=True) fails like numpy stack error\n    # Then we retry per-text (convert_to_numpy=True) which succeeds.\n    mock_model.encode.side_effect = [\n        ValueError(\"all input arrays must have the same shape\"),\n        np.ones((1, 4), dtype=np.float32),\n        np.zeros((1, 4), dtype=np.float32),\n    ]\n    mock_model.get_sentence_embedding_dimension.return_value = 4\n    mocker.patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\",\n        return_value=mock_model,\n    )\n\n    out = embed_texts([\"a\", \"b\"], model=\"test-model\")\n\n    assert out == [[1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0]]\n    assert mock_model.encode.call_count == 3\n\n\ndef test_embed_texts_encode_failure_with_dimension_error():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_model = Mock()\n        mock_model.encode.side_effect = RuntimeError(\"Encoding failed\")\n        mock_model.get_sentence_embedding_dimension.side_effect = RuntimeError(\n            \"Dimension error\"\n        )\n        mock_get_model.return_value = mock_model\n\n        result = embed_texts([\"Hello\"], model=\"test-model\")\n\n        assert len(result) == 1\n        assert len(result[0]) == 768\n        assert set(result[0]) == {0.0}\n\n\ndef test_embed_texts_model_load_runtime_error():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_get_model.side_effect = RuntimeError(\"Runtime error\")\n\n        result = embed_texts(\"test\", model=\"test-model\")\n\n        assert len(result) == 1\n        assert len(result[0]) == 768\n        assert set(result[0]) == {0.0}\n\n\ndef test_embed_texts_model_load_value_error():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformersEmbedder._get_model\"\n    ) as mock_get_model:\n        mock_get_model.side_effect = ValueError(\"Value error\")\n\n        result = embed_texts(\"test\", model=\"test-model\")\n\n        assert len(result) == 1\n        assert len(result[0]) == 768\n        assert set(result[0]) == {0.0}\n\n\n@pytest.mark.asyncio\nasync def test_embed_texts_async_single_string():\n    cfg = Config()\n    mock_result = [[0.1, 0.2, 0.3]]\n\n    async def mock_run_in_executor(executor, func, *args):\n        return mock_result\n\n    with patch(\"asyncio.get_event_loop\") as mock_loop:\n        mock_loop.return_value.run_in_executor = mock_run_in_executor\n\n        result = await embed_texts(\n            \"Hello world\",\n            model=cfg.embeddings.model,\n            async_=True,\n        )\n\n        assert len(result) == 1\n        assert result[0] == pytest.approx([0.1, 0.2, 0.3])\n\n\n@pytest.mark.asyncio\nasync def test_embed_texts_async_list():\n    cfg = Config()\n    mock_result = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]\n\n    async def mock_run_in_executor(executor, func, *args):\n        return mock_result\n\n    with patch(\"asyncio.get_event_loop\") as mock_loop:\n        mock_loop.return_value.run_in_executor = mock_run_in_executor\n\n        result = await embed_texts(\n            [\"Hello\", \"World\"],\n            model=cfg.embeddings.model,\n            async_=True,\n        )\n\n        assert len(result) == 2\n        assert result[0] == pytest.approx([0.1, 0.2, 0.3])\n        assert result[1] == pytest.approx([0.4, 0.5, 0.6])\n\n\n@pytest.mark.asyncio\nasync def test_embed_texts_async_custom_model():\n    mock_result = [[0.1, 0.2, 0.3]]\n\n    async def mock_run_in_executor(executor, func, *args):\n        return mock_result\n\n    with patch(\"asyncio.get_event_loop\") as mock_loop:\n        mock_loop.return_value.run_in_executor = mock_run_in_executor\n\n        result = await embed_texts(\"test\", model=\"custom-model\", async_=True)\n\n        assert len(result) == 1\n        assert result[0] == pytest.approx([0.1, 0.2, 0.3])\n\n\ndef test_embed_texts_uses_tei_remote(mocker):\n    tei = TEI(url=\"http://localhost:8080/v1/embeddings\")\n    mock_post = mocker.patch(\"memori.embeddings._tei.requests.post\")\n    mock_response = mocker.Mock()\n    mock_response.json.side_effect = [\n        {\"data\": [{\"embedding\": [1.0, 0.0]}]},\n        {\"data\": [{\"embedding\": [0.0, 1.0]}]},\n    ]\n    mock_response.raise_for_status.return_value = None\n    mock_post.return_value = mock_response\n\n    out = embed_texts([\"a\", \"b\"], model=\"tei-model\", tei=tei)\n\n    assert out == [[1.0, 0.0], [0.0, 1.0]]\n    assert mock_post.call_count == 2\n    first_kwargs = mock_post.call_args_list[0].kwargs\n    second_kwargs = mock_post.call_args_list[1].kwargs\n    assert first_kwargs[\"json\"] == {\"input\": [\"a\"], \"model\": \"tei-model\"}\n    assert second_kwargs[\"json\"] == {\"input\": [\"b\"], \"model\": \"tei-model\"}\n    assert first_kwargs[\"timeout\"] == 30.0\n    assert second_kwargs[\"timeout\"] == 30.0\n\n\ndef test_embed_texts_tei_token_chunks_and_pools(mocker):\n    tei = TEI(url=\"http://localhost:8080/v1/embeddings\")\n\n    tokenizer = mocker.Mock()\n    tokenizer.return_value = {\"input_ids\": [[0, 1, 2, 3]]}\n    tokenizer.decode.side_effect = [\"c1\", \"c2\"]\n\n    mock_post = mocker.patch(\"memori.embeddings._tei.requests.post\")\n    mock_response = mocker.Mock()\n    mock_response.raise_for_status.return_value = None\n    # Two chunks -> mean([1,0],[0,1]) renorm => [0.707..., 0.707...]\n    mock_response.json.return_value = {\n        \"data\": [{\"embedding\": [1.0, 0.0]}, {\"embedding\": [0.0, 1.0]}]\n    }\n    mock_post.return_value = mock_response\n\n    out = embed_texts(\n        \"abcd\", model=\"tei-model\", tei=tei, tokenizer=tokenizer, chunk_size=2\n    )\n\n    assert len(out) == 1\n    assert out[0] == pytest.approx([0.707106, 0.707106], rel=1e-5)\n    _, kwargs = mock_post.call_args\n    assert kwargs[\"json\"] == {\"input\": [\"c1\", \"c2\"], \"model\": \"tei-model\"}\n    assert kwargs[\"timeout\"] == 30.0\n"
  },
  {
    "path": "tests/llm/test_llm_embeddings_bundled.py",
    "content": "from unittest.mock import Mock, patch\n\nimport memori.embeddings._sentence_transformers as st_core\n\n\ndef test_get_model_downloads_from_huggingface():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformer\"\n    ) as mock_transformer:\n        mock_model = Mock()\n        mock_transformer.return_value = mock_model\n\n        result = st_core.get_sentence_transformers_embedder(\n            \"all-mpnet-base-v2\"\n        )._get_model()\n\n        assert result is mock_model\n        mock_transformer.assert_called_once_with(\"all-mpnet-base-v2\")\n\n\ndef test_get_model_caching():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformer\"\n    ) as mock_transformer:\n        mock_model = Mock()\n        mock_transformer.return_value = mock_model\n\n        embedder = st_core.get_sentence_transformers_embedder(\"test-model\")\n        result1 = embedder._get_model()\n        result2 = embedder._get_model()\n\n        assert result1 is result2\n        mock_transformer.assert_called_once()\n\n\ndef test_get_model_different_models():\n    st_core._EMBEDDER_CACHE.clear()\n    with patch(\n        \"memori.embeddings._sentence_transformers.SentenceTransformer\"\n    ) as mock_transformer:\n        mock_model1 = Mock()\n        mock_model2 = Mock()\n        mock_transformer.side_effect = [mock_model1, mock_model2]\n\n        result1 = st_core.get_sentence_transformers_embedder(\"model-1\")._get_model()\n        result2 = st_core.get_sentence_transformers_embedder(\"model-2\")._get_model()\n\n        assert result1 is not result2\n        assert mock_transformer.call_count == 2\n"
  },
  {
    "path": "tests/llm/test_llm_provider_sdk_version.py",
    "content": "import pytest\n\nfrom memori._config import Config\nfrom memori.llm._clients import Anthropic, Google, OpenAi, XAi\n\n\n@pytest.fixture\ndef config():\n    return Config()\n\n\n@pytest.fixture\ndef anthropic_client(config):\n    return Anthropic(config)\n\n\n@pytest.fixture\ndef google_client(config):\n    return Google(config)\n\n\n@pytest.fixture\ndef openai_client(config):\n    return OpenAi(config)\n\n\n@pytest.fixture\ndef xai_client(config):\n    return XAi(config)\n\n\ndef test_anthropic_captures_provider_sdk_version(anthropic_client, mocker):\n    \"\"\"Test that Anthropic client captures provider_sdk_version from anthropic module.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.messages.create = mocker.MagicMock()\n    mock_client.beta.messages.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_anthropic_module = mocker.MagicMock()\n    mock_anthropic_module.__version__ = \"0.75.0\"\n    mocker.patch.dict(\"sys.modules\", {\"anthropic\": mock_anthropic_module})\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    anthropic_client.register(mock_client)\n\n    assert anthropic_client.config.llm.provider_sdk_version == \"0.75.0\"\n\n\ndef test_anthropic_handles_missing_version_gracefully(anthropic_client, mocker):\n    \"\"\"Test that Anthropic client handles missing __version__ gracefully.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.messages.create = mocker.MagicMock()\n    mock_client.beta.messages.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_anthropic_module = mocker.MagicMock(spec=[])\n    del mock_anthropic_module.__version__\n    mocker.patch.dict(\"sys.modules\", {\"anthropic\": mock_anthropic_module})\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    anthropic_client.register(mock_client)\n\n    assert anthropic_client.config.llm.provider_sdk_version is None\n\n\ndef test_google_captures_provider_sdk_version(google_client, mocker):\n    \"\"\"Test that Google client captures provider_sdk_version from google.genai module.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.aio\n\n    mock_genai_module = mocker.MagicMock()\n    mock_genai_module.__version__ = \"1.52.0\"\n\n    mock_google_module = mocker.MagicMock()\n    mock_google_module.genai = mock_genai_module\n\n    mocker.patch.dict(\n        \"sys.modules\", {\"google\": mock_google_module, \"google.genai\": mock_genai_module}\n    )\n\n    google_client.register(mock_client)\n\n    assert google_client.config.llm.provider_sdk_version == \"1.52.0\"\n\n\ndef test_google_falls_back_to_importlib_metadata(google_client, mocker):\n    \"\"\"Test that Google client falls back to importlib.metadata.version.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.aio\n\n    mock_genai_module = mocker.MagicMock(spec=[])\n    del mock_genai_module.__version__\n\n    mock_google_module = mocker.MagicMock()\n    mock_google_module.genai = mock_genai_module\n\n    mocker.patch.dict(\n        \"sys.modules\", {\"google\": mock_google_module, \"google.genai\": mock_genai_module}\n    )\n\n    mock_version = mocker.patch(\"importlib.metadata.version\", return_value=\"1.52.0\")\n\n    google_client.register(mock_client)\n\n    mock_version.assert_called_once_with(\"google-genai\")\n    assert google_client.config.llm.provider_sdk_version == \"1.52.0\"\n\n\ndef test_google_handles_missing_version_gracefully(google_client, mocker):\n    \"\"\"Test that Google client handles missing version gracefully.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.aio\n\n    mock_genai_module = mocker.MagicMock(spec=[])\n    del mock_genai_module.__version__\n\n    mock_google_module = mocker.MagicMock()\n    mock_google_module.genai = mock_genai_module\n\n    mocker.patch.dict(\n        \"sys.modules\", {\"google\": mock_google_module, \"google.genai\": mock_genai_module}\n    )\n    mocker.patch(\"importlib.metadata.version\", side_effect=Exception)\n\n    google_client.register(mock_client)\n\n    assert google_client.config.llm.provider_sdk_version is None\n\n\ndef test_openai_captures_provider_sdk_version_from_client(openai_client, mocker):\n    \"\"\"Test that OpenAI client captures provider_sdk_version from client._version.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"2.8.1\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.base_url\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    openai_client.register(mock_client)\n\n    assert openai_client.config.llm.provider_sdk_version == \"2.8.1\"\n\n\ndef test_xai_captures_provider_sdk_version(xai_client, mocker):\n    \"\"\"Test that XAI client captures provider_sdk_version from xai_sdk module.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.chat.create = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.chat.completions\n\n    mock_xai_sdk_module = mocker.MagicMock()\n    mock_xai_sdk_module.__version__ = \"1.4.1\"\n    mocker.patch.dict(\"sys.modules\", {\"xai_sdk\": mock_xai_sdk_module})\n\n    xai_client.register(mock_client)\n\n    assert xai_client.config.llm.provider_sdk_version == \"1.4.1\"\n\n\ndef test_xai_handles_missing_version_gracefully(xai_client, mocker):\n    \"\"\"Test that XAI client handles missing __version__ gracefully.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client.chat.create = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.chat.completions\n\n    mock_xai_sdk_module = mocker.MagicMock(spec=[])\n    del mock_xai_sdk_module.__version__\n    mocker.patch.dict(\"sys.modules\", {\"xai_sdk\": mock_xai_sdk_module})\n\n    xai_client.register(mock_client)\n\n    assert xai_client.config.llm.provider_sdk_version is None\n\n\ndef test_xai_with_completions_captures_provider_sdk_version(xai_client, mocker):\n    \"\"\"Test that XAI client with completions API captures provider_sdk_version.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"1.4.1\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_xai_sdk_module = mocker.MagicMock()\n    mock_xai_sdk_module.__version__ = \"1.4.1\"\n    mocker.patch.dict(\"sys.modules\", {\"xai_sdk\": mock_xai_sdk_module})\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    xai_client.register(mock_client)\n\n    assert xai_client.config.llm.provider_sdk_version == \"1.4.1\"\n\n\ndef test_openai_with_nebius_platform(openai_client, mocker):\n    \"\"\"Test that OpenAI client detects Nebius platform and captures SDK version.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"2.8.1\"\n    mock_client.base_url = \"https://api.studio.nebius.com/v1/\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    openai_client.register(mock_client)\n\n    assert openai_client.config.platform.provider == \"nebius\"\n    assert openai_client.config.llm.provider_sdk_version == \"2.8.1\"\n\n\ndef test_nvidia_with_nim_platform(openai_client, mocker):\n    \"\"\"Test that NVIDIA NIM platform is detected and SDK version is captured.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"2.8.1\"\n    mock_client.base_url = \"https://integrate.api.nvidia.com/v1/\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    openai_client.register(mock_client)\n\n    assert openai_client.config.platform.provider == \"nvidia_nim\"\n    assert openai_client.config.llm.provider_sdk_version == \"2.8.1\"\n\n\ndef test_deepseek_platform(openai_client, mocker):\n    \"\"\"Test that DeepSeek platform is detected and SDK version is captured.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"2.8.1\"\n    mock_client.base_url = \"https://api.deepseek.com\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    openai_client.register(mock_client)\n\n    assert openai_client.config.platform.provider == \"deepseek\"\n    assert openai_client.config.llm.provider_sdk_version == \"2.8.1\"\n\n\ndef test_provider_sdk_version_separate_from_model_version(openai_client, mocker):\n    \"\"\"Test that provider_sdk_version is separate from model version (llm.version).\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_client._version = \"2.8.1\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.base_url\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    openai_client.register(mock_client)\n\n    # provider_sdk_version should be set during registration\n    assert openai_client.config.llm.provider_sdk_version == \"2.8.1\"\n\n    # llm.version should still be None (set later from kwargs[\"model\"])\n    assert openai_client.config.llm.version is None\n"
  },
  {
    "path": "tests/llm/test_llm_registry.py",
    "content": "import pytest\n\nfrom memori._exceptions import UnsupportedLLMProviderError\nfrom memori.llm._constants import (\n    ATHROPIC_LLM_PROVIDER,\n    GOOGLE_LLM_PROVIDER,\n    LANGCHAIN_CHATBEDROCK_LLM_PROVIDER,\n    LANGCHAIN_FRAMEWORK_PROVIDER,\n    OPENAI_LLM_PROVIDER,\n)\nfrom memori.llm._registry import Registry\nfrom memori.llm.adapters.anthropic._adapter import Adapter as AnthropicLlmAdapter\nfrom memori.llm.adapters.bedrock._adapter import Adapter as BedrockLlmAdapter\nfrom memori.llm.adapters.google._adapter import Adapter as GoogleLlmAdapter\nfrom memori.llm.adapters.openai._adapter import Adapter as OpenAiLlmAdapter\n\n\ndef test_llm_anthropic():\n    assert isinstance(\n        Registry().adapter(None, ATHROPIC_LLM_PROVIDER), AnthropicLlmAdapter\n    )\n\n\ndef test_llm_bedrock():\n    assert isinstance(\n        Registry().adapter(\n            LANGCHAIN_FRAMEWORK_PROVIDER, LANGCHAIN_CHATBEDROCK_LLM_PROVIDER\n        ),\n        BedrockLlmAdapter,\n    )\n\n\ndef test_llm_google():\n    assert isinstance(Registry().adapter(None, GOOGLE_LLM_PROVIDER), GoogleLlmAdapter)\n\n\ndef test_llm_openai():\n    assert isinstance(Registry().adapter(None, OPENAI_LLM_PROVIDER), OpenAiLlmAdapter)\n\n\ndef test_llm_adapter_raises_for_none_provider():\n    \"\"\"Test that providing None as both provider and title raises UnsupportedLLMProviderError.\"\"\"\n\n    with pytest.raises(UnsupportedLLMProviderError, match=\"Unsupported LLM provider\"):\n        Registry().adapter(None, None)\n\n\ndef test_llm_adapter_raises_for_unsupported_provider():\n    \"\"\"Test that providing an unsupported provider raises UnsupportedLLMProviderError.\"\"\"\n\n    with pytest.raises(UnsupportedLLMProviderError, match=\"Unsupported LLM provider\"):\n        Registry().adapter(\"mistral\", \"mistral\")\n\n\ndef test_llm_client_raises_for_unsupported_client_type():\n    \"\"\"Test that registering an unsupported direct client raises UnsupportedLLMProviderError.\"\"\"\n\n    class MockUnsupportedClient:\n        pass\n\n    MockUnsupportedClient.__module__ = \"some_unknown.llm\"\n    MockUnsupportedClient.__name__ = \"UnsupportedClient\"\n\n    with pytest.raises(UnsupportedLLMProviderError):\n        Registry().client(MockUnsupportedClient(), None)\n\n\ndef test_llm_client_raises_helpful_error_for_langchain():\n    \"\"\"Test that LangChain clients produce a helpful error message.\"\"\"\n\n    class MockLangChainClient:\n        pass\n\n    MockLangChainClient.__module__ = \"langchain_openai.chat_models.base\"\n    MockLangChainClient.__name__ = \"ChatOpenAI\"\n\n    mock_client = MockLangChainClient()\n    mock_config = None\n\n    with pytest.raises(\n        RuntimeError,\n        match=r\"LangChain models require named parameters.*llm\\.register\\(chatopenai=client\\)\",\n    ):\n        Registry().client(mock_client, mock_config)\n"
  },
  {
    "path": "tests/llm/test_llm_utils.py",
    "content": "from memori.llm._constants import (\n    AGNO_ANTHROPIC_LLM_PROVIDER,\n    AGNO_FRAMEWORK_PROVIDER,\n    AGNO_GOOGLE_LLM_PROVIDER,\n    AGNO_OPENAI_LLM_PROVIDER,\n    AGNO_XAI_LLM_PROVIDER,\n    ATHROPIC_LLM_PROVIDER,\n    GOOGLE_LLM_PROVIDER,\n    LANGCHAIN_CHATBEDROCK_LLM_PROVIDER,\n    LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER,\n    LANGCHAIN_CHATVERTEXAI_LLM_PROVIDER,\n    LANGCHAIN_FRAMEWORK_PROVIDER,\n    LANGCHAIN_OPENAI_LLM_PROVIDER,\n    OPENAI_LLM_PROVIDER,\n)\nfrom memori.llm._utils import (\n    agno_is_anthropic,\n    agno_is_google,\n    agno_is_openai,\n    agno_is_xai,\n    client_is_bedrock,\n    llm_is_anthropic,\n    llm_is_bedrock,\n    llm_is_google,\n    llm_is_openai,\n    provider_is_agno,\n    provider_is_langchain,\n)\n\n\ndef test_client_is_bedrock():\n    assert client_is_bedrock(\"abc\", \"def\") is False\n    assert client_is_bedrock(LANGCHAIN_FRAMEWORK_PROVIDER, \"def\") is False\n    assert client_is_bedrock(\"abc\", LANGCHAIN_CHATBEDROCK_LLM_PROVIDER) is False\n    assert (\n        client_is_bedrock(\n            LANGCHAIN_FRAMEWORK_PROVIDER, LANGCHAIN_CHATBEDROCK_LLM_PROVIDER\n        )\n        is True\n    )\n\n\ndef test_llm_is_anthropic():\n    assert llm_is_anthropic(\"abc\", \"def\") is False\n    assert llm_is_anthropic(\"abc\", ATHROPIC_LLM_PROVIDER) is True\n    assert llm_is_anthropic(None, ATHROPIC_LLM_PROVIDER) is True\n\n\ndef test_llm_is_bedrock():\n    assert llm_is_bedrock(\"abc\", \"def\") is False\n    assert (\n        llm_is_bedrock(LANGCHAIN_FRAMEWORK_PROVIDER, LANGCHAIN_CHATBEDROCK_LLM_PROVIDER)\n        is True\n    )\n    assert llm_is_bedrock(LANGCHAIN_FRAMEWORK_PROVIDER, \"def\") is False\n    assert llm_is_bedrock(\"abc\", LANGCHAIN_CHATBEDROCK_LLM_PROVIDER) is False\n\n\ndef test_llm_is_google():\n    assert llm_is_google(\"abc\", \"def\") is False\n    assert llm_is_google(\"abc\", GOOGLE_LLM_PROVIDER) is True\n    assert llm_is_google(None, GOOGLE_LLM_PROVIDER) is True\n    assert (\n        llm_is_google(\n            LANGCHAIN_FRAMEWORK_PROVIDER, LANGCHAIN_CHATGOOGLEGENAI_LLM_PROVIDER\n        )\n        is True\n    )\n    assert (\n        llm_is_google(LANGCHAIN_FRAMEWORK_PROVIDER, LANGCHAIN_CHATVERTEXAI_LLM_PROVIDER)\n        is True\n    )\n\n\ndef test_llm_is_openai():\n    assert llm_is_openai(\"abc\", \"def\") is False\n    assert llm_is_openai(\"abc\", OPENAI_LLM_PROVIDER) is True\n    assert llm_is_openai(None, OPENAI_LLM_PROVIDER) is True\n    assert (\n        llm_is_openai(LANGCHAIN_FRAMEWORK_PROVIDER, LANGCHAIN_OPENAI_LLM_PROVIDER)\n        is True\n    )\n\n\ndef test_provider_is_langchain():\n    assert provider_is_langchain(\"abc\") is False\n    assert provider_is_langchain(LANGCHAIN_FRAMEWORK_PROVIDER) is True\n    assert provider_is_langchain(None) is False\n\n\ndef test_provider_is_agno():\n    assert provider_is_agno(\"abc\") is False\n    assert provider_is_agno(AGNO_FRAMEWORK_PROVIDER) is True\n    assert provider_is_agno(None) is False\n\n\ndef test_agno_is_openai():\n    assert agno_is_openai(\"abc\", \"def\") is False\n    assert agno_is_openai(AGNO_FRAMEWORK_PROVIDER, \"def\") is False\n    assert agno_is_openai(\"abc\", AGNO_OPENAI_LLM_PROVIDER) is False\n    assert agno_is_openai(AGNO_FRAMEWORK_PROVIDER, AGNO_OPENAI_LLM_PROVIDER) is True\n\n\ndef test_agno_is_anthropic():\n    assert agno_is_anthropic(\"abc\", \"def\") is False\n    assert agno_is_anthropic(AGNO_FRAMEWORK_PROVIDER, \"def\") is False\n    assert agno_is_anthropic(\"abc\", AGNO_ANTHROPIC_LLM_PROVIDER) is False\n    assert (\n        agno_is_anthropic(AGNO_FRAMEWORK_PROVIDER, AGNO_ANTHROPIC_LLM_PROVIDER) is True\n    )\n\n\ndef test_agno_is_google():\n    assert agno_is_google(\"abc\", \"def\") is False\n    assert agno_is_google(AGNO_FRAMEWORK_PROVIDER, \"def\") is False\n    assert agno_is_google(\"abc\", AGNO_GOOGLE_LLM_PROVIDER) is False\n    assert agno_is_google(AGNO_FRAMEWORK_PROVIDER, AGNO_GOOGLE_LLM_PROVIDER) is True\n\n\ndef test_agno_is_xai():\n    assert agno_is_xai(\"abc\", \"def\") is False\n    assert agno_is_xai(AGNO_FRAMEWORK_PROVIDER, \"def\") is False\n    assert agno_is_xai(\"abc\", AGNO_XAI_LLM_PROVIDER) is False\n    assert agno_is_xai(AGNO_FRAMEWORK_PROVIDER, AGNO_XAI_LLM_PROVIDER) is True\n"
  },
  {
    "path": "tests/llm/test_llm_xai_wrappers.py",
    "content": "from unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.llm._xai_wrappers import XAiWrappers\n\n\n@pytest.fixture\ndef config():\n    return Config()\n\n\n@pytest.fixture\ndef xai_wrappers(config):\n    return XAiWrappers(config)\n\n\ndef test_inject_conversation_history_no_conversation_id(xai_wrappers):\n    kwargs = {\"messages\": [\"msg1\", \"msg2\"]}\n    result = xai_wrappers.inject_conversation_history(kwargs)\n    assert result == {\"messages\": [\"msg1\", \"msg2\"]}\n\n\ndef test_inject_conversation_history_cache_miss_loads_from_session(\n    xai_wrappers, config, mocker\n):\n    mock_user = mocker.patch(\"xai_sdk.chat.user\")\n    mock_assistant = mocker.patch(\"xai_sdk.chat.assistant\")\n\n    config.session_id = \"session-uuid\"\n\n    mock_driver = mocker.MagicMock()\n    mock_driver.session.read.return_value = 11\n    mock_driver.conversation.read_id_by_session_id.return_value = \"conv123\"\n    mock_driver.conversation.messages.read.return_value = [\n        {\"role\": \"user\", \"content\": \"Hello\"},\n        {\"role\": \"assistant\", \"content\": \"Hi\"},\n    ]\n    mock_storage = mocker.MagicMock()\n    mock_storage.driver = mock_driver\n    config.storage = mock_storage\n\n    mock_user.return_value = \"user_msg\"\n    mock_assistant.return_value = \"assistant_msg\"\n\n    kwargs = {\"messages\": [\"new_msg\"]}\n    result = xai_wrappers.inject_conversation_history(kwargs)\n\n    assert config.cache.session_id == 11\n    assert config.cache.conversation_id == \"conv123\"\n    mock_driver.session.read.assert_called_once_with(\"session-uuid\")\n    mock_driver.conversation.read_id_by_session_id.assert_called_once_with(11)\n    mock_driver.conversation.messages.read.assert_called_once_with(\"conv123\")\n    assert result == {\"messages\": [\"user_msg\", \"assistant_msg\", \"new_msg\"]}\n\n\ndef test_inject_conversation_history_empty_messages(xai_wrappers, config, mocker):\n    config.cache.conversation_id = \"conv123\"\n    mock_driver = mocker.MagicMock()\n    mock_driver.conversation.messages.read.return_value = []\n    mock_storage = mocker.MagicMock()\n    mock_storage.driver = mock_driver\n    config.storage = mock_storage\n\n    kwargs = {\"messages\": [\"msg1\"]}\n    result = xai_wrappers.inject_conversation_history(kwargs)\n\n    mock_driver.conversation.messages.read.assert_called_once_with(\"conv123\")\n    assert result == {\"messages\": [\"msg1\"]}\n\n\ndef test_inject_conversation_history_with_user_messages(xai_wrappers, config, mocker):\n    with (\n        patch(\"xai_sdk.chat.user\") as mock_user,\n        patch(\"xai_sdk.chat.assistant\"),\n    ):\n        config.cache.conversation_id = \"conv123\"\n        mock_driver = mocker.MagicMock()\n        mock_driver.conversation.messages.read.return_value = [\n            {\"role\": \"user\", \"content\": \"Hello\"}\n        ]\n        mock_storage = mocker.MagicMock()\n        mock_storage.driver = mock_driver\n        config.storage = mock_storage\n\n        mock_user.return_value = \"user_msg\"\n        kwargs = {\"messages\": [\"new_msg\"]}\n        result = xai_wrappers.inject_conversation_history(kwargs)\n\n        mock_driver.conversation.messages.read.assert_called_once_with(\"conv123\")\n        mock_user.assert_called_once_with(\"Hello\")\n        assert result == {\"messages\": [\"user_msg\", \"new_msg\"]}\n\n\ndef test_inject_conversation_history_with_assistant_messages(\n    xai_wrappers, config, mocker\n):\n    with (\n        patch(\"xai_sdk.chat.user\"),\n        patch(\"xai_sdk.chat.assistant\") as mock_assistant,\n    ):\n        config.cache.conversation_id = \"conv123\"\n        mock_driver = mocker.MagicMock()\n        mock_driver.conversation.messages.read.return_value = [\n            {\"role\": \"assistant\", \"content\": \"Hi there\"}\n        ]\n        mock_storage = mocker.MagicMock()\n        mock_storage.driver = mock_driver\n        config.storage = mock_storage\n\n        mock_assistant.return_value = \"assistant_msg\"\n        kwargs = {\"messages\": [\"new_msg\"]}\n        result = xai_wrappers.inject_conversation_history(kwargs)\n\n        mock_driver.conversation.messages.read.assert_called_once_with(\"conv123\")\n        mock_assistant.assert_called_once_with(\"Hi there\")\n        assert result == {\"messages\": [\"assistant_msg\", \"new_msg\"]}\n\n\ndef test_inject_conversation_history_with_multiple_messages(\n    xai_wrappers, config, mocker\n):\n    with (\n        patch(\"xai_sdk.chat.user\") as mock_user,\n        patch(\"xai_sdk.chat.assistant\") as mock_assistant,\n    ):\n        config.cache.conversation_id = \"conv123\"\n        mock_driver = mocker.MagicMock()\n        mock_driver.conversation.messages.read.return_value = [\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi\"},\n            {\"role\": \"user\", \"content\": \"How are you?\"},\n        ]\n        mock_storage = mocker.MagicMock()\n        mock_storage.driver = mock_driver\n        config.storage = mock_storage\n\n        mock_user.side_effect = [\"user_msg1\", \"user_msg2\"]\n        mock_assistant.return_value = \"assistant_msg\"\n        kwargs = {\"messages\": [\"new_msg\"]}\n        result = xai_wrappers.inject_conversation_history(kwargs)\n\n        assert result == {\n            \"messages\": [\"user_msg1\", \"assistant_msg\", \"user_msg2\", \"new_msg\"]\n        }\n\n\ndef test_inject_conversation_history_ignores_unknown_roles(\n    xai_wrappers, config, mocker\n):\n    with (\n        patch(\"xai_sdk.chat.user\") as mock_user,\n        patch(\"xai_sdk.chat.assistant\"),\n    ):\n        config.cache.conversation_id = \"conv123\"\n        mock_driver = mocker.MagicMock()\n        mock_driver.conversation.messages.read.return_value = [\n            {\"role\": \"system\", \"content\": \"ignored\"},\n            {\"role\": \"user\", \"content\": \"Hello\"},\n        ]\n        mock_storage = mocker.MagicMock()\n        mock_storage.driver = mock_driver\n        config.storage = mock_storage\n\n        mock_user.return_value = \"user_msg\"\n        kwargs = {\"messages\": [\"new_msg\"]}\n        result = xai_wrappers.inject_conversation_history(kwargs)\n\n        assert result == {\"messages\": [\"user_msg\", \"new_msg\"]}\n\n\ndef test_wrap_chat_methods_already_installed(xai_wrappers):\n    chat_obj = MagicMock()\n    chat_obj._memori_installed = True\n    original_sample = chat_obj.sample\n\n    xai_wrappers.wrap_chat_methods(chat_obj, \"1.0.0\")\n\n    assert chat_obj.sample == original_sample\n\n\ndef test_wrap_chat_methods_sync(xai_wrappers):\n    chat_obj = MagicMock()\n    chat_obj.sample = MagicMock()\n    del chat_obj._memori_installed\n\n    xai_wrappers.wrap_chat_methods(chat_obj, \"1.0.0\")\n\n    assert hasattr(chat_obj, \"_sample\")\n    assert hasattr(chat_obj, \"_memori_installed\")\n    assert chat_obj._memori_installed is True\n\n\ndef test_wrap_chat_methods_async(xai_wrappers):\n    chat_obj = MagicMock()\n    chat_obj.sample = AsyncMock()\n    del chat_obj._memori_installed\n\n    xai_wrappers.wrap_chat_methods(chat_obj, \"1.0.0\")\n\n    assert hasattr(chat_obj, \"_sample\")\n    assert hasattr(chat_obj, \"_memori_installed\")\n    assert chat_obj._memori_installed is True\n\n\ndef test_wrap_chat_methods_with_stream(xai_wrappers):\n    chat_obj = MagicMock()\n    chat_obj.sample = MagicMock()\n    chat_obj.stream = MagicMock()\n    del chat_obj._memori_installed\n\n    xai_wrappers.wrap_chat_methods(chat_obj, \"1.0.0\")\n\n    assert hasattr(chat_obj, \"_stream\")\n    assert hasattr(chat_obj, \"_memori_installed\")\n\n\ndef test_normalize_role_assistant(xai_wrappers):\n    response = MagicMock()\n    response.role.name = \"ROLE_ASSISTANT\"\n\n    assert xai_wrappers._normalize_role(response) == \"assistant\"\n\n\ndef test_normalize_role_user(xai_wrappers):\n    response = MagicMock()\n    response.role.name = \"ROLE_USER\"\n\n    assert xai_wrappers._normalize_role(response) == \"user\"\n\n\ndef test_normalize_role_system(xai_wrappers):\n    response = MagicMock()\n    response.role.name = \"ROLE_SYSTEM\"\n\n    assert xai_wrappers._normalize_role(response) == \"system\"\n\n\ndef test_normalize_role_unknown(xai_wrappers):\n    response = MagicMock()\n    response.role.name = \"ROLE_CUSTOM\"\n\n    assert xai_wrappers._normalize_role(response) == \"custom\"\n\n\ndef test_normalize_role_without_name_attr(xai_wrappers):\n    response = MagicMock()\n    mock_role = MagicMock()\n    mock_role.__str__ = lambda self: \"ROLE_ASSISTANT\"\n    del mock_role.name\n    response.role = mock_role\n\n    assert xai_wrappers._normalize_role(response) == \"assistant\"\n\n\ndef test_build_payload(xai_wrappers, config):\n    query_formatted = {\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}]}\n    response_json = {\"content\": \"Hi\", \"role\": \"assistant\"}\n    client_version = \"1.0.0\"\n    start_time = 1234567890.0\n\n    payload = xai_wrappers._build_payload(\n        query_formatted, response_json, client_version, start_time\n    )\n\n    assert payload[\"attribution\"][\"entity\"][\"id\"] == config.entity_id\n    assert payload[\"attribution\"][\"process\"][\"id\"] == config.process_id\n    assert payload[\"conversation\"][\"client\"][\"title\"] == \"xai\"\n    assert payload[\"conversation\"][\"client\"][\"version\"] == \"1.0.0\"\n    assert payload[\"conversation\"][\"query\"] == query_formatted\n    assert payload[\"conversation\"][\"response\"] == response_json\n    assert payload[\"meta\"][\"api\"][\"key\"] == config.api_key\n    assert payload[\"meta\"][\"fnfg\"][\"status\"] == \"succeeded\"\n    assert payload[\"session\"][\"uuid\"] == str(config.session_id)\n    assert payload[\"time\"][\"start\"] == start_time\n    assert \"end\" in payload[\"time\"]\n\n\ndef test_create_sync_sample_wrapper(xai_wrappers, mocker):\n    chat_obj = MagicMock()\n    mock_response = MagicMock()\n    mock_response.content = \"Hello world\"\n    mock_response.role.name = \"ROLE_ASSISTANT\"\n    chat_obj._sample = MagicMock(return_value=mock_response)\n    chat_obj.messages = []\n\n    mock_manager = mocker.patch(\"memori.memory._manager.Manager\")\n\n    wrapper = xai_wrappers._create_sync_sample_wrapper(chat_obj, \"1.0.0\")\n    result = wrapper()\n\n    assert result == mock_response\n    chat_obj._sample.assert_called_once()\n    mock_manager.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_create_async_sample_wrapper(xai_wrappers, mocker):\n    chat_obj = MagicMock()\n    mock_response = MagicMock()\n    mock_response.content = \"Hello world\"\n    mock_response.role.name = \"ROLE_ASSISTANT\"\n    chat_obj._sample = AsyncMock(return_value=mock_response)\n    chat_obj.messages = []\n\n    mock_manager = mocker.patch(\"memori.memory._manager.Manager\")\n\n    wrapper = xai_wrappers._create_async_sample_wrapper(chat_obj, \"1.0.0\")\n    result = await wrapper()\n\n    assert result == mock_response\n    chat_obj._sample.assert_called_once()\n    mock_manager.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_create_stream_wrapper_with_tuple_items(xai_wrappers, mocker):\n    chat_obj = MagicMock()\n    mock_delta1 = MagicMock()\n    mock_delta1.content = \"Hello\"\n    mock_delta2 = MagicMock()\n    mock_delta2.content = \" world\"\n    mock_response = MagicMock()\n    mock_response.role.name = \"ROLE_ASSISTANT\"\n\n    async def mock_stream(*args, **kwargs):\n        yield (mock_response, mock_delta1)\n        yield (mock_response, mock_delta2)\n\n    chat_obj._stream = mock_stream\n    chat_obj.messages = []\n\n    mock_manager = mocker.patch(\"memori.memory._manager.Manager\")\n\n    wrapper = xai_wrappers._create_stream_wrapper(chat_obj, \"1.0.0\")\n\n    items = []\n    async for item in wrapper():\n        items.append(item)\n\n    assert len(items) == 2\n    mock_manager.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_create_stream_wrapper_with_content_items(xai_wrappers, mocker):\n    chat_obj = MagicMock()\n    mock_item1 = MagicMock()\n    mock_item1.content = \"Hello\"\n    mock_item1.role.name = \"ROLE_ASSISTANT\"\n    mock_item2 = MagicMock()\n    mock_item2.content = \" world\"\n    mock_item2.role.name = \"ROLE_ASSISTANT\"\n\n    async def mock_stream(*args, **kwargs):\n        yield mock_item1\n        yield mock_item2\n\n    chat_obj._stream = mock_stream\n    chat_obj.messages = []\n\n    mock_manager = mocker.patch(\"memori.memory._manager.Manager\")\n\n    wrapper = xai_wrappers._create_stream_wrapper(chat_obj, \"1.0.0\")\n\n    items = []\n    async for item in wrapper():\n        items.append(item)\n\n    assert len(items) == 2\n    mock_manager.assert_called_once()\n\n\n@pytest.mark.asyncio\nasync def test_create_stream_wrapper_without_content(xai_wrappers, mocker):\n    chat_obj = MagicMock()\n    mock_item = MagicMock(spec=[])\n\n    async def mock_stream(*args, **kwargs):\n        yield mock_item\n\n    chat_obj._stream = mock_stream\n    chat_obj.messages = []\n\n    mock_manager = mocker.patch(\"memori.memory._manager.Manager\")\n\n    wrapper = xai_wrappers._create_stream_wrapper(chat_obj, \"1.0.0\")\n\n    items = []\n    async for item in wrapper():\n        items.append(item)\n\n    assert len(items) == 1\n    mock_manager.assert_not_called()\n"
  },
  {
    "path": "tests/llm/unit_test_objects.py",
    "content": "class UnitTestX:\n    def __init__(self):\n        self.a = 1\n        self.b = 2\n\n\nclass UnitTestY:\n    def __init__(self):\n        self.c = 3\n        self.d = UnitTestX()\n"
  },
  {
    "path": "tests/memory/augmentation/test_aa_payload_unit.py",
    "content": "from unittest.mock import MagicMock\n\nimport pytest\n\nfrom memori.memory.augmentation._models import (\n    AttributionData,\n    AugmentationPayload,\n    ConversationData,\n    EntityData,\n    FrameworkData,\n    LlmData,\n    MetaData,\n    ModelData,\n    PlatformData,\n    ProcessData,\n    SdkData,\n    SdkVersionData,\n    StorageData,\n    hash_id,\n)\n\n\nclass TestHashId:\n    def test_hash_id_returns_64_chars(self):\n        result = hash_id(\"test-user-123\")\n        assert result is not None\n        assert len(result) == 64\n        assert all(c in \"0123456789abcdef\" for c in result)\n\n    def test_hash_id_returns_none_for_none(self):\n        assert hash_id(None) is None\n\n    def test_hash_id_returns_none_for_empty_string(self):\n        assert hash_id(\"\") is None\n\n    def test_hash_id_is_deterministic(self):\n        hash1 = hash_id(\"consistent-user\")\n        hash2 = hash_id(\"consistent-user\")\n        assert hash1 == hash2\n\n    def test_hash_id_different_inputs_different_hashes(self):\n        hash1 = hash_id(\"user-1\")\n        hash2 = hash_id(\"user-2\")\n        assert hash1 != hash2\n\n\nclass TestDataclassModels:\n    def test_conversation_data_structure(self):\n        messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n        conv = ConversationData(messages=messages, summary=\"A greeting\")\n\n        assert conv.messages == messages\n        assert conv.summary == \"A greeting\"\n\n    def test_conversation_data_summary_optional(self):\n        conv = ConversationData(messages=[])\n        assert conv.summary is None\n\n    def test_entity_data_structure(self):\n        entity = EntityData(id=hash_id(\"user-123\"))\n        assert entity.id is not None\n        assert len(entity.id) == 64\n\n    def test_attribution_data_structure(self):\n        attr = AttributionData(\n            entity=EntityData(id=hash_id(\"user\")),\n            process=ProcessData(id=hash_id(\"process\")),\n        )\n        assert attr.entity.id is not None\n        assert attr.process.id is not None\n\n    def test_meta_data_has_all_required_fields(self):\n        meta = MetaData()\n\n        assert hasattr(meta, \"attribution\")\n        assert hasattr(meta, \"framework\")\n        assert hasattr(meta, \"llm\")\n        assert hasattr(meta, \"platform\")\n        assert hasattr(meta, \"sdk\")\n        assert hasattr(meta, \"storage\")\n\n    def test_sdk_data_defaults_to_python(self):\n        sdk = SdkData()\n        assert sdk.lang == \"python\"\n\n    def test_storage_data_defaults(self):\n        storage = StorageData()\n        assert storage.cockroachdb is False\n        assert storage.dialect is None\n\n\nclass TestAugmentationPayloadToDict:\n    def test_payload_has_required_top_level_keys(self):\n        payload = AugmentationPayload(\n            conversation=ConversationData(messages=[]),\n            meta=MetaData(),\n        )\n        result = payload.to_dict()\n\n        assert \"conversation\" in result\n        assert \"meta\" in result\n\n    def test_payload_conversation_structure(self):\n        messages = [\n            {\"role\": \"system\", \"content\": \"You are helpful.\"},\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n        ]\n        payload = AugmentationPayload(\n            conversation=ConversationData(messages=messages, summary=\"A conversation\"),\n            meta=MetaData(),\n        )\n        result = payload.to_dict()\n\n        assert \"messages\" in result[\"conversation\"]\n        assert isinstance(result[\"conversation\"][\"messages\"], list)\n        assert len(result[\"conversation\"][\"messages\"]) == 3\n        assert result[\"conversation\"][\"summary\"] == \"A conversation\"\n\n    def test_payload_meta_has_all_required_keys(self):\n        payload = AugmentationPayload(\n            conversation=ConversationData(messages=[]),\n            meta=MetaData(\n                attribution=AttributionData(\n                    entity=EntityData(id=hash_id(\"user\")),\n                    process=ProcessData(id=hash_id(\"proc\")),\n                ),\n                framework=FrameworkData(provider=\"openai\"),\n                llm=LlmData(\n                    model=ModelData(\n                        provider=\"openai\",\n                        sdk=SdkVersionData(version=\"1.0.0\"),\n                        version=\"gpt-4\",\n                    )\n                ),\n                platform=PlatformData(provider=\"python\"),\n                sdk=SdkData(lang=\"python\", version=\"0.1.0\"),\n                storage=StorageData(cockroachdb=False, dialect=\"sqlite\"),\n            ),\n        )\n        result = payload.to_dict()\n\n        meta = result[\"meta\"]\n        assert \"attribution\" in meta\n        assert \"framework\" in meta\n        assert \"llm\" in meta\n        assert \"platform\" in meta\n        assert \"sdk\" in meta\n        assert \"storage\" in meta\n\n    def test_payload_attribution_structure(self):\n        payload = AugmentationPayload(\n            conversation=ConversationData(messages=[]),\n            meta=MetaData(\n                attribution=AttributionData(\n                    entity=EntityData(id=hash_id(\"entity-123\")),\n                    process=ProcessData(id=hash_id(\"process-456\")),\n                )\n            ),\n        )\n        result = payload.to_dict()\n\n        attr = result[\"meta\"][\"attribution\"]\n        assert \"entity\" in attr\n        assert \"id\" in attr[\"entity\"]\n        assert len(attr[\"entity\"][\"id\"]) == 64\n\n        assert \"process\" in attr\n        assert \"id\" in attr[\"process\"]\n        assert len(attr[\"process\"][\"id\"]) == 64\n\n    def test_payload_llm_structure(self):\n        payload = AugmentationPayload(\n            conversation=ConversationData(messages=[]),\n            meta=MetaData(\n                llm=LlmData(\n                    model=ModelData(\n                        provider=\"anthropic\",\n                        sdk=SdkVersionData(version=\"0.30.0\"),\n                        version=\"claude-3-opus\",\n                    )\n                )\n            ),\n        )\n        result = payload.to_dict()\n\n        llm = result[\"meta\"][\"llm\"]\n        assert \"model\" in llm\n        assert llm[\"model\"][\"provider\"] == \"anthropic\"\n        assert llm[\"model\"][\"version\"] == \"claude-3-opus\"\n        assert llm[\"model\"][\"sdk\"][\"version\"] == \"0.30.0\"\n\n    def test_payload_sdk_structure(self):\n        payload = AugmentationPayload(\n            conversation=ConversationData(messages=[]),\n            meta=MetaData(sdk=SdkData(lang=\"python\", version=\"1.2.3\")),\n        )\n        result = payload.to_dict()\n\n        sdk = result[\"meta\"][\"sdk\"]\n        assert sdk[\"lang\"] == \"python\"\n        assert sdk[\"version\"] == \"1.2.3\"\n\n    def test_payload_storage_structure(self):\n        payload = AugmentationPayload(\n            conversation=ConversationData(messages=[]),\n            meta=MetaData(storage=StorageData(cockroachdb=True, dialect=\"postgresql\")),\n        )\n        result = payload.to_dict()\n\n        storage = result[\"meta\"][\"storage\"]\n        assert storage[\"cockroachdb\"] is True\n        assert storage[\"dialect\"] == \"postgresql\"\n\n\nclass TestBuildApiPayload:\n    @pytest.fixture\n    def mock_config(self):\n        config = MagicMock()\n        config.framework.provider = \"openai\"\n        config.llm.provider = \"openai\"\n        config.llm.provider_sdk_version = \"1.50.0\"\n        config.llm.version = \"gpt-4o-mini\"\n        config.platform.provider = \"python\"\n        config.version = \"0.1.0\"\n        config.storage_config.cockroachdb = False\n        return config\n\n    @pytest.fixture\n    def augmentation(self, mock_config):\n        from memori.memory.augmentation.augmentations.memori._augmentation import (\n            AdvancedAugmentation,\n        )\n\n        aug = AdvancedAugmentation(config=mock_config)\n        return aug\n\n    def test_build_payload_returns_dict(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[{\"role\": \"user\", \"content\": \"test\"}],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user-123\",\n            process_id=\"proc-456\",\n        )\n\n        assert isinstance(payload, dict)\n\n    def test_build_payload_has_required_keys(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[{\"role\": \"user\", \"content\": \"test\"}],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user-123\",\n            process_id=\"proc-456\",\n        )\n\n        assert \"conversation\" in payload\n        assert \"meta\" in payload\n\n    def test_build_payload_hashes_entity_id(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"my-user-id\",\n            process_id=\"my-process-id\",\n        )\n\n        entity_id = payload[\"meta\"][\"attribution\"][\"entity\"][\"id\"]\n        assert entity_id is not None\n        assert len(entity_id) == 64\n        assert entity_id != \"my-user-id\"\n\n    def test_build_payload_hashes_process_id(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"my-process-id\",\n        )\n\n        process_id = payload[\"meta\"][\"attribution\"][\"process\"][\"id\"]\n        assert process_id is not None\n        assert len(process_id) == 64\n        assert process_id != \"my-process-id\"\n\n    def test_build_payload_includes_messages(self, augmentation):\n        messages = [\n            {\"role\": \"system\", \"content\": \"You are helpful.\"},\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi!\"},\n        ]\n\n        payload = augmentation._build_api_payload(\n            messages=messages,\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"conversation\"][\"messages\"] == messages\n\n    def test_build_payload_includes_summary(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[{\"role\": \"user\", \"content\": \"test\"}],\n            summary=\"This is a test conversation\",\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"conversation\"][\"summary\"] == \"This is a test conversation\"\n\n    def test_build_payload_includes_dialect(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[],\n            summary=None,\n            system_prompt=None,\n            dialect=\"postgresql\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"storage\"][\"dialect\"] == \"postgresql\"\n\n    def test_build_payload_includes_llm_provider(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"provider\"] == \"openai\"\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"version\"] == \"gpt-4o-mini\"\n\n    def test_build_payload_includes_sdk_info(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"sdk\"][\"lang\"] == \"python\"\n        assert payload[\"meta\"][\"sdk\"][\"version\"] == \"0.1.0\"\n\n    def test_build_payload_includes_framework_provider(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"framework\"][\"provider\"] == \"openai\"\n\n    def test_build_payload_none_entity_id(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=None,\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"attribution\"][\"entity\"][\"id\"] is None\n\n    def test_build_payload_none_process_id(self, augmentation):\n        payload = augmentation._build_api_payload(\n            messages=[],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=None,\n        )\n\n        assert payload[\"meta\"][\"attribution\"][\"process\"][\"id\"] is None\n\n\nclass TestPayloadValidator:\n    def validate_payload_structure(self, payload: dict) -> list[str]:\n        errors = []\n\n        if not payload:\n            return [\"No payload to validate\"]\n\n        if \"conversation\" not in payload:\n            errors.append(\"Missing 'conversation' key\")\n        if \"meta\" not in payload:\n            errors.append(\"Missing 'meta' key\")\n\n        if \"conversation\" in payload:\n            conv = payload[\"conversation\"]\n            if \"messages\" not in conv:\n                errors.append(\"Missing 'conversation.messages'\")\n            elif not isinstance(conv[\"messages\"], list):\n                errors.append(\"'conversation.messages' must be a list\")\n\n        if \"meta\" in payload:\n            meta = payload[\"meta\"]\n\n            required_meta = [\n                \"attribution\",\n                \"framework\",\n                \"llm\",\n                \"platform\",\n                \"sdk\",\n                \"storage\",\n            ]\n            for key in required_meta:\n                if key not in meta:\n                    errors.append(f\"Missing 'meta.{key}'\")\n\n            if \"attribution\" in meta:\n                attr = meta[\"attribution\"]\n                if \"entity\" not in attr or \"id\" not in attr.get(\"entity\", {}):\n                    errors.append(\"Missing 'meta.attribution.entity.id'\")\n                if \"process\" not in attr or \"id\" not in attr.get(\"process\", {}):\n                    errors.append(\"Missing 'meta.attribution.process.id'\")\n\n                entity_id = attr.get(\"entity\", {}).get(\"id\")\n                if entity_id is not None and len(entity_id) != 64:\n                    errors.append(\n                        f\"Entity ID not hashed: {len(entity_id)} chars, expected 64\"\n                    )\n\n                process_id = attr.get(\"process\", {}).get(\"id\")\n                if process_id is not None and len(process_id) != 64:\n                    errors.append(\n                        f\"Process ID not hashed: {len(process_id)} chars, expected 64\"\n                    )\n\n            if \"llm\" in meta:\n                llm = meta[\"llm\"]\n                if \"model\" not in llm:\n                    errors.append(\"Missing 'meta.llm.model'\")\n                elif \"provider\" not in llm.get(\"model\", {}):\n                    errors.append(\"Missing 'meta.llm.model.provider'\")\n\n            if \"sdk\" in meta:\n                sdk = meta[\"sdk\"]\n                if sdk.get(\"lang\") != \"python\":\n                    lang = sdk.get(\"lang\")\n                    errors.append(f\"Expected sdk.lang='python', got '{lang}'\")\n\n            if \"storage\" in meta:\n                storage = meta[\"storage\"]\n                if \"dialect\" not in storage:\n                    errors.append(\"Missing 'meta.storage.dialect'\")\n                if \"cockroachdb\" not in storage:\n                    errors.append(\"Missing 'meta.storage.cockroachdb'\")\n\n        return errors\n\n    def test_valid_payload_passes_validation(self):\n        payload = AugmentationPayload(\n            conversation=ConversationData(\n                messages=[{\"role\": \"user\", \"content\": \"Hello\"}],\n                summary=None,\n            ),\n            meta=MetaData(\n                attribution=AttributionData(\n                    entity=EntityData(id=hash_id(\"user-123\")),\n                    process=ProcessData(id=hash_id(\"proc-456\")),\n                ),\n                framework=FrameworkData(provider=\"openai\"),\n                llm=LlmData(\n                    model=ModelData(\n                        provider=\"openai\",\n                        sdk=SdkVersionData(version=\"1.0.0\"),\n                        version=\"gpt-4\",\n                    )\n                ),\n                platform=PlatformData(provider=\"python\"),\n                sdk=SdkData(lang=\"python\", version=\"0.1.0\"),\n                storage=StorageData(cockroachdb=False, dialect=\"sqlite\"),\n            ),\n        )\n\n        errors = self.validate_payload_structure(payload.to_dict())\n        assert len(errors) == 0, f\"Validation errors: {errors}\"\n\n    def test_missing_conversation_fails_validation(self):\n        payload = {\"meta\": {}}\n        errors = self.validate_payload_structure(payload)\n        assert \"Missing 'conversation' key\" in errors\n\n    def test_missing_meta_fails_validation(self):\n        payload = {\"conversation\": {\"messages\": []}}\n        errors = self.validate_payload_structure(payload)\n        assert \"Missing 'meta' key\" in errors\n\n    def test_unhashed_entity_id_fails_validation(self):\n        payload = {\n            \"conversation\": {\"messages\": []},\n            \"meta\": {\n                \"attribution\": {\n                    \"entity\": {\"id\": \"raw-user-id\"},\n                    \"process\": {\"id\": hash_id(\"proc\")},\n                },\n                \"framework\": {\"provider\": \"openai\"},\n                \"llm\": {\"model\": {\"provider\": \"openai\"}},\n                \"platform\": {\"provider\": \"python\"},\n                \"sdk\": {\"lang\": \"python\", \"version\": \"0.1.0\"},\n                \"storage\": {\"cockroachdb\": False, \"dialect\": \"sqlite\"},\n            },\n        }\n\n        errors = self.validate_payload_structure(payload)\n        assert any(\"Entity ID not hashed\" in e for e in errors)\n\n\nclass TestProviderSpecificPayloads:\n    @pytest.fixture\n    def make_augmentation(self):\n        def _make(provider: str, sdk_version: str = \"1.0.0\", model: str = \"test-model\"):\n            from memori.memory.augmentation.augmentations.memori._augmentation import (\n                AdvancedAugmentation,\n            )\n\n            config = MagicMock()\n            config.framework.provider = provider\n            config.llm.provider = provider\n            config.llm.provider_sdk_version = sdk_version\n            config.llm.version = model\n            config.platform.provider = \"python\"\n            config.version = \"0.1.0\"\n            config.storage_config.cockroachdb = False\n\n            return AdvancedAugmentation(config=config)\n\n        return _make\n\n    def test_openai_payload(self, make_augmentation):\n        aug = make_augmentation(\"openai\", \"1.50.0\", \"gpt-4o-mini\")\n        payload = aug._build_api_payload(\n            messages=[{\"role\": \"user\", \"content\": \"test\"}],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"framework\"][\"provider\"] == \"openai\"\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"provider\"] == \"openai\"\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"version\"] == \"gpt-4o-mini\"\n\n    def test_anthropic_payload(self, make_augmentation):\n        aug = make_augmentation(\"anthropic\", \"0.30.0\", \"claude-3-opus-20240229\")\n        payload = aug._build_api_payload(\n            messages=[{\"role\": \"user\", \"content\": \"test\"}],\n            summary=None,\n            system_prompt=None,\n            dialect=\"postgresql\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"framework\"][\"provider\"] == \"anthropic\"\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"provider\"] == \"anthropic\"\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"version\"] == \"claude-3-opus-20240229\"\n\n    def test_google_payload(self, make_augmentation):\n        aug = make_augmentation(\"google\", \"1.0.0\", \"gemini-1.5-flash\")\n        payload = aug._build_api_payload(\n            messages=[{\"role\": \"user\", \"content\": \"test\"}],\n            summary=None,\n            system_prompt=None,\n            dialect=\"mysql\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"framework\"][\"provider\"] == \"google\"\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"provider\"] == \"google\"\n\n    def test_bedrock_payload(self, make_augmentation):\n        aug = make_augmentation(\n            \"bedrock\", \"0.2.0\", \"anthropic.claude-3-sonnet-20240229-v1:0\"\n        )\n        payload = aug._build_api_payload(\n            messages=[{\"role\": \"user\", \"content\": \"test\"}],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"framework\"][\"provider\"] == \"bedrock\"\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"provider\"] == \"bedrock\"\n\n    def test_xai_payload(self, make_augmentation):\n        aug = make_augmentation(\"xai\", \"1.0.0\", \"grok-beta\")\n        payload = aug._build_api_payload(\n            messages=[{\"role\": \"user\", \"content\": \"test\"}],\n            summary=None,\n            system_prompt=None,\n            dialect=\"sqlite\",\n            entity_id=\"user\",\n            process_id=\"proc\",\n        )\n\n        assert payload[\"meta\"][\"framework\"][\"provider\"] == \"xai\"\n        assert payload[\"meta\"][\"llm\"][\"model\"][\"provider\"] == \"xai\"\n"
  },
  {
    "path": "tests/memory/augmentation/test_advanced_augmentation.py",
    "content": "from unittest.mock import AsyncMock, Mock, patch\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.memory._struct import Memories\nfrom memori.memory.augmentation._base import AugmentationContext\nfrom memori.memory.augmentation._message import ConversationMessage\nfrom memori.memory.augmentation.augmentations.memori._augmentation import (\n    AdvancedAugmentation,\n)\nfrom memori.memory.augmentation.input import AugmentationInput\n\n\n@pytest.fixture\ndef config():\n    config = Config()\n    config.llm.provider = \"openai\"\n    config.llm.version = \"gpt-4\"\n    config.version = \"1.0.0\"\n    config.storage_config.cockroachdb = False\n    return config\n\n\n@pytest.fixture\ndef augmentation(config):\n    return AdvancedAugmentation(config=config, enabled=True)\n\n\n@pytest.fixture\ndef driver():\n    driver = Mock()\n    driver.conversation.conn.get_dialect.return_value = \"postgresql\"\n    driver.conversation.read.return_value = {\"summary\": \"Previous conversation summary\"}\n    driver.entity.create.return_value = 1\n    driver.process.create.return_value = 1\n    return driver\n\n\n@pytest.fixture\ndef augmentation_input():\n    return AugmentationInput(\n        conversation_id=\"conv-123\",\n        entity_id=\"entity-456\",\n        process_id=\"process-789\",\n        conversation_messages=[\n            ConversationMessage(role=\"user\", content=\"Hello\"),\n            ConversationMessage(role=\"assistant\", content=\"Hi there\"),\n        ],\n    )\n\n\n@pytest.mark.asyncio\nasync def test_process_with_summary_sends_only_last_user_assistant_pair(\n    augmentation, driver, mocker\n):\n    driver.conversation.read.return_value = {\"summary\": \"Previous conversation summary\"}\n\n    input_payload = AugmentationInput(\n        conversation_id=\"conv-123\",\n        entity_id=\"entity-456\",\n        process_id=\"process-789\",\n        conversation_messages=[\n            ConversationMessage(role=\"system\", content=\"system\"),\n            ConversationMessage(role=\"user\", content=\"Hello\"),\n            ConversationMessage(role=\"assistant\", content=\"Hi there\"),\n            ConversationMessage(role=\"user\", content=\"Second question\"),\n            ConversationMessage(role=\"assistant\", content=\"Second answer\"),\n        ],\n    )\n    ctx = AugmentationContext(payload=input_payload)\n\n    augmentation_async = mocker.patch(\n        \"memori._network.Api.augmentation_async\", return_value={}\n    )\n\n    result = await augmentation.process(ctx, driver)\n\n    assert result == ctx\n    augmentation_async.assert_called_once()\n    payload = augmentation_async.call_args[0][0]\n    assert payload[\"conversation\"][\"summary\"] == \"Previous conversation summary\"\n    assert payload[\"conversation\"][\"messages\"] == [\n        {\"role\": \"user\", \"content\": \"Second question\"},\n        {\"role\": \"assistant\", \"content\": \"Second answer\"},\n    ]\n\n\n@pytest.mark.asyncio\nasync def test_process_without_summary_sends_full_message_history(\n    augmentation, driver, mocker\n):\n    driver.conversation.read.return_value = {}\n\n    messages = [\n        ConversationMessage(role=\"system\", content=\"system\"),\n        ConversationMessage(role=\"user\", content=\"Hello\"),\n        ConversationMessage(role=\"assistant\", content=\"Hi there\"),\n        ConversationMessage(role=\"user\", content=\"Second question\"),\n        ConversationMessage(role=\"assistant\", content=\"Second answer\"),\n    ]\n    input_payload = AugmentationInput(\n        conversation_id=\"conv-123\",\n        entity_id=\"entity-456\",\n        process_id=\"process-789\",\n        conversation_messages=messages,\n    )\n    ctx = AugmentationContext(payload=input_payload)\n\n    augmentation_async = mocker.patch(\n        \"memori._network.Api.augmentation_async\", return_value={}\n    )\n\n    result = await augmentation.process(ctx, driver)\n\n    assert result == ctx\n    augmentation_async.assert_called_once()\n    payload = augmentation_async.call_args[0][0]\n    assert payload[\"conversation\"][\"summary\"] is None\n    assert payload[\"conversation\"][\"messages\"] == [m.to_dict() for m in messages]\n\n\n@pytest.mark.asyncio\nasync def test_process_no_entity_id(augmentation, driver):\n    input_payload = AugmentationInput(\n        conversation_id=\"conv-123\",\n        entity_id=None,\n        process_id=\"process-789\",\n        conversation_messages=[],\n    )\n    ctx = AugmentationContext(payload=input_payload)\n\n    result = await augmentation.process(ctx, driver)\n\n    assert result == ctx\n    assert \"memories\" not in ctx.data\n\n\n@pytest.mark.asyncio\nasync def test_process_no_conversation_id(augmentation, driver):\n    input_payload = AugmentationInput(\n        conversation_id=None,\n        entity_id=\"entity-456\",\n        process_id=\"process-789\",\n        conversation_messages=[],\n    )\n    ctx = AugmentationContext(payload=input_payload)\n\n    result = await augmentation.process(ctx, driver)\n\n    assert result == ctx\n    assert \"memories\" not in ctx.data\n\n\n@pytest.mark.asyncio\nasync def test_build_api_payload_with_system_prompt(augmentation):\n    messages = [{\"role\": \"user\", \"content\": \"Test\"}]\n    summary = \"Test summary\"\n    system_prompt = \"You are helpful\"\n    dialect = \"postgresql\"\n    entity_id = \"entity-123\"\n    process_id = \"process-456\"\n\n    payload = augmentation._build_api_payload(\n        messages, summary, system_prompt, dialect, entity_id, process_id\n    )\n\n    assert payload[\"conversation\"][\"messages\"] == messages\n    assert payload[\"conversation\"][\"summary\"] == summary\n    assert \"system_prompt\" not in payload[\"conversation\"]\n    assert payload[\"meta\"][\"storage\"][\"dialect\"] == dialect\n    assert payload[\"meta\"][\"attribution\"][\"entity\"][\"id\"] is not None\n    assert payload[\"meta\"][\"attribution\"][\"process\"][\"id\"] is not None\n\n\n@pytest.mark.asyncio\nasync def test_build_api_payload_without_system_prompt(augmentation):\n    messages = [{\"role\": \"user\", \"content\": \"Test\"}]\n    summary = \"Test summary\"\n    system_prompt = None\n    dialect = \"mysql\"\n    entity_id = None\n    process_id = None\n\n    payload = augmentation._build_api_payload(\n        messages, summary, system_prompt, dialect, entity_id, process_id\n    )\n\n    assert payload[\"conversation\"][\"messages\"] == messages\n    assert payload[\"conversation\"][\"summary\"] == summary\n    assert \"system_prompt\" not in payload[\"conversation\"]\n    assert payload[\"meta\"][\"storage\"][\"dialect\"] == dialect\n    assert payload[\"meta\"][\"attribution\"][\"entity\"][\"id\"] is None\n    assert payload[\"meta\"][\"attribution\"][\"process\"][\"id\"] is None\n\n\n@pytest.mark.asyncio\nasync def test_build_api_payload_hashes_ids_consistently(augmentation):\n    messages = [{\"role\": \"user\", \"content\": \"Test\"}]\n    summary = \"Test summary\"\n    system_prompt = None\n    dialect = \"postgresql\"\n    entity_id = \"user_123\"\n    process_id = \"checkout_flow\"\n\n    payload1 = augmentation._build_api_payload(\n        messages, summary, system_prompt, dialect, entity_id, process_id\n    )\n    payload2 = augmentation._build_api_payload(\n        messages, summary, system_prompt, dialect, entity_id, process_id\n    )\n\n    assert (\n        payload1[\"meta\"][\"attribution\"][\"entity\"][\"id\"]\n        == payload2[\"meta\"][\"attribution\"][\"entity\"][\"id\"]\n    )\n    assert (\n        payload1[\"meta\"][\"attribution\"][\"process\"][\"id\"]\n        == payload2[\"meta\"][\"attribution\"][\"process\"][\"id\"]\n    )\n    assert payload1[\"meta\"][\"attribution\"][\"entity\"][\"id\"] != entity_id\n    assert payload1[\"meta\"][\"attribution\"][\"process\"][\"id\"] != process_id\n    assert len(payload1[\"meta\"][\"attribution\"][\"entity\"][\"id\"]) == 64\n    assert len(payload1[\"meta\"][\"attribution\"][\"process\"][\"id\"]) == 64\n\n\n@pytest.mark.asyncio\nasync def test_get_conversation_summary_success(augmentation, driver):\n    summary = augmentation._get_conversation_summary(driver, \"conv-123\")\n\n    assert summary == \"Previous conversation summary\"\n    driver.conversation.read.assert_called_once_with(\"conv-123\")\n\n\n@pytest.mark.asyncio\nasync def test_get_conversation_summary_no_summary(augmentation, driver):\n    driver.conversation.read.return_value = {}\n\n    summary = augmentation._get_conversation_summary(driver, \"conv-123\")\n\n    assert summary == \"\"\n\n\n@pytest.mark.asyncio\nasync def test_get_conversation_summary_exception(augmentation, driver):\n    driver.conversation.read.side_effect = Exception(\"Database error\")\n\n    summary = augmentation._get_conversation_summary(driver, \"conv-123\")\n\n    assert summary == \"\"\n\n\n@pytest.mark.asyncio\nasync def test_process_api_response_dict_to_memories(augmentation):\n    api_response = {\n        \"entity\": {\"facts\": [\"User likes pizza\", \"User is from NYC\"], \"triples\": []},\n        \"conversation\": {\"summary\": \"Discussed food preferences\"},\n        \"process\": {\"attributes\": [\"food\", \"location\"]},\n    }\n\n    with patch(\n        \"memori.memory.augmentation.augmentations.memori._augmentation.embed_texts\",\n        new_callable=AsyncMock,\n    ) as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2], [0.3, 0.4]]\n\n        result = await augmentation._process_api_response(api_response)\n\n        assert isinstance(result, Memories)\n        assert result.entity.facts == [\"User likes pizza\", \"User is from NYC\"]\n        assert result.conversation.summary == \"Discussed food preferences\"\n\n\n@pytest.mark.asyncio\nasync def test_process_api_response_triples_to_facts(augmentation):\n    api_response = {\n        \"entity\": {\n            \"facts\": [],\n            \"triples\": [\n                {\n                    \"subject\": {\"name\": \"User\", \"type\": \"Person\"},\n                    \"predicate\": \"likes\",\n                    \"object\": {\"name\": \"Pizza\", \"type\": \"Food\"},\n                },\n                {\n                    \"subject\": {\"name\": \"User\", \"type\": \"Person\"},\n                    \"predicate\": \"lives_in\",\n                    \"object\": {\"name\": \"NYC\", \"type\": \"City\"},\n                },\n            ],\n        }\n    }\n\n    with patch(\n        \"memori.memory.augmentation.augmentations.memori._augmentation.embed_texts\",\n        new_callable=AsyncMock,\n    ) as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2], [0.3, 0.4]]\n\n        await augmentation._process_api_response(api_response)\n\n        mock_embed.assert_called_once()\n        call_args = mock_embed.call_args[0][0]\n        assert \"User likes Pizza\" in call_args\n        assert \"User lives_in NYC\" in call_args\n\n\n@pytest.mark.asyncio\nasync def test_process_api_response_triples_prefers_content_field(augmentation):\n    api_response = {\n        \"entity\": {\n            \"facts\": [],\n            \"triples\": [\n                {\n                    \"subject\": {\"name\": \"User\", \"type\": \"Person\"},\n                    \"predicate\": \"likes\",\n                    \"object\": {\"name\": \"Pizza\", \"type\": \"Food\"},\n                    \"content\": \"User enjoys pizza on weekends\",\n                }\n            ],\n        }\n    }\n\n    with patch(\n        \"memori.memory.augmentation.augmentations.memori._augmentation.embed_texts\",\n        new_callable=AsyncMock,\n    ) as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2]]\n\n        result = await augmentation._process_api_response(api_response)\n\n        mock_embed.assert_called_once_with(\n            [\"User enjoys pizza on weekends\"],\n            model=augmentation.config.embeddings.model,\n            async_=True,\n        )\n        assert result.entity.facts == [\"User enjoys pizza on weekends\"]\n\n\n@pytest.mark.asyncio\nasync def test_schedule_entity_writes(augmentation, driver, augmentation_input):\n    ctx = AugmentationContext(payload=augmentation_input)\n    memories = Memories()\n    memories.entity.facts = [\"Fact 1\", \"Fact 2\"]\n    memories.entity.fact_embeddings = [[0.1, 0.2], [0.3, 0.4]]\n\n    await augmentation._schedule_entity_writes(ctx, driver, memories)\n\n    assert len(ctx.writes) == 1\n    assert ctx.writes[0][\"method_path\"] == \"entity_fact.create\"\n\n\n@pytest.mark.asyncio\nasync def test_schedule_entity_writes_with_semantic_triples(\n    augmentation, driver, augmentation_input\n):\n    ctx = AugmentationContext(payload=augmentation_input)\n    memories = Memories()\n    memories.entity.facts = [\"Fact 1\"]\n    memories.entity.fact_embeddings = [[0.1, 0.2]]\n\n    from memori.memory._struct import SemanticTriple\n\n    triple = SemanticTriple()\n    triple.subject_name = \"User\"\n    triple.predicate = \"likes\"\n    triple.object_name = \"Pizza\"\n    memories.entity.semantic_triples = [triple]\n\n    await augmentation._schedule_entity_writes(ctx, driver, memories)\n\n    assert len(ctx.writes) == 2\n    assert ctx.writes[0][\"method_path\"] == \"entity_fact.create\"\n    assert ctx.writes[1][\"method_path\"] == \"knowledge_graph.create\"\n\n\n@pytest.mark.asyncio\nasync def test_schedule_process_writes(augmentation, driver, augmentation_input):\n    ctx = AugmentationContext(payload=augmentation_input)\n    memories = Memories()\n    memories.process.attributes = [\"attr1\", \"attr2\"]\n\n    augmentation._schedule_process_writes(ctx, driver, memories)\n\n    assert len(ctx.writes) == 1\n    assert ctx.writes[0][\"method_path\"] == \"process_attribute.create\"\n\n\n@pytest.mark.asyncio\nasync def test_schedule_conversation_writes(augmentation, augmentation_input):\n    ctx = AugmentationContext(payload=augmentation_input)\n    memories = Memories()\n    memories.conversation.summary = \"New summary\"\n\n    augmentation._schedule_conversation_writes(ctx, memories)\n\n    assert len(ctx.writes) == 1\n    assert ctx.writes[0][\"method_path\"] == \"conversation.update\"\n    assert ctx.writes[0][\"args\"][0] == \"conv-123\"\n\n\n@pytest.mark.asyncio\nasync def test_schedule_writes_skips_if_no_data(\n    augmentation, driver, augmentation_input\n):\n    ctx = AugmentationContext(payload=augmentation_input)\n    memories = Memories()\n\n    await augmentation._schedule_entity_writes(ctx, driver, memories)\n    augmentation._schedule_process_writes(ctx, driver, memories)\n    augmentation._schedule_conversation_writes(ctx, memories)\n\n    assert len(ctx.writes) == 0\n"
  },
  {
    "path": "tests/memory/augmentation/test_base.py",
    "content": "import pytest\n\nfrom memori.memory.augmentation._base import AugmentationContext, BaseAugmentation\n\n\ndef test_base_augmentation_init_default():\n    aug = BaseAugmentation()\n    assert aug.enabled is True\n\n\ndef test_base_augmentation_init_disabled():\n    aug = BaseAugmentation(enabled=False)\n    assert aug.enabled is False\n\n\n@pytest.mark.asyncio\nasync def test_base_augmentation_process_not_implemented():\n    from memori.memory.augmentation.input import AugmentationInput\n\n    aug = BaseAugmentation()\n    mock_input = AugmentationInput(\n        conversation_id=None, entity_id=None, process_id=None, conversation_messages=[]\n    )\n    with pytest.raises(NotImplementedError):\n        await aug.process(AugmentationContext(payload=mock_input), None)\n"
  },
  {
    "path": "tests/memory/augmentation/test_handler.py",
    "content": "from memori._config import Config\nfrom memori.memory.augmentation._handler import handle_augmentation\nfrom memori.memory.augmentation.augmentations.memori.models import (\n    AttributionData,\n    AugmentationInputData,\n    ConversationMessage,\n    EntityData,\n    ProcessData,\n    SessionData,\n)\n\n\ndef test_handle_augmentation_cloud_posts_cloud_payload(mocker):\n    cfg = Config()\n    cfg.cloud = True\n    cfg.entity_id = \"abc\"\n    cfg.process_id = \"def\"\n    cfg.framework.provider = \"agno\"\n    cfg.llm.provider = \"openai\"\n    cfg.llm.version = \"gpt-4o-mini\"\n    cfg.platform.provider = \"local\"\n    cfg.request_num_backoff = 2\n    cfg.request_backoff_factor = 0\n    cfg.request_secs_timeout = 1\n    cfg.thread_pool_executor = mocker.Mock()\n\n    api = mocker.Mock()\n    api.post.return_value = 204\n    mocker.patch(\"memori.memory.augmentation._handler.Api\", return_value=api)\n\n    sleep = mocker.patch(\"memori.memory.augmentation._handler.time.sleep\")\n\n    handle_augmentation(\n        config=cfg,\n        payload=AugmentationInputData(\n            attribution=AttributionData(\n                entity=EntityData(id=\"abc\"),\n                process=ProcessData(id=\"def\"),\n            ),\n            messages=[\n                ConversationMessage(role=\"user\", content=\"hello\"),\n                ConversationMessage(role=\"assistant\", content=\"ok\"),\n            ],\n            session=SessionData(id=str(cfg.session_id)),\n        ),\n        kwargs={},\n        augmentation_manager=mocker.Mock(),\n        log_content=None,\n    )\n\n    cfg.thread_pool_executor.submit.assert_called_once()\n    fn, cfg_arg, payload_arg = cfg.thread_pool_executor.submit.call_args.args\n    fn(cfg_arg, payload_arg)\n\n    assert sleep.call_count == 0\n    assert api.post.call_count == 1\n    route, sent = api.post.call_args.args\n    assert route == \"cloud/augmentation\"\n    assert \"conversation\" in sent and \"messages\" in sent[\"conversation\"]\n    assert [m[\"role\"] for m in sent[\"conversation\"][\"messages\"]] == [\n        \"user\",\n        \"assistant\",\n    ]\n    assert sent[\"meta\"][\"framework\"][\"provider\"] == \"agno\"\n    assert sent[\"meta\"][\"llm\"][\"model\"][\"provider\"] == \"openai\"\n    assert sent[\"meta\"][\"platform\"][\"provider\"] == \"local\"\n    assert sent[\"meta\"][\"sdk\"]\n    assert sent[\"meta\"][\"storage\"] is None\n    assert sent[\"conversation\"][\"summary\"] is None\n\n\ndef test_handle_augmentation_non_cloud_enqueues(mocker):\n    cfg = Config()\n    cfg.cloud = False\n    cfg.entity_id = \"abc\"\n    cfg.process_id = \"def\"\n    cfg.cache.conversation_id = 123\n\n    aug = mocker.Mock()\n\n    handle_augmentation(\n        config=cfg,\n        payload=AugmentationInputData(\n            attribution=AttributionData(\n                entity=EntityData(id=\"abc\"),\n                process=ProcessData(id=\"def\"),\n            ),\n            messages=[ConversationMessage(role=\"user\", content=\"hi\")],\n            session=SessionData(id=str(cfg.session_id)),\n        ),\n        kwargs={},\n        augmentation_manager=aug,\n        log_content=None,\n    )\n\n    aug.enqueue.assert_called_once()\n    input_data = aug.enqueue.call_args.args[0]\n    assert input_data.conversation_id == 123\n    assert input_data.entity_id == \"abc\"\n    assert input_data.process_id == \"def\"\n    assert input_data.conversation_messages[0].role == \"user\"\n    assert input_data.conversation_messages[0].content == \"hi\"\n\n\ndef test_handle_augmentation_cloud_logs_error_on_failed_post(mocker):\n    cfg = Config()\n    cfg.cloud = True\n    cfg.entity_id = \"abc\"\n    cfg.process_id = \"def\"\n    cfg.thread_pool_executor = mocker.Mock()\n\n    api = mocker.Mock()\n    api.post.return_value = 500\n    mocker.patch(\"memori.memory.augmentation._handler.Api\", return_value=api)\n    err = mocker.patch(\"memori.memory.augmentation._handler.logger.error\")\n\n    handle_augmentation(\n        config=cfg,\n        payload=AugmentationInputData(\n            attribution=AttributionData(\n                entity=EntityData(id=\"abc\"),\n                process=ProcessData(id=\"def\"),\n            ),\n            messages=[\n                ConversationMessage(role=\"user\", content=\"hello\"),\n                ConversationMessage(role=\"assistant\", content=\"ok\"),\n            ],\n            session=SessionData(id=str(cfg.session_id)),\n        ),\n        kwargs={},\n        augmentation_manager=mocker.Mock(),\n        log_content=None,\n    )\n\n    fn, cfg_arg, payload_arg = cfg.thread_pool_executor.submit.call_args.args\n    fn(cfg_arg, payload_arg)\n    assert err.called\n\n\ndef test_handle_augmentation_no_attribution_noops(mocker):\n    cfg = Config()\n    cfg.cloud = True\n    cfg.entity_id = None\n    cfg.process_id = None\n    cfg.thread_pool_executor = mocker.Mock()\n\n    api = mocker.Mock()\n    mocker.patch(\"memori.memory.augmentation._handler.Api\", return_value=api)\n\n    handle_augmentation(\n        config=cfg,\n        payload=AugmentationInputData(\n            attribution=AttributionData(\n                entity=EntityData(id=None),\n                process=ProcessData(id=None),\n            ),\n            messages=[ConversationMessage(role=\"user\", content=\"hello\")],\n            session=SessionData(id=str(cfg.session_id)),\n        ),\n        kwargs={},\n        augmentation_manager=mocker.Mock(),\n        log_content=None,\n    )\n\n    cfg.thread_pool_executor.submit.assert_not_called()\n    api.post.assert_not_called()\n\n\ndef test_handle_augmentation_cloud_without_executor_posts_inline(mocker):\n    cfg = Config()\n    cfg.cloud = True\n    cfg.entity_id = \"abc\"\n    cfg.process_id = \"def\"\n    cfg.thread_pool_executor = None\n\n    api = mocker.Mock()\n    api.post.return_value = 204\n    mocker.patch(\"memori.memory.augmentation._handler.Api\", return_value=api)\n\n    handle_augmentation(\n        config=cfg,\n        payload=AugmentationInputData(\n            attribution=AttributionData(\n                entity=EntityData(id=\"abc\"),\n                process=ProcessData(id=\"def\"),\n            ),\n            messages=[ConversationMessage(role=\"user\", content=\"hello\")],\n            session=SessionData(id=str(cfg.session_id)),\n        ),\n        kwargs={},\n        augmentation_manager=mocker.Mock(),\n        log_content=None,\n    )\n\n    api.post.assert_called_once()\n"
  },
  {
    "path": "tests/memory/augmentation/test_manager.py",
    "content": "import asyncio\nimport concurrent.futures\nimport time\nfrom unittest.mock import Mock\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.memory.augmentation._manager import Manager\nfrom memori.memory.augmentation._runtime import AugmentationRuntime, get_runtime\n\n\ndef test_augmentation_runtime_init():\n    runtime = AugmentationRuntime()\n    assert runtime.loop is None\n    assert runtime.thread is None\n    assert runtime.semaphore is None\n    assert runtime.max_workers == 50\n\n\ndef test_manager_init():\n    config = Config()\n\n    manager = Manager(config)\n    manager.max_workers = 75\n\n    assert manager.config == config\n    assert manager.augmentations is not None\n    assert manager.conn_factory is None\n    assert manager.max_workers == 75\n\n\ndef test_manager_start_sets_max_workers():\n    config = Config()\n    mock_conn = Mock()\n\n    manager = Manager(config)\n    manager.max_workers = 75\n    manager.start(mock_conn)\n\n    runtime = get_runtime()\n    assert runtime.max_workers == 75\n\n\ndef test_manager_start_with_none_conn():\n    config = Config()\n    manager = Manager(config)\n\n    result = manager.start(None)\n\n    assert result == manager\n    assert manager.conn_factory is None\n    assert manager._active is False\n\n\ndef test_manager_start_with_conn():\n    config = Config()\n    manager = Manager(config)\n    mock_conn = Mock()\n\n    result = manager.start(mock_conn)\n\n    assert result == manager\n    assert manager.conn_factory == mock_conn\n    assert manager._active is True\n\n\ndef test_manager_enqueue_inactive():\n    from memori.memory.augmentation.input import AugmentationInput\n\n    config = Config()\n    manager = Manager(config)\n    payload = AugmentationInput(\n        conversation_id=None, entity_id=None, process_id=None, conversation_messages=[]\n    )\n\n    result = manager.enqueue(payload)\n\n    assert result == manager\n\n\ndef test_manager_enqueue_no_conn_factory():\n    from memori.memory.augmentation.input import AugmentationInput\n\n    config = Config()\n    manager = Manager(config)\n    manager._active = True\n    payload = AugmentationInput(\n        conversation_id=None, entity_id=None, process_id=None, conversation_messages=[]\n    )\n\n    result = manager.enqueue(payload)\n\n    assert result == manager\n\n\ndef test_runtime_ensure_started():\n    runtime = get_runtime()\n    original_thread = runtime.thread\n\n    runtime.ensure_started(50)\n\n    if original_thread is None:\n        assert runtime.thread is not None\n        time.sleep(0.1)\n        assert runtime.loop is not None\n        assert runtime.semaphore is not None\n\n\n@pytest.mark.asyncio\nasync def test_manager_process_augmentations_no_augmentations():\n    from memori.memory.augmentation.input import AugmentationInput\n\n    config = Config()\n    manager = Manager(config)\n    manager.conn_factory = Mock()\n    manager.augmentations = []\n    payload = AugmentationInput(\n        conversation_id=\"123\", entity_id=None, process_id=None, conversation_messages=[]\n    )\n\n    runtime = get_runtime()\n    original_semaphore = runtime.semaphore\n    runtime.semaphore = asyncio.Semaphore(10)\n\n    try:\n        await manager._process_augmentations(payload)\n    finally:\n        runtime.semaphore = original_semaphore\n\n\ndef test_manager_wait_no_pending_futures():\n    config = Config()\n    manager = Manager(config)\n\n    result = manager.wait()\n\n    assert result is True\n\n\ndef test_manager_wait_with_completed_futures():\n    config = Config()\n    manager = Manager(config)\n    mock_conn = Mock()\n    manager.start(mock_conn)\n\n    with concurrent.futures.ThreadPoolExecutor() as executor:\n        future = executor.submit(lambda: None)\n        manager._pending_futures.append(future)\n        future.add_done_callback(lambda f: manager._handle_augmentation_result(f))\n        time.sleep(0.1)\n\n        result = manager.wait(timeout=1.0)\n\n        assert result is True\n\n\ndef test_manager_wait_with_timeout():\n    config = Config()\n    manager = Manager(config)\n\n    with concurrent.futures.ThreadPoolExecutor() as executor:\n        future = executor.submit(time.sleep, 2)\n        manager._pending_futures.append(future)\n\n        result = manager.wait(timeout=0.1)\n\n        assert result is False\n\n\ndef test_manager_wait_for_db_writer_queue():\n    from queue import Queue\n\n    from memori.memory.augmentation._db_writer import WriteTask, get_db_writer\n\n    config = Config()\n    manager = Manager(config)\n    mock_conn = Mock()\n    manager.start(mock_conn)\n\n    db_writer = get_db_writer()\n    original_queue = db_writer.queue\n\n    try:\n        test_queue = Queue()\n        db_writer.queue = test_queue\n\n        task = WriteTask(\n            conn_factory=mock_conn, method_path=\"test.method\", args=(), kwargs={}\n        )\n        test_queue.put(task)\n\n        with concurrent.futures.ThreadPoolExecutor() as executor:\n            executor.submit(lambda: test_queue.get())\n\n            result = manager.wait(timeout=1.0)\n\n            assert result is True\n    finally:\n        db_writer.queue = original_queue\n"
  },
  {
    "path": "tests/memory/augmentation/test_manager_quota.py",
    "content": "import os\nimport time\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori._exceptions import QuotaExceededError\nfrom memori.memory.augmentation._manager import Manager\nfrom memori.memory.augmentation._message import ConversationMessage\nfrom memori.memory.augmentation.input import AugmentationInput\n\n\n@pytest.fixture\ndef mock_conn_factory():\n    return MagicMock()\n\n\n@pytest.fixture\ndef augmentation_input():\n    return AugmentationInput(\n        entity_id=\"user123\",\n        process_id=\"test-process\",\n        conversation_id=\"1\",\n        conversation_messages=[ConversationMessage(role=\"user\", content=\"test\")],\n        system_prompt=None,\n    )\n\n\ndef test_quota_error_prevents_subsequent_augmentations(\n    mock_conn_factory, augmentation_input\n):\n    if \"MEMORI_API_KEY\" in os.environ:\n        del os.environ[\"MEMORI_API_KEY\"]\n\n    config = Config()\n    manager = Manager(config)\n\n    with patch(\n        \"memori.memory.augmentation.augmentations.memori._augmentation.Api\"\n    ) as MockApi:\n        mock_api_instance = MockApi.return_value\n        mock_api_instance.augmentation_async = AsyncMock(\n            side_effect=QuotaExceededError(\"Anonymous user quota exceeded\")\n        )\n\n        with patch(\n            \"memori.memory.augmentation._manager.connection_context\"\n        ) as mock_ctx:\n            mock_driver = MagicMock()\n            mock_driver.conversation.conn.get_dialect.return_value = \"postgresql\"\n            mock_driver.conversation.read.return_value = None\n            mock_driver.entity.create.return_value = 1\n            mock_ctx.return_value.__enter__.return_value = (None, None, mock_driver)\n\n            manager.start(mock_conn_factory)\n\n            manager.enqueue(augmentation_input)\n\n            time.sleep(1.0)\n\n            assert manager._quota_error is not None, \"Quota error should be stored\"\n\n            with pytest.raises(\n                QuotaExceededError, match=\"Anonymous user quota exceeded\"\n            ):\n                manager.enqueue(augmentation_input)\n\n\ndef test_quota_error_does_not_prevent_when_authenticated():\n    os.environ[\"MEMORI_API_KEY\"] = \"test-key\"\n\n    try:\n        config = Config()\n\n        augmentation_input = AugmentationInput(\n            entity_id=\"user123\",\n            process_id=\"test-process\",\n            conversation_id=\"1\",\n            conversation_messages=[ConversationMessage(role=\"user\", content=\"test\")],\n            system_prompt=None,\n        )\n\n        with patch(\n            \"memori.memory.augmentation.augmentations.memori._augmentation.Api\"\n        ) as MockApi:\n            mock_api_instance = MockApi.return_value\n            mock_api_instance.augmentation_async = AsyncMock(return_value={})\n\n            with patch(\n                \"memori.memory.augmentation._manager.connection_context\"\n            ) as mock_ctx:\n                mock_driver = MagicMock()\n                mock_driver.conversation.conn.get_dialect.return_value = \"postgresql\"\n                mock_driver.conversation.read.return_value = None\n                mock_driver.entity.create.return_value = 1\n                mock_driver.process.create.return_value = 1\n                mock_ctx.return_value.__enter__.return_value = (None, None, mock_driver)\n\n                manager = Manager(config)\n                manager.start(lambda: MagicMock())\n\n                manager.enqueue(augmentation_input)\n                time.sleep(0.5)\n\n                manager.enqueue(augmentation_input)\n                time.sleep(0.5)\n\n                assert mock_api_instance.augmentation_async.call_count >= 1\n    finally:\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n"
  },
  {
    "path": "tests/memory/augmentation/test_models.py",
    "content": "from memori.memory.augmentation._models import (\n    AugmentationPayload,\n    ConversationData,\n    FrameworkData,\n    LlmData,\n    MetaData,\n    ModelData,\n    PlatformData,\n    SdkData,\n    SdkVersionData,\n    StorageData,\n    hash_id,\n)\n\n\ndef test_conversation_data_with_summary():\n    \"\"\"Test ConversationData with summary.\"\"\"\n    conversation = ConversationData(\n        messages=[{\"role\": \"user\", \"content\": \"test\"}],\n        summary=\"Test summary\",\n    )\n\n    assert conversation.messages == [{\"role\": \"user\", \"content\": \"test\"}]\n    assert conversation.summary == \"Test summary\"\n\n\ndef test_conversation_data_without_summary():\n    \"\"\"Test ConversationData without summary.\"\"\"\n    conversation = ConversationData(\n        messages=[{\"role\": \"user\", \"content\": \"test\"}],\n    )\n\n    assert conversation.messages == [{\"role\": \"user\", \"content\": \"test\"}]\n    assert conversation.summary is None\n\n\ndef test_model_data_structure():\n    \"\"\"Test ModelData with SDK version.\"\"\"\n    model = ModelData(\n        provider=\"openai\",\n        sdk=SdkVersionData(version=\"2.8.1\"),\n        version=\"gpt-4\",\n    )\n\n    assert model.provider == \"openai\"\n    assert model.sdk.version == \"2.8.1\"\n    assert model.version == \"gpt-4\"\n\n\ndef test_meta_data_defaults():\n    \"\"\"Test MetaData initializes with defaults.\"\"\"\n    meta = MetaData()\n\n    assert meta.framework.provider is None\n    assert meta.llm.model.provider is None\n    assert meta.platform.provider is None\n    assert meta.sdk.lang == \"python\"\n    assert meta.storage.cockroachdb is False\n\n\ndef test_augmentation_payload_to_dict():\n    \"\"\"Test AugmentationPayload.to_dict() produces correct structure.\"\"\"\n    conversation = ConversationData(\n        messages=[{\"role\": \"user\", \"content\": \"test\"}],\n        summary=\"Test summary\",\n    )\n\n    meta = MetaData(\n        framework=FrameworkData(provider=\"openai\"),\n        llm=LlmData(\n            model=ModelData(\n                provider=\"openai\",\n                sdk=SdkVersionData(version=\"2.8.1\"),\n                version=\"gpt-4\",\n            )\n        ),\n        platform=PlatformData(provider=\"nebius\"),\n        sdk=SdkData(lang=\"python\", version=\"3.0.3\"),\n        storage=StorageData(\n            cockroachdb=False,\n            dialect=\"postgresql\",\n        ),\n    )\n\n    payload = AugmentationPayload(conversation=conversation, meta=meta)\n    result = payload.to_dict()\n\n    assert result[\"conversation\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"test\"}]\n    assert result[\"conversation\"][\"summary\"] == \"Test summary\"\n    assert result[\"meta\"][\"framework\"][\"provider\"] == \"openai\"\n    assert result[\"meta\"][\"llm\"][\"model\"][\"provider\"] == \"openai\"\n    assert result[\"meta\"][\"llm\"][\"model\"][\"sdk\"][\"version\"] == \"2.8.1\"\n    assert result[\"meta\"][\"llm\"][\"model\"][\"version\"] == \"gpt-4\"\n    assert result[\"meta\"][\"platform\"][\"provider\"] == \"nebius\"\n    assert result[\"meta\"][\"sdk\"][\"lang\"] == \"python\"\n    assert result[\"meta\"][\"sdk\"][\"version\"] == \"3.0.3\"\n    assert result[\"meta\"][\"storage\"][\"cockroachdb\"] is False\n    assert result[\"meta\"][\"storage\"][\"dialect\"] == \"postgresql\"\n\n\ndef test_augmentation_payload_with_none_values():\n    \"\"\"Test payload handles None values correctly.\"\"\"\n    conversation = ConversationData(\n        messages=[],\n        summary=None,\n    )\n\n    meta = MetaData(\n        framework=FrameworkData(provider=None),\n        llm=LlmData(\n            model=ModelData(\n                provider=None,\n                sdk=SdkVersionData(version=None),\n                version=None,\n            )\n        ),\n        platform=PlatformData(provider=None),\n        sdk=SdkData(lang=\"python\", version=None),\n        storage=StorageData(\n            cockroachdb=False,\n            dialect=None,\n        ),\n    )\n\n    payload = AugmentationPayload(conversation=conversation, meta=meta)\n    result = payload.to_dict()\n\n    assert result[\"conversation\"][\"summary\"] is None\n    assert result[\"meta\"][\"framework\"][\"provider\"] is None\n    assert result[\"meta\"][\"llm\"][\"model\"][\"provider\"] is None\n    assert result[\"meta\"][\"llm\"][\"model\"][\"sdk\"][\"version\"] is None\n    assert result[\"meta\"][\"platform\"][\"provider\"] is None\n\n\ndef test_sdk_data_default_lang():\n    \"\"\"Test SdkData defaults to python.\"\"\"\n    sdk = SdkData(version=\"3.0.3\")\n\n    assert sdk.lang == \"python\"\n    assert sdk.version == \"3.0.3\"\n\n\ndef test_storage_data_defaults():\n    \"\"\"Test StorageData default values.\"\"\"\n    storage = StorageData()\n\n    assert storage.cockroachdb is False\n    assert storage.dialect is None\n\n\ndef test_hash_id_returns_sha256():\n    \"\"\"Test hash_id returns SHA-256 hex digest.\"\"\"\n    result = hash_id(\"user_123\")\n\n    assert result is not None\n    assert len(result) == 64\n    assert all(c in \"0123456789abcdef\" for c in result)\n\n\ndef test_hash_id_is_consistent():\n    \"\"\"Test hash_id returns same hash for same input.\"\"\"\n    input_value = \"user_123\"\n\n    hash1 = hash_id(input_value)\n    hash2 = hash_id(input_value)\n\n    assert hash1 == hash2\n\n\ndef test_hash_id_different_inputs():\n    \"\"\"Test hash_id returns different hashes for different inputs.\"\"\"\n    hash1 = hash_id(\"user_123\")\n    hash2 = hash_id(\"user_456\")\n\n    assert hash1 != hash2\n\n\ndef test_hash_id_none_input():\n    \"\"\"Test hash_id returns None for None input.\"\"\"\n    result = hash_id(None)\n\n    assert result is None\n\n\ndef test_hash_id_empty_string():\n    \"\"\"Test hash_id returns None for empty string.\"\"\"\n    result = hash_id(\"\")\n\n    assert result is None\n\n\ndef test_meta_data_with_hashed_ids():\n    \"\"\"Test MetaData includes entity and process IDs in attribution.\"\"\"\n    from memori.memory.augmentation._models import (\n        AttributionData,\n        EntityData,\n        ProcessData,\n    )\n\n    meta = MetaData(\n        attribution=AttributionData(\n            entity=EntityData(id=\"hashed_entity\"),\n            process=ProcessData(id=\"hashed_process\"),\n        ),\n    )\n\n    assert meta.attribution.entity.id == \"hashed_entity\"\n    assert meta.attribution.process.id == \"hashed_process\"\n\n\ndef test_augmentation_payload_includes_hashed_ids():\n    \"\"\"Test AugmentationPayload.to_dict() includes hashed entity and process IDs.\"\"\"\n    from memori.memory.augmentation._models import (\n        AttributionData,\n        EntityData,\n        ProcessData,\n    )\n\n    conversation = ConversationData(messages=[], summary=None)\n    meta = MetaData(\n        attribution=AttributionData(\n            entity=EntityData(id=\"abc123\"),\n            process=ProcessData(id=\"xyz789\"),\n        ),\n    )\n\n    payload = AugmentationPayload(conversation=conversation, meta=meta)\n    result = payload.to_dict()\n\n    assert result[\"meta\"][\"attribution\"][\"entity\"][\"id\"] == \"abc123\"\n    assert result[\"meta\"][\"attribution\"][\"process\"][\"id\"] == \"xyz789\"\n"
  },
  {
    "path": "tests/memory/augmentation/test_quota_propagation.py",
    "content": "import os\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori._exceptions import QuotaExceededError\nfrom memori.memory.augmentation._base import AugmentationContext\nfrom memori.memory.augmentation._message import ConversationMessage\nfrom memori.memory.augmentation.augmentations.memori._augmentation import (\n    AdvancedAugmentation,\n)\nfrom memori.memory.augmentation.input import AugmentationInput\n\n\n@pytest.mark.asyncio\nasync def test_quota_exceeded_error_propagates_for_anonymous_users():\n    if \"MEMORI_API_KEY\" in os.environ:\n        del os.environ[\"MEMORI_API_KEY\"]\n\n    config = Config()\n    augmentation = AdvancedAugmentation(config=config)\n\n    payload = AugmentationInput(\n        entity_id=\"user123\",\n        process_id=\"test-process\",\n        conversation_id=\"1\",\n        conversation_messages=[ConversationMessage(role=\"user\", content=\"test\")],\n    )\n    ctx = AugmentationContext(payload=payload)\n\n    mock_driver = MagicMock()\n    mock_driver.conversation.conn.get_dialect.return_value = \"postgresql\"\n    mock_driver.conversation.read.return_value = None\n\n    with patch(\n        \"memori.memory.augmentation.augmentations.memori._augmentation.Api\"\n    ) as MockApi:\n        mock_api_instance = MockApi.return_value\n        mock_api_instance.augmentation_async = AsyncMock(\n            side_effect=QuotaExceededError(\"Quota exceeded for anonymous user\")\n        )\n\n        with pytest.raises(QuotaExceededError) as exc_info:\n            await augmentation.process(ctx, mock_driver)\n\n        assert \"Quota exceeded for anonymous user\" in str(exc_info.value)\n\n\n@pytest.mark.asyncio\nasync def test_other_exceptions_caught_gracefully():\n    config = Config()\n    augmentation = AdvancedAugmentation(config=config)\n\n    payload = AugmentationInput(\n        entity_id=\"user123\",\n        process_id=\"test-process\",\n        conversation_id=\"1\",\n        conversation_messages=[ConversationMessage(role=\"user\", content=\"test\")],\n    )\n    ctx = AugmentationContext(payload=payload)\n\n    mock_driver = MagicMock()\n    mock_driver.conversation.conn.get_dialect.return_value = \"postgresql\"\n    mock_driver.conversation.read.return_value = None\n\n    with patch(\n        \"memori.memory.augmentation.augmentations.memori._augmentation.Api\"\n    ) as MockApi:\n        mock_api_instance = MockApi.return_value\n        mock_api_instance.augmentation_async = AsyncMock(\n            side_effect=RuntimeError(\"Some other error\")\n        )\n\n        result = await augmentation.process(ctx, mock_driver)\n        assert result == ctx\n"
  },
  {
    "path": "tests/memory/augmentation/test_registry.py",
    "content": "from memori.memory.augmentation._base import BaseAugmentation\nfrom memori.memory.augmentation._registry import Registry\n\n\ndef test_registry_register():\n    original_count = len(Registry._augmentations)\n\n    @Registry.register(\"test_aug\")\n    class TestAugmentation(BaseAugmentation):\n        async def process(self, ctx, driver):\n            return ctx\n\n    assert \"test_aug\" in Registry._augmentations\n    assert Registry._augmentations[\"test_aug\"] == TestAugmentation\n\n    del Registry._augmentations[\"test_aug\"]\n    assert len(Registry._augmentations) == original_count\n\n\ndef test_registry_augmentations():\n    original_augs = Registry._augmentations.copy()\n    Registry._augmentations = {}\n\n    @Registry.register(\"aug1\")\n    class Aug1(BaseAugmentation):\n        async def process(self, ctx, driver):\n            return ctx\n\n    @Registry.register(\"aug2\")\n    class Aug2(BaseAugmentation):\n        async def process(self, ctx, driver):\n            return ctx\n\n    registry = Registry()\n    augs = registry.augmentations()\n\n    assert len(augs) == 2\n    assert isinstance(augs[0], BaseAugmentation)\n    assert isinstance(augs[1], BaseAugmentation)\n\n    Registry._augmentations = original_augs\n"
  },
  {
    "path": "tests/memory/test_conversation_messages.py",
    "content": "import json\n\nfrom memori.memory._conversation_messages import parse_payload_conversation_messages\n\n\ndef test_parse_payload_conversation_messages_stringifies(mocker):\n    payload = {\n        \"conversation\": {\"client\": {\"provider\": \"x\", \"title\": \"y\"}},\n    }\n\n    adapter = mocker.Mock()\n    adapter.get_formatted_query.return_value = [\n        {\"role\": \"system\", \"content\": \"ignore\"},\n        {\"role\": \"user\", \"content\": {\"a\": 1}},\n        {\"role\": \"assistant\", \"content\": [\"x\", 2]},\n    ]\n    adapter.get_formatted_response.return_value = [\n        {\"role\": \"assistant\", \"type\": \"output_text\", \"text\": \"ok\"},\n    ]\n\n    messages = list(parse_payload_conversation_messages(payload, adapter=adapter))\n\n    assert messages == [\n        {\"role\": \"system\", \"type\": None, \"text\": \"ignore\"},\n        {\"role\": \"user\", \"type\": None, \"text\": json.dumps({\"a\": 1})},\n        {\"role\": \"assistant\", \"type\": None, \"text\": json.dumps([\"x\", 2])},\n        {\"role\": \"assistant\", \"type\": \"output_text\", \"text\": \"ok\"},\n    ]\n\n    adapter.get_formatted_query.assert_called_once_with(payload)\n    adapter.get_formatted_response.assert_called_once_with(payload)\n\n\ndef test_parse_payload_conversation_messages_uses_registry_when_no_adapter(mocker):\n    payload = {\n        \"conversation\": {\"client\": {\"provider\": \"agno\", \"title\": \"OpenAIChat\"}},\n    }\n\n    adapter = mocker.Mock()\n    adapter.get_formatted_query.return_value = []\n    adapter.get_formatted_response.return_value = []\n\n    registry = mocker.Mock()\n    registry.adapter.return_value = adapter\n\n    list(parse_payload_conversation_messages(payload, registry=registry))\n\n    registry.adapter.assert_called_once_with(\"agno\", \"OpenAIChat\")\n\n\ndef test_parse_payload_conversation_messages_passthrough_existing_messages():\n    payload = {\n        \"conversation\": {\n            \"client\": {\"provider\": \"x\", \"title\": \"y\"},\n            \"messages\": [\n                {\"role\": \"user\", \"type\": \"text\", \"text\": \"hi\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"ok\"},\n            ],\n        }\n    }\n\n    assert list(parse_payload_conversation_messages(payload)) == [\n        {\"role\": \"user\", \"type\": \"text\", \"text\": \"hi\"},\n        {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"ok\"},\n    ]\n\n\ndef test_parse_payload_conversation_messages_preserves_response_type(mocker):\n    payload = {\n        \"conversation\": {\"client\": {\"provider\": \"x\", \"title\": \"y\"}},\n    }\n\n    adapter = mocker.Mock()\n    adapter.get_formatted_query.return_value = []\n    adapter.get_formatted_response.return_value = [\n        {\"role\": \"assistant\", \"text\": \"hello\"},\n        {\"role\": \"assistant\", \"type\": \"output_text\", \"text\": \"ok\"},\n    ]\n\n    messages = list(parse_payload_conversation_messages(payload, adapter=adapter))\n\n    assert messages == [\n        {\"role\": \"assistant\", \"type\": None, \"text\": \"hello\"},\n        {\"role\": \"assistant\", \"type\": \"output_text\", \"text\": \"ok\"},\n    ]\n"
  },
  {
    "path": "tests/memory/test_manager_enterprise_retry.py",
    "content": "import pytest\n\nfrom memori._config import Config\nfrom memori._exceptions import MemoriApiError\nfrom memori.memory._manager import Manager\n\n\ndef test_manager_cloud_retries_until_201(mocker):\n    cfg = Config()\n    cfg.cloud = True\n    cfg.request_num_backoff = 3\n    cfg.request_backoff_factor = 0.01\n\n    api = mocker.Mock()\n    api.post.side_effect = [500, 200, 201]\n    mocker.patch(\"memori.memory._manager.Api\", return_value=api)\n    sleep = mocker.patch(\"memori.memory._manager.time.sleep\")\n\n    out = Manager(cfg)._handle_cloud({\"messages\": []})\n\n    assert out is None\n    assert api.post.call_count == 3\n    assert sleep.call_count == 2\n\n\ndef test_manager_cloud_raises_after_exhausting_attempts(mocker):\n    cfg = Config()\n    cfg.cloud = True\n    cfg.request_num_backoff = 2\n    cfg.request_backoff_factor = 0\n\n    api = mocker.Mock()\n    api.post.return_value = 200\n    mocker.patch(\"memori.memory._manager.Api\", return_value=api)\n    mocker.patch(\"memori.memory._manager.time.sleep\")\n\n    with pytest.raises(MemoriApiError, match=\"Expected 201\"):\n        Manager(cfg)._handle_cloud({\"messages\": []})\n"
  },
  {
    "path": "tests/memory/test_memory_augmentation_db_writer.py",
    "content": "from contextlib import contextmanager\nfrom types import SimpleNamespace\nfrom unittest.mock import Mock\n\nfrom memori.memory.augmentation._db_writer import DbWriterRuntime, WriteTask\n\n\nclass TestWriteTask:\n    def test_write_task_execution(self):\n        mock_driver = Mock()\n        mock_method = Mock(return_value=\"success\")\n        mock_driver.conversation.message.create = mock_method\n\n        task = WriteTask(\n            conn_factory=lambda: object(),\n            method_path=\"conversation.message.create\",\n            args=(1, 2),\n            kwargs={\"key\": \"value\"},\n        )\n\n        result = task.execute(mock_driver)\n\n        mock_method.assert_called_once_with(1, 2, key=\"value\")\n        assert result == \"success\"\n\n\nclass TestDbWriterRuntime:\n    def test_enqueue_write_success(self):\n        import queue as queue_module\n\n        runtime = DbWriterRuntime()\n        runtime.queue = queue_module.Queue(maxsize=1000)\n        task = WriteTask(\n            conn_factory=lambda: object(),\n            method_path=\"conversation.message.create\",\n        )\n\n        result = runtime.enqueue_write(task, timeout=1.0)\n\n        assert result is True\n        assert runtime.queue.qsize() == 1\n\n    def test_enqueue_write_full_queue(self):\n        import queue as queue_module\n\n        runtime = DbWriterRuntime()\n        runtime.queue = Mock()\n        runtime.queue.put = Mock(side_effect=queue_module.Full(\"Queue full\"))\n        runtime.queue.maxsize = 1000\n\n        task = WriteTask(\n            conn_factory=lambda: object(),\n            method_path=\"conversation.message.create\",\n        )\n        result = runtime.enqueue_write(task, timeout=1.0)\n\n        assert result is False\n\n    def test_drain_batches_opens_connection_only_when_work(self, mocker):\n        import queue as queue_module\n\n        runtime = DbWriterRuntime()\n        runtime.queue = queue_module.Queue(maxsize=1000)\n        runtime.batch_timeout = 0.01\n        runtime.batch_size = 100\n\n        driver = SimpleNamespace(\n            conversation=SimpleNamespace(\n                message=SimpleNamespace(create=mocker.Mock(return_value=None))\n            )\n        )\n        adapter = SimpleNamespace(\n            flush=mocker.Mock(),\n            commit=mocker.Mock(),\n            rollback=mocker.Mock(),\n        )\n\n        events: list[str] = []\n\n        @contextmanager\n        def fake_connection_context(_factory):\n            events.append(\"enter\")\n            yield object(), adapter, driver\n            events.append(\"exit\")\n\n        mocker.patch(\n            \"memori.memory.augmentation._db_writer.connection_context\",\n            fake_connection_context,\n        )\n\n        # First drain: no work -> no connection open.\n        runtime._drain_batches()\n        assert events == []\n\n        def factory():\n            return object()\n\n        runtime.enqueue_write(\n            WriteTask(conn_factory=factory, method_path=\"conversation.message.create\")\n        )\n        runtime.enqueue_write(\n            WriteTask(conn_factory=factory, method_path=\"conversation.message.create\")\n        )\n\n        runtime._drain_batches()\n\n        assert events == [\"enter\", \"exit\"]\n        assert driver.conversation.message.create.call_count == 2\n        adapter.flush.assert_called()\n        adapter.commit.assert_called()\n"
  },
  {
    "path": "tests/memory/test_memory_struct.py",
    "content": "from memori.memory._struct import Conversation, Entity, Memories, Process\n\n\ndef test_conversation_configure_from_advanced_augmentation():\n    conversation = Conversation().configure_from_advanced_augmentation({})\n    assert conversation.summary is None\n\n    conversation = Conversation().configure_from_advanced_augmentation(\n        {\"conversation\": {}}\n    )\n    assert conversation.summary is None\n\n    conversation = Conversation().configure_from_advanced_augmentation(\n        {\"conversation\": {\"summary\": \"Abc def\"}}\n    )\n    assert conversation.summary == \"Abc def\"\n\n\ndef test_entity_configure_from_advanced_augmentation():\n    entity = Entity().configure_from_advanced_augmentation({})\n    assert entity.facts == []\n    assert entity.semantic_triples == []\n\n    entity = Entity().configure_from_advanced_augmentation({\"entity\": {}})\n    assert entity.facts == []\n    assert entity.semantic_triples == []\n\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"facts\": [\"Abc def\", \"ghi\", \"jkl\"],\n                \"semantic_triples\": [\n                    {\n                        \"subject\": {\"name\": \"Mno\", \"type\": \"Pqr\"},\n                        \"predicate\": \"stu\",\n                        \"object\": {\"name\": \"vwx\", \"type\": \"Yza\"},\n                    }\n                ],\n            }\n        }\n    )\n    assert entity.facts == [\"Abc def\", \"ghi\", \"jkl\"]\n    assert len(entity.semantic_triples) == 1\n    assert entity.semantic_triples[0].subject_name == \"Mno\"\n    assert entity.semantic_triples[0].subject_type == \"pqr\"\n    assert entity.semantic_triples[0].predicate == \"stu\"\n    assert entity.semantic_triples[0].object_name == \"vwx\"\n    assert entity.semantic_triples[0].object_type == \"yza\"\n\n\ndef test_process_configure_from_advanced_augmentation():\n    process = Process().configure_from_advanced_augmentation({})\n    assert process.attributes == []\n\n    process = Process().configure_from_advanced_augmentation({\"process\": {}})\n    assert process.attributes == []\n\n    process = Process().configure_from_advanced_augmentation(\n        {\"process\": {\"attributes\": [\"Abc\", \"def\"]}}\n    )\n    assert process.attributes == [\"Abc\", \"def\"]\n\n\ndef test_memories_configure_from_advanced_augmentation():\n    memories = Memories().configure_from_advanced_augmentation(\n        {\n            \"conversation\": {\"summary\": \"Abc def\"},\n            \"entity\": {\n                \"facts\": [\"Abc def\", \"ghi\", \"jkl\"],\n                \"semantic_triples\": [\n                    {\n                        \"subject\": {\"name\": \"Mno\", \"type\": \"Pqr\"},\n                        \"predicate\": \"stu\",\n                        \"object\": {\"name\": \"vwx\", \"type\": \"Yza\"},\n                    }\n                ],\n            },\n            \"process\": {\"attributes\": [\"Abc\", \"def\"]},\n        }\n    )\n\n    assert memories.conversation.summary == \"Abc def\"\n    assert memories.entity.facts == [\"Abc def\", \"ghi\", \"jkl\"]\n    assert len(memories.entity.semantic_triples) == 1\n    assert memories.entity.semantic_triples[0].subject_name == \"Mno\"\n    assert memories.entity.semantic_triples[0].subject_type == \"pqr\"\n    assert memories.entity.semantic_triples[0].predicate == \"stu\"\n    assert memories.entity.semantic_triples[0].object_name == \"vwx\"\n    assert memories.entity.semantic_triples[0].object_type == \"yza\"\n    assert memories.process.attributes == [\"Abc\", \"def\"]\n"
  },
  {
    "path": "tests/memory/test_memory_struct_triples.py",
    "content": "from memori.memory._struct import Entity\n\n\ndef test_entity_configure_with_triples_field():\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"triples\": [\n                    {\n                        \"subject\": {\"name\": \"User\", \"type\": \"Person\"},\n                        \"predicate\": \"likes\",\n                        \"object\": {\"name\": \"Pizza\", \"type\": \"Food\"},\n                    }\n                ]\n            }\n        }\n    )\n\n    assert len(entity.semantic_triples) == 1\n    assert entity.semantic_triples[0].subject_name == \"User\"\n    assert entity.semantic_triples[0].subject_type == \"person\"\n    assert entity.semantic_triples[0].predicate == \"likes\"\n    assert entity.semantic_triples[0].object_name == \"Pizza\"\n    assert entity.semantic_triples[0].object_type == \"food\"\n    assert len(entity.facts) == 1\n    assert entity.facts[0] == \"User likes Pizza\"\n\n\ndef test_entity_configure_with_both_semantic_triples_and_triples():\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"semantic_triples\": [\n                    {\n                        \"subject\": {\"name\": \"Alice\", \"type\": \"Person\"},\n                        \"predicate\": \"works_at\",\n                        \"object\": {\"name\": \"TechCorp\", \"type\": \"Organization\"},\n                    }\n                ],\n                \"triples\": [\n                    {\n                        \"subject\": {\"name\": \"Bob\", \"type\": \"Person\"},\n                        \"predicate\": \"lives_in\",\n                        \"object\": {\"name\": \"NYC\", \"type\": \"City\"},\n                    }\n                ],\n            }\n        }\n    )\n\n    assert len(entity.semantic_triples) == 2\n    assert entity.semantic_triples[0].subject_name == \"Alice\"\n    assert entity.semantic_triples[1].subject_name == \"Bob\"\n    assert len(entity.facts) == 1\n    assert entity.facts[0] == \"Bob lives_in NYC\"\n\n\ndef test_entity_triples_generates_fact_text():\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"triples\": [\n                    {\n                        \"subject\": {\"name\": \"Car\", \"type\": \"Vehicle\"},\n                        \"predicate\": \"has_color\",\n                        \"object\": {\"name\": \"Red\", \"type\": \"Color\"},\n                    },\n                    {\n                        \"subject\": {\"name\": \"House\", \"type\": \"Building\"},\n                        \"predicate\": \"located_in\",\n                        \"object\": {\"name\": \"Seattle\", \"type\": \"City\"},\n                    },\n                ]\n            }\n        }\n    )\n\n    assert len(entity.facts) == 2\n    assert \"Car has_color Red\" in entity.facts\n    assert \"House located_in Seattle\" in entity.facts\n\n\ndef test_entity_triples_prefers_content_field_for_fact_text():\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"triples\": [\n                    {\n                        \"subject\": {\"name\": \"User\", \"type\": \"Person\"},\n                        \"predicate\": \"likes\",\n                        \"object\": {\"name\": \"Pizza\", \"type\": \"Food\"},\n                        \"content\": \"User enjoys pizza on weekends\",\n                    }\n                ]\n            }\n        }\n    )\n\n    assert len(entity.facts) == 1\n    assert entity.facts[0] == \"User enjoys pizza on weekends\"\n\n\ndef test_entity_triples_with_existing_facts():\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"facts\": [\"Existing fact 1\", \"Existing fact 2\"],\n                \"triples\": [\n                    {\n                        \"subject\": {\"name\": \"John\", \"type\": \"Person\"},\n                        \"predicate\": \"drives\",\n                        \"object\": {\"name\": \"Tesla\", \"type\": \"Car\"},\n                    }\n                ],\n            }\n        }\n    )\n\n    assert len(entity.facts) == 3\n    assert \"Existing fact 1\" in entity.facts\n    assert \"Existing fact 2\" in entity.facts\n    assert \"John drives Tesla\" in entity.facts\n\n\ndef test_entity_triples_with_missing_fields_skipped():\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"triples\": [\n                    {\n                        \"subject\": {\"name\": \"Valid\", \"type\": \"Person\"},\n                        \"predicate\": \"likes\",\n                        \"object\": {\"name\": \"Coffee\", \"type\": \"Beverage\"},\n                    },\n                    {\"subject\": {\"name\": \"Invalid\"}, \"predicate\": \"missing_object\"},\n                    {\"predicate\": \"no_subject_or_object\"},\n                ]\n            }\n        }\n    )\n\n    assert len(entity.semantic_triples) == 1\n    assert entity.semantic_triples[0].subject_name == \"Valid\"\n    assert len(entity.facts) == 1\n    assert entity.facts[0] == \"Valid likes Coffee\"\n\n\ndef test_entity_semantic_triples_only_not_added_to_facts():\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"semantic_triples\": [\n                    {\n                        \"subject\": {\"name\": \"Alice\", \"type\": \"Person\"},\n                        \"predicate\": \"knows\",\n                        \"object\": {\"name\": \"Bob\", \"type\": \"Person\"},\n                    }\n                ]\n            }\n        }\n    )\n\n    assert len(entity.semantic_triples) == 1\n    assert len(entity.facts) == 0\n\n\ndef test_entity_triples_empty_list():\n    entity = Entity().configure_from_advanced_augmentation({\"entity\": {\"triples\": []}})\n\n    assert len(entity.semantic_triples) == 0\n    assert len(entity.facts) == 0\n\n\ndef test_entity_triples_type_normalization():\n    entity = Entity().configure_from_advanced_augmentation(\n        {\n            \"entity\": {\n                \"triples\": [\n                    {\n                        \"subject\": {\"name\": \"Test\", \"type\": \"UPPERCASE\"},\n                        \"predicate\": \"test_predicate\",\n                        \"object\": {\"name\": \"Object\", \"type\": \"MiXeDCaSe\"},\n                    }\n                ]\n            }\n        }\n    )\n\n    assert entity.semantic_triples[0].subject_type == \"uppercase\"\n    assert entity.semantic_triples[0].object_type == \"mixedcase\"\n"
  },
  {
    "path": "tests/memory/test_memory_writer.py",
    "content": "from memori.memory._writer import Writer\n\n\ndef test_execute(config, mocker):\n    Writer(config).execute(\n        {\n            \"messages\": [\n                {\"role\": \"user\", \"type\": None, \"text\": \"abc\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"def\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"ghi\"},\n            ]\n        }\n    )\n\n    assert config.cache.session_id is not None\n    assert config.cache.conversation_id is not None\n\n    assert config.storage.driver.session.create.called\n    assert config.storage.driver.conversation.create.called\n    assert config.storage.driver.conversation.message.create.call_count == 3\n\n    calls = config.storage.driver.conversation.message.create.call_args_list\n    assert calls[0][0][1] == \"user\"\n    assert calls[0][0][3] == \"abc\"\n    assert calls[1][0][1] == \"assistant\"\n    assert calls[1][0][3] == \"def\"\n    assert calls[2][0][1] == \"assistant\"\n    assert calls[2][0][3] == \"ghi\"\n\n\ndef test_execute_with_entity_and_process(config, mocker):\n    config.entity_id = \"123\"\n    config.process_id = \"456\"\n\n    Writer(config).execute(\n        {\n            \"messages\": [\n                {\"role\": \"user\", \"type\": None, \"text\": \"abc\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"def\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"ghi\"},\n            ]\n        }\n    )\n\n    assert config.cache.entity_id is not None\n    assert config.cache.process_id is not None\n    assert config.cache.session_id is not None\n    assert config.cache.conversation_id is not None\n\n    assert config.storage.driver.entity.create.called\n    assert config.storage.driver.entity.create.call_args[0][0] == \"123\"\n\n    assert config.storage.driver.process.create.called\n    assert config.storage.driver.process.create.call_args[0][0] == \"456\"\n\n    assert config.storage.driver.session.create.called\n    session_call_args = config.storage.driver.session.create.call_args[0]\n    assert session_call_args[1] == config.cache.entity_id\n    assert session_call_args[2] == config.cache.process_id\n\n    assert config.storage.driver.conversation.message.create.call_count == 3\n\n\ndef test_execute_includes_system_messages(config, mocker):\n    Writer(config).execute(\n        {\n            \"messages\": [\n                {\n                    \"role\": \"system\",\n                    \"type\": None,\n                    \"text\": \"You are a helpful assistant\",\n                },\n                {\"role\": \"user\", \"type\": None, \"text\": \"Hello\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"Hi there!\"},\n            ]\n        }\n    )\n\n    assert config.storage.driver.conversation.message.create.call_count == 3\n\n    calls = config.storage.driver.conversation.message.create.call_args_list\n    assert calls[0][0][1] == \"system\"\n    assert calls[0][0][3] == \"You are a helpful assistant\"\n    assert calls[1][0][1] == \"user\"\n    assert calls[1][0][3] == \"Hello\"\n    assert calls[2][0][1] == \"assistant\"\n    assert calls[2][0][3] == \"Hi there!\"\n\n\ndef test_execute_writes_response_type(config, mocker):\n    Writer(config).execute(\n        {\n            \"messages\": [\n                {\"role\": \"user\", \"type\": None, \"text\": \"hello\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"ok\"},\n            ]\n        }\n    )\n\n    calls = config.storage.driver.conversation.message.create.call_args_list\n    assert calls[0][0][2] is None\n    assert calls[1][0][2] == \"text\"\n\n\ndef test_execute_multiple_turns_ingests_all_messages(config, mocker):\n    \"\"\"Multiple conversation turns reuse the same conversation_id and write all messages.\"\"\"\n    conversation_id = 123\n    config.storage.driver.conversation.create.return_value = conversation_id\n    config.cache.conversation_id = None\n\n    # First turn\n    Writer(config).execute(\n        {\n            \"messages\": [\n                {\"role\": \"user\", \"type\": None, \"text\": \"Hello\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"Hi there!\"},\n            ]\n        }\n    )\n\n    assert config.cache.conversation_id == conversation_id\n    assert config.storage.driver.conversation.message.create.call_count == 2\n    calls1 = config.storage.driver.conversation.message.create.call_args_list\n    assert calls1[0][0][3] == \"Hello\"\n    assert calls1[1][0][3] == \"Hi there!\"\n\n    # Second turn: same conversation_id, new messages\n    config.storage.driver.conversation.message.create.reset_mock()\n    Writer(config).execute(\n        {\n            \"messages\": [\n                {\"role\": \"user\", \"type\": None, \"text\": \"What's the weather?\"},\n                {\"role\": \"assistant\", \"type\": \"text\", \"text\": \"I don't have access.\"},\n            ]\n        }\n    )\n\n    assert config.cache.conversation_id == conversation_id\n    assert config.storage.driver.conversation.message.create.call_count == 2\n    calls2 = config.storage.driver.conversation.message.create.call_args_list\n    assert calls2[0][0][3] == \"What's the weather?\"\n    assert calls2[1][0][3] == \"I don't have access.\"\n"
  },
  {
    "path": "tests/memory/test_recall.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nfrom unittest.mock import Mock, patch\n\nimport pytest\nfrom sqlalchemy.exc import OperationalError\n\nfrom memori._config import Config\nfrom memori.memory.recall import MAX_RETRIES, RETRY_BACKOFF_BASE, Recall\nfrom memori.search import FactSearchResult\n\n\ndef test_recall_init():\n    config = Config()\n    recall = Recall(config)\n    assert recall.config is config\n\n\ndef test_search_facts_no_storage():\n    config = Config()\n    config.storage = None\n    recall = Recall(config)\n\n    result = recall.search_facts(\"test query\")\n\n    assert result == []\n\n\ndef test_search_facts_no_driver():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = None\n    recall = Recall(config)\n\n    result = recall.search_facts(\"test query\")\n\n    assert result == []\n\n\ndef test_search_facts_no_entity_id_in_config():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.entity_id = None\n    recall = Recall(config)\n\n    result = recall.search_facts(\"test query\", entity_id=None)\n\n    assert result == []\n\n\ndef test_search_facts_entity_create_returns_none():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = None\n    config.entity_id = \"test-entity\"\n    recall = Recall(config)\n\n    result = recall.search_facts(\"test query\")\n\n    assert result == []\n    config.storage.driver.entity.create.assert_called_once_with(\"test-entity\")\n\n\ndef test_search_facts_uses_provided_entity_id():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.entity_id = None\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.return_value = [\n                FactSearchResult(\n                    id=1,\n                    content=\"fact 1\",\n                    similarity=0.9,\n                    rank_score=0.9,\n                    date_created=\"2026-01-01 10:30:00\",\n                )\n            ]\n\n            result = recall.search_facts(\"test query\", entity_id=42)\n\n            assert len(result) == 1\n            mock_search.assert_called_once()\n            args = mock_search.call_args[0]\n            assert args[1] == 42\n\n\ndef test_search_facts_success():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    config.storage.driver.entity.create.return_value = 1\n    config.entity_id = \"test-entity\"\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.return_value = [\n                FactSearchResult(\n                    id=1,\n                    content=\"User likes pizza\",\n                    similarity=0.9,\n                    rank_score=0.9,\n                    date_created=\"2026-01-01 10:30:00\",\n                ),\n                FactSearchResult(\n                    id=2,\n                    content=\"User lives in NYC\",\n                    similarity=0.85,\n                    rank_score=0.85,\n                    date_created=\"2026-01-02 11:15:00\",\n                ),\n            ]\n\n            result = recall.search_facts(\"What do I like?\", limit=5, entity_id=1)\n\n            assert len(result) == 2\n            assert result[0].content == \"User likes pizza\"\n            assert result[1].content == \"User lives in NYC\"\n\n            mock_embed.assert_called_once_with(\n                \"What do I like?\",\n                model=config.embeddings.model,\n            )\n            mock_search.assert_called_once_with(\n                config.storage.driver.entity_fact,\n                1,\n                [0.1, 0.2, 0.3],\n                5,\n                config.recall_embeddings_limit,\n                query_text=\"What do I like?\",\n            )\n\n\ndef test_search_facts_with_custom_limit():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.return_value = []\n\n            recall.search_facts(\"test query\", limit=10, entity_id=1)\n\n            mock_search.assert_called_once()\n            assert mock_search.call_args[0][3] == 10\n            assert mock_search.call_args[0][4] == config.recall_embeddings_limit\n\n\ndef test_search_facts_retry_on_operational_error():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.side_effect = [\n                OperationalError(\n                    \"statement\", \"params\", Exception(\"restart transaction\")\n                ),\n                [{\"content\": \"fact\", \"similarity\": 0.9}],\n            ]\n\n            with patch(\"memori.memory.recall.time.sleep\") as mock_sleep:\n                result = recall.search_facts(\"test query\", entity_id=1)\n\n                assert len(result) == 1\n                assert mock_search.call_count == 2\n                mock_sleep.assert_called_once()\n                assert mock_sleep.call_args[0][0] == RETRY_BACKOFF_BASE * (2**0)\n\n\ndef test_search_facts_retry_multiple_times():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.side_effect = [\n                OperationalError(\n                    \"statement\", \"params\", Exception(\"restart transaction\")\n                ),\n                OperationalError(\n                    \"statement\", \"params\", Exception(\"restart transaction\")\n                ),\n                [{\"content\": \"fact\", \"similarity\": 0.9}],\n            ]\n\n            with patch(\"memori.memory.recall.time.sleep\") as mock_sleep:\n                result = recall.search_facts(\"test query\", entity_id=1)\n\n                assert len(result) == 1\n                assert mock_search.call_count == 3\n                assert mock_sleep.call_count == 2\n                assert mock_sleep.call_args_list[0][0][0] == RETRY_BACKOFF_BASE * (2**0)\n                assert mock_sleep.call_args_list[1][0][0] == RETRY_BACKOFF_BASE * (2**1)\n\n\ndef test_search_facts_raises_after_max_retries():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.side_effect = OperationalError(\n                \"statement\", \"params\", Exception(\"restart transaction\")\n            )\n\n            with patch(\"memori.memory.recall.time.sleep\"):\n                with pytest.raises(OperationalError):\n                    recall.search_facts(\"test query\", entity_id=1)\n\n                assert mock_search.call_count == MAX_RETRIES\n\n\ndef test_search_facts_raises_on_non_restart_error():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.side_effect = OperationalError(\n                \"statement\", \"params\", Exception(\"some other error\")\n            )\n\n            with pytest.raises(OperationalError):\n                recall.search_facts(\"test query\", entity_id=1)\n\n            assert mock_search.call_count == 1\n\n\ndef test_search_facts_returns_empty_on_no_results():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.return_value = []\n\n            result = recall.search_facts(\"test query\", entity_id=1)\n\n            assert result == []\n\n\ndef test_search_facts_embeds_query_correctly():\n    config = Config()\n    config.storage = Mock()\n    config.storage.driver = Mock()\n    recall = Recall(config)\n\n    with patch(\"memori.memory.recall.embed_texts\") as mock_embed:\n        mock_embed.return_value = [[0.1, 0.2, 0.3, 0.4, 0.5]]\n\n        with patch(\"memori.memory.recall.search_facts_api\") as mock_search:\n            mock_search.return_value = []\n\n            recall.search_facts(\"My test query\", entity_id=1)\n\n            mock_embed.assert_called_once_with(\n                \"My test query\",\n                model=config.embeddings.model,\n            )\n            mock_search.assert_called_once()\n            assert mock_search.call_args[0][2] == [0.1, 0.2, 0.3, 0.4, 0.5]\n\n\ndef test_search_facts_cloud_includes_explicit_limit_in_payload(mocker):\n    config = Config()\n    config.cloud = True\n    config.entity_id = \"entity-id\"\n    config.process_id = \"process-id\"\n    config.session_id = \"session-id\"\n    recall = Recall(config)\n\n    post = mocker.patch(\n        \"memori.memory.recall.Api.post\",\n        autospec=True,\n        return_value={\"facts\": [\"fact-a\"], \"messages\": []},\n    )\n\n    result = recall.search_facts(\"test query\", limit=10)\n\n    assert result == [\"fact-a\"]\n    assert post.call_args[0][1] == \"cloud/recall\"\n    payload = post.call_args[0][2]\n    assert payload[\"limit\"] == 10\n\n\ndef test_search_facts_cloud_defaults_to_config_recall_facts_limit(mocker):\n    config = Config()\n    config.cloud = True\n    config.entity_id = \"entity-id\"\n    config.process_id = \"process-id\"\n    config.session_id = \"session-id\"\n    config.recall_facts_limit = 7\n    recall = Recall(config)\n\n    post = mocker.patch(\n        \"memori.memory.recall.Api.post\",\n        autospec=True,\n        return_value={\"facts\": [], \"messages\": []},\n    )\n\n    recall.search_facts(\"test query\")\n\n    assert post.call_args[0][1] == \"cloud/recall\"\n    payload = post.call_args[0][2]\n    assert payload[\"limit\"] == 7\n\n\ndef test_search_facts_cloud_uses_none_for_missing_process_id(mocker):\n    config = Config()\n    config.cloud = True\n    config.entity_id = \"entity-id\"\n    config.process_id = None\n    config.session_id = \"session-id\"\n    recall = Recall(config)\n\n    post = mocker.patch(\n        \"memori.memory.recall.Api.post\",\n        autospec=True,\n        return_value={\"facts\": [], \"messages\": []},\n    )\n\n    recall.search_facts(\"test query\")\n\n    assert post.call_args[0][1] == \"cloud/recall\"\n    payload = post.call_args[0][2]\n    assert payload[\"attribution\"][\"process\"] is None\n\n\ndef test_constants():\n    assert MAX_RETRIES == 3\n    assert RETRY_BACKOFF_BASE == 0.05\n"
  },
  {
    "path": "tests/memory/test_recall_eval_harness.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nfrom __future__ import annotations\n\nimport math\nimport struct\nfrom dataclasses import dataclass\nfrom typing import cast\n\nfrom memori._config import Config\nfrom memori.memory.recall import Recall\n\n\ndef _pack_embedding(vec: list[float]) -> bytes:\n    return struct.pack(f\"<{len(vec)}f\", *vec)\n\n\n@dataclass(frozen=True)\nclass _Case:\n    query: str\n    expected_top_ids: set[int]\n\n\nclass _FakeEntityFactDriver:\n    def __init__(self, *, facts: dict[int, str], embeddings: dict[int, list[float]]):\n        self._facts = dict(facts)\n        self._embeddings = dict(embeddings)\n\n    def get_embeddings(self, entity_id: int, limit: int = 1000):\n        _ = entity_id\n        rows = []\n        for fid, vec in self._embeddings.items():\n            rows.append({\"id\": fid, \"content_embedding\": _pack_embedding(vec)})\n        return rows[:limit]\n\n    def get_facts_by_ids(self, fact_ids: list[int]):\n        rows = []\n        for fid in fact_ids:\n            content = self._facts.get(fid)\n            if content is not None:\n                rows.append({\"id\": fid, \"content\": content})\n        return rows\n\n\nclass _FakeEntityDriver:\n    def create(self, entity_id: str) -> int:\n        _ = entity_id\n        return 1\n\n\nclass _FakeStorageDriver:\n    def __init__(self, entity_fact: _FakeEntityFactDriver):\n        self.entity = _FakeEntityDriver()\n        self.entity_fact = entity_fact\n\n\nclass _FakeStorage:\n    def __init__(self, driver: _FakeStorageDriver):\n        self.driver = driver\n\n\ndef _recall_at_k(*, cases: list[_Case], results_by_query: dict[str, list[int]], k: int):\n    hits = 0\n    for c in cases:\n        got = results_by_query.get(c.query, [])[:k]\n        if any(fid in c.expected_top_ids for fid in got):\n            hits += 1\n    return hits / (len(cases) or 1)\n\n\ndef _mrr_at_k(*, cases: list[_Case], results_by_query: dict[str, list[int]], k: int):\n    rr_sum = 0.0\n    for c in cases:\n        got = results_by_query.get(c.query, [])[:k]\n        rr = 0.0\n        for idx, fid in enumerate(got, start=1):\n            if fid in c.expected_top_ids:\n                rr = 1.0 / float(idx)\n                break\n        rr_sum += rr\n    return rr_sum / (len(cases) or 1)\n\n\ndef _ndcg_at_k(*, cases: list[_Case], results_by_query: dict[str, list[int]], k: int):\n    def dcg(ids: list[int], rel: set[int]) -> float:\n        total = 0.0\n        for i, fid in enumerate(ids[:k], start=1):\n            gain = 1.0 if fid in rel else 0.0\n            total += gain / math.log2(i + 1.0)\n        return total\n\n    ndcg_sum = 0.0\n    for c in cases:\n        got = results_by_query.get(c.query, [])[:k]\n        ideal = list(c.expected_top_ids)[:k]\n        denom = dcg(ideal, c.expected_top_ids)\n        ndcg_sum += 0.0 if denom == 0.0 else (dcg(got, c.expected_top_ids) / denom)\n    return ndcg_sum / (len(cases) or 1)\n\n\ndef test_recall_eval_harness_reports_expected_metrics(mocker):\n    facts = {\n        1: \"Favorite color is blue.\",\n        2: \"Lives in New York City.\",\n        3: \"Likes pizza.\",\n        4: \"Prefers decaf coffee.\",\n        5: \"Dog is named Miso.\",\n    }\n\n    # 3D toy embedding space: each fact sits near an axis.\n    embeddings = {\n        1: [1.0, 0.0, 0.0],\n        2: [0.0, 1.0, 0.0],\n        3: [0.0, 0.0, 1.0],\n        4: [0.8, 0.2, 0.0],\n        5: [0.0, 0.7, 0.3],\n    }\n\n    query_to_embedding = {\n        \"What's my favorite color?\": [0.95, 0.05, 0.0],\n        \"Where do I live?\": [0.05, 0.95, 0.0],\n        \"What food do I like?\": [0.05, 0.05, 0.9],\n    }\n\n    def _embed_side_effect(text, *, model):\n        _ = model\n        if isinstance(text, list):\n            return [query_to_embedding[t] for t in text]\n        return [query_to_embedding[text]]\n\n    mocker.patch(\"memori.memory.recall.embed_texts\", side_effect=_embed_side_effect)\n\n    cfg = Config()\n    cfg.storage = _FakeStorage(\n        _FakeStorageDriver(_FakeEntityFactDriver(facts=facts, embeddings=embeddings))\n    )\n    cfg.entity_id = \"entity-1\"\n\n    recall = Recall(cfg)\n\n    cases = [\n        _Case(query=\"What's my favorite color?\", expected_top_ids={1}),\n        _Case(query=\"Where do I live?\", expected_top_ids={2}),\n        _Case(query=\"What food do I like?\", expected_top_ids={3}),\n    ]\n\n    results_by_query: dict[str, list[int]] = {}\n    for c in cases:\n        rows = recall.search_facts(query=c.query, limit=3, entity_id=1)\n        # Test uses int IDs from _FakeEntityFactDriver, cast for type checker\n        results_by_query[c.query] = [cast(int, r.id) for r in rows]\n\n    recall_at_1 = _recall_at_k(cases=cases, results_by_query=results_by_query, k=1)\n    mrr_at_3 = _mrr_at_k(cases=cases, results_by_query=results_by_query, k=3)\n    ndcg_at_3 = _ndcg_at_k(cases=cases, results_by_query=results_by_query, k=3)\n\n    assert recall_at_1 == 1.0\n    assert mrr_at_3 == 1.0\n    assert ndcg_at_3 == 1.0\n"
  },
  {
    "path": "tests/storage/adapters/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture\ndef mongodb_conn(mocker):\n    \"\"\"Create a mock MongoDB database connection.\"\"\"\n    # Mock MongoDB database connection\n    mock_db = mocker.MagicMock()\n    mock_db.database = mocker.MagicMock()\n    mock_db.list_collection_names = mocker.MagicMock(return_value=[\"test_collection\"])\n\n    # Mock collection\n    mock_collection = mocker.MagicMock()\n    mock_collection.find_one = mocker.MagicMock(return_value={\"test\": \"value\"})\n    mock_collection.insert_one = mocker.MagicMock(\n        return_value=mocker.MagicMock(inserted_id=\"507f1f77bcf86cd799439011\")\n    )\n    mock_collection.find = mocker.MagicMock(return_value=[{\"test\": \"value\"}])\n    mock_collection.delete_many = mocker.MagicMock(\n        return_value=mocker.MagicMock(deleted_count=1)\n    )\n    mock_collection.update_one = mocker.MagicMock(\n        return_value=mocker.MagicMock(modified_count=1)\n    )\n\n    # Mock database to return collection when accessed with []\n    mock_db.__getitem__ = mocker.MagicMock(return_value=mock_collection)\n\n    return mock_db\n"
  },
  {
    "path": "tests/storage/adapters/dbapi/test_dbapi_no_conflicts.py",
    "content": "from memori.storage._registry import Registry\nfrom memori.storage.adapters.dbapi._adapter import (\n    Adapter as DBAPIAdapter,\n)\nfrom memori.storage.adapters.dbapi._adapter import (\n    is_dbapi_connection,\n)\nfrom memori.storage.adapters.django._adapter import Adapter as DjangoAdapter\nfrom memori.storage.adapters.sqlalchemy._adapter import Adapter as SQLAlchemyAdapter\n\n\ndef test_sqlalchemy_session_not_detected_as_dbapi(session):\n    assert is_dbapi_connection(session) is False\n\n\ndef test_registry_routes_sqlalchemy_to_sqlalchemy_adapter(session):\n    registry = Registry()\n    adapter = registry.adapter(lambda: session)\n    assert isinstance(adapter, SQLAlchemyAdapter)\n    assert not isinstance(adapter, DBAPIAdapter)\n\n\ndef test_registry_routes_postgres_session_to_sqlalchemy_adapter(postgres_session):\n    registry = Registry()\n    adapter = registry.adapter(lambda: postgres_session)\n    assert isinstance(adapter, SQLAlchemyAdapter)\n    assert not isinstance(adapter, DBAPIAdapter)\n\n\ndef test_django_connection_not_detected_as_dbapi(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\", \"vendor\"])\n    mock_conn.__class__.__module__ = \"django.db.backends.postgresql.base\"\n    mock_conn.vendor = \"postgresql\"\n    assert is_dbapi_connection(mock_conn) is False\n\n\ndef test_registry_routes_django_to_django_adapter(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\", \"vendor\"])\n    mock_conn.__class__.__module__ = \"django.db.backends.postgresql.base\"\n    mock_conn.vendor = \"postgresql\"\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.__enter__ = mocker.MagicMock(return_value=mock_cursor)\n    mock_cursor.__exit__ = mocker.MagicMock(return_value=False)\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n\n    registry = Registry()\n    adapter = registry.adapter(lambda: mock_conn)\n    assert isinstance(adapter, DjangoAdapter)\n    assert not isinstance(adapter, DBAPIAdapter)\n"
  },
  {
    "path": "tests/storage/adapters/dbapi/test_storage_adapters_dbapi_adapter.py",
    "content": "import pytest\n\nfrom memori.storage._registry import Registry\nfrom memori.storage.adapters.dbapi._adapter import (\n    Adapter as DBAPIAdapter,\n)\nfrom memori.storage.adapters.dbapi._adapter import (\n    is_dbapi_connection,\n)\n\n\n@pytest.fixture\ndef mock_psycopg2_conn(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg2\"\n    type(mock_conn).__module__ = \"psycopg2\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    return mock_conn\n\n\n@pytest.fixture\ndef mock_pymysql_conn(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"pymysql\"\n    type(mock_conn).__module__ = \"pymysql.connections\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    return mock_conn\n\n\n@pytest.fixture\ndef mock_pyobvector_conn(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"pyobvector\"\n    type(mock_conn).__module__ = \"pyobvector.dbapi\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    return mock_conn\n\n\n@pytest.fixture\ndef mock_sqlite3_conn(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"sqlite3\"\n    type(mock_conn).__module__ = \"sqlite3\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    return mock_conn\n\n\ndef test_commit_psycopg2(mock_psycopg2_conn):\n    adapter = DBAPIAdapter(lambda: mock_psycopg2_conn)\n    result = adapter.commit()\n\n    mock_psycopg2_conn.commit.assert_called_once()\n    assert result is adapter\n\n\ndef test_execute_psycopg2(mock_psycopg2_conn):\n    adapter = DBAPIAdapter(lambda: mock_psycopg2_conn)\n    cursor = adapter.execute(\"SELECT 1\")\n\n    mock_psycopg2_conn.cursor.assert_called_once()\n    assert cursor is not None\n\n\ndef test_execute_with_binds_psycopg2(mock_psycopg2_conn):\n    adapter = DBAPIAdapter(lambda: mock_psycopg2_conn)\n    adapter.execute(\"SELECT * FROM users WHERE id = %s\", (1,))\n\n    mock_psycopg2_conn.cursor.assert_called_once()\n    mock_cursor = mock_psycopg2_conn.cursor.return_value\n    mock_cursor.execute.assert_called_once_with(\n        \"SELECT * FROM users WHERE id = %s\", (1,)\n    )\n\n\ndef test_flush_psycopg2(mock_psycopg2_conn):\n    adapter = DBAPIAdapter(lambda: mock_psycopg2_conn)\n    result = adapter.flush()\n\n    assert result is adapter\n\n\ndef test_get_dialect_psycopg2(mock_psycopg2_conn):\n    adapter = DBAPIAdapter(lambda: mock_psycopg2_conn)\n    assert adapter.get_dialect() == \"postgresql\"\n\n\ndef test_rollback_psycopg2(mock_psycopg2_conn):\n    adapter = DBAPIAdapter(lambda: mock_psycopg2_conn)\n    result = adapter.rollback()\n\n    mock_psycopg2_conn.rollback.assert_called_once()\n    assert result is adapter\n\n\ndef test_commit_pymysql(mock_pymysql_conn):\n    adapter = DBAPIAdapter(lambda: mock_pymysql_conn)\n    result = adapter.commit()\n\n    mock_pymysql_conn.commit.assert_called_once()\n    assert result is adapter\n\n\ndef test_execute_pymysql(mock_pymysql_conn):\n    adapter = DBAPIAdapter(lambda: mock_pymysql_conn)\n    cursor = adapter.execute(\"SELECT 1\")\n\n    mock_pymysql_conn.cursor.assert_called_once()\n    assert cursor is not None\n\n\ndef test_get_dialect_pymysql(mock_pymysql_conn):\n    adapter = DBAPIAdapter(lambda: mock_pymysql_conn)\n    assert adapter.get_dialect() == \"mysql\"\n\n\ndef test_get_dialect_pyobvector(mock_pyobvector_conn):\n    adapter = DBAPIAdapter(lambda: mock_pyobvector_conn)\n    assert adapter.get_dialect() == \"oceanbase\"\n\n\ndef test_rollback_pymysql(mock_pymysql_conn):\n    adapter = DBAPIAdapter(lambda: mock_pymysql_conn)\n    result = adapter.rollback()\n\n    mock_pymysql_conn.rollback.assert_called_once()\n    assert result is adapter\n\n\ndef test_commit_sqlite3(mock_sqlite3_conn):\n    adapter = DBAPIAdapter(lambda: mock_sqlite3_conn)\n    result = adapter.commit()\n\n    mock_sqlite3_conn.commit.assert_called_once()\n    assert result is adapter\n\n\ndef test_execute_sqlite3(mock_sqlite3_conn):\n    adapter = DBAPIAdapter(lambda: mock_sqlite3_conn)\n    cursor = adapter.execute(\"SELECT 1\")\n\n    mock_sqlite3_conn.cursor.assert_called_once()\n    assert cursor is not None\n\n\ndef test_get_dialect_sqlite3(mock_sqlite3_conn):\n    adapter = DBAPIAdapter(lambda: mock_sqlite3_conn)\n    assert adapter.get_dialect() == \"sqlite\"\n\n\ndef test_rollback_sqlite3(mock_sqlite3_conn):\n    adapter = DBAPIAdapter(lambda: mock_sqlite3_conn)\n    result = adapter.rollback()\n\n    mock_sqlite3_conn.rollback.assert_called_once()\n    assert result is adapter\n\n\ndef test_execute_closes_cursor_on_exception(mock_psycopg2_conn):\n    adapter = DBAPIAdapter(lambda: mock_psycopg2_conn)\n    mock_cursor = mock_psycopg2_conn.cursor.return_value\n    mock_cursor.execute.side_effect = Exception(\"Query error\")\n\n    with pytest.raises(Exception, match=\"Query error\"):\n        adapter.execute(\"SELECT invalid\")\n\n    mock_cursor.close.assert_called_once()\n\n\ndef test_get_dialect_unknown_raises_error(mocker):\n    mock_conn = mocker.MagicMock()\n    mock_conn.__module__ = \"unknown_driver\"\n    type(mock_conn).__module__ = \"unknown_driver\"\n    mock_conn.cursor = mocker.MagicMock()\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    adapter = DBAPIAdapter(lambda: mock_conn)\n\n    with pytest.raises(ValueError, match=\"Unable to determine dialect\"):\n        adapter.get_dialect()\n\n\ndef test_is_dbapi_connection_psycopg2(mock_psycopg2_conn):\n    assert is_dbapi_connection(mock_psycopg2_conn) is True\n\n\ndef test_is_dbapi_connection_pymysql(mock_pymysql_conn):\n    assert is_dbapi_connection(mock_pymysql_conn) is True\n\n\ndef test_is_dbapi_connection_pyobvector(mock_pyobvector_conn):\n    assert is_dbapi_connection(mock_pyobvector_conn) is True\n\n\ndef test_is_dbapi_connection_sqlite3(mock_sqlite3_conn):\n    assert is_dbapi_connection(mock_sqlite3_conn) is True\n\n\ndef test_is_dbapi_connection_rejects_non_dbapi(mocker):\n    mock_conn = mocker.MagicMock()\n    del mock_conn.cursor\n    assert is_dbapi_connection(mock_conn) is False\n\n\ndef test_is_dbapi_connection_rejects_non_callable_methods(mocker):\n    mock_conn = mocker.MagicMock()\n    mock_conn.cursor = \"not_callable\"\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n    assert is_dbapi_connection(mock_conn) is False\n\n\ndef test_registry_routes_psycopg2_to_dbapi_adapter(mock_psycopg2_conn):\n    registry = Registry()\n    adapter = registry.adapter(lambda: mock_psycopg2_conn)\n    assert isinstance(adapter, DBAPIAdapter)\n\n\ndef test_registry_routes_pymysql_to_dbapi_adapter(mock_pymysql_conn):\n    registry = Registry()\n    adapter = registry.adapter(lambda: mock_pymysql_conn)\n    assert isinstance(adapter, DBAPIAdapter)\n\n\ndef test_registry_routes_sqlite3_to_dbapi_adapter(mock_sqlite3_conn):\n    registry = Registry()\n    adapter = registry.adapter(lambda: mock_sqlite3_conn)\n    assert isinstance(adapter, DBAPIAdapter)\n"
  },
  {
    "path": "tests/storage/adapters/django/test_storage_adapters_django_adapter.py",
    "content": "import pytest\n\nfrom memori.storage._registry import Registry\nfrom memori.storage.adapters.django._adapter import (\n    Adapter as DjangoAdapter,\n)\nfrom memori.storage.adapters.django._adapter import (\n    is_django_connection,\n)\n\n\n@pytest.fixture\ndef mock_django_postgresql_conn(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\", \"vendor\"])\n    mock_conn.__class__.__module__ = \"django.db.backends.postgresql.base\"\n    mock_conn.vendor = \"postgresql\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.fetchone = mocker.MagicMock()\n    mock_cursor.fetchall = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    return mock_conn\n\n\n@pytest.fixture\ndef mock_django_mysql_conn(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\", \"vendor\"])\n    mock_conn.__class__.__module__ = \"django.db.backends.mysql.base\"\n    mock_conn.vendor = \"mysql\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.fetchone = mocker.MagicMock()\n    mock_cursor.fetchall = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    return mock_conn\n\n\n@pytest.fixture\ndef mock_django_sqlite_conn(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\", \"vendor\"])\n    mock_conn.__class__.__module__ = \"django.db.backends.sqlite3.base\"\n    mock_conn.vendor = \"sqlite\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.fetchone = mocker.MagicMock()\n    mock_cursor.fetchall = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    return mock_conn\n\n\ndef test_commit_postgresql(mock_django_postgresql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_postgresql_conn)\n    result = adapter.commit()\n\n    mock_django_postgresql_conn.commit.assert_called_once()\n    assert result is adapter\n\n\ndef test_execute_postgresql(mock_django_postgresql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_postgresql_conn)\n    cursor = adapter.execute(\"SELECT 1\")\n\n    mock_django_postgresql_conn.cursor.assert_called_once()\n    assert cursor is not None\n\n\ndef test_execute_with_binds_postgresql(mock_django_postgresql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_postgresql_conn)\n    adapter.execute(\"SELECT * FROM users WHERE id = %s\", (1,))\n\n    mock_django_postgresql_conn.cursor.assert_called_once()\n    mock_cursor = mock_django_postgresql_conn.cursor.return_value\n    mock_cursor.execute.assert_called_once_with(\n        \"SELECT * FROM users WHERE id = %s\", (1,)\n    )\n\n\ndef test_flush_postgresql(mock_django_postgresql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_postgresql_conn)\n    result = adapter.flush()\n\n    assert result is adapter\n\n\ndef test_get_dialect_postgresql(mock_django_postgresql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_postgresql_conn)\n    assert adapter.get_dialect() == \"postgresql\"\n\n\ndef test_rollback_postgresql(mock_django_postgresql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_postgresql_conn)\n    result = adapter.rollback()\n\n    mock_django_postgresql_conn.rollback.assert_called_once()\n    assert result is adapter\n\n\ndef test_commit_mysql(mock_django_mysql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_mysql_conn)\n    result = adapter.commit()\n\n    mock_django_mysql_conn.commit.assert_called_once()\n    assert result is adapter\n\n\ndef test_execute_mysql(mock_django_mysql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_mysql_conn)\n    cursor = adapter.execute(\"SELECT 1\")\n\n    mock_django_mysql_conn.cursor.assert_called_once()\n    assert cursor is not None\n\n\ndef test_get_dialect_mysql(mock_django_mysql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_mysql_conn)\n    assert adapter.get_dialect() == \"mysql\"\n\n\ndef test_rollback_mysql(mock_django_mysql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_mysql_conn)\n    result = adapter.rollback()\n\n    mock_django_mysql_conn.rollback.assert_called_once()\n    assert result is adapter\n\n\ndef test_commit_sqlite(mock_django_sqlite_conn):\n    adapter = DjangoAdapter(lambda: mock_django_sqlite_conn)\n    result = adapter.commit()\n\n    mock_django_sqlite_conn.commit.assert_called_once()\n    assert result is adapter\n\n\ndef test_execute_sqlite(mock_django_sqlite_conn):\n    adapter = DjangoAdapter(lambda: mock_django_sqlite_conn)\n    cursor = adapter.execute(\"SELECT 1\")\n\n    mock_django_sqlite_conn.cursor.assert_called_once()\n    assert cursor is not None\n\n\ndef test_get_dialect_sqlite(mock_django_sqlite_conn):\n    adapter = DjangoAdapter(lambda: mock_django_sqlite_conn)\n    assert adapter.get_dialect() == \"sqlite\"\n\n\ndef test_rollback_sqlite(mock_django_sqlite_conn):\n    adapter = DjangoAdapter(lambda: mock_django_sqlite_conn)\n    result = adapter.rollback()\n\n    mock_django_sqlite_conn.rollback.assert_called_once()\n    assert result is adapter\n\n\ndef test_execute_closes_cursor_on_exception(mock_django_postgresql_conn):\n    adapter = DjangoAdapter(lambda: mock_django_postgresql_conn)\n    mock_cursor = mock_django_postgresql_conn.cursor.return_value\n    mock_cursor.execute.side_effect = Exception(\"Query error\")\n\n    with pytest.raises(Exception, match=\"Query error\"):\n        adapter.execute(\"SELECT invalid\")\n\n    mock_cursor.close.assert_called_once()\n\n\ndef test_get_dialect_unknown_raises_error(mocker):\n    mock_conn = mocker.MagicMock()\n    mock_conn.__class__.__module__ = \"django.db.backends.unknown.base\"\n    mock_conn.vendor = \"unknown_db\"\n    mock_conn.cursor = mocker.MagicMock()\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    adapter = DjangoAdapter(lambda: mock_conn)\n\n    with pytest.raises(ValueError, match=\"Unable to determine dialect\"):\n        adapter.get_dialect()\n\n\ndef test_is_django_connection_postgresql(mock_django_postgresql_conn):\n    assert is_django_connection(mock_django_postgresql_conn) is True\n\n\ndef test_is_django_connection_mysql(mock_django_mysql_conn):\n    assert is_django_connection(mock_django_mysql_conn) is True\n\n\ndef test_is_django_connection_sqlite(mock_django_sqlite_conn):\n    assert is_django_connection(mock_django_sqlite_conn) is True\n\n\ndef test_is_django_connection_rejects_non_django(mocker):\n    mock_conn = mocker.MagicMock()\n    mock_conn.__class__.__module__ = \"psycopg2\"\n    assert is_django_connection(mock_conn) is False\n\n\ndef test_is_django_connection_rejects_non_callable_cursor(mocker):\n    mock_conn = mocker.MagicMock()\n    mock_conn.__class__.__module__ = \"django.db.backends.postgresql.base\"\n    mock_conn.cursor = \"not_callable\"\n    assert is_django_connection(mock_conn) is False\n\n\ndef test_registry_routes_postgresql_to_django_adapter(mock_django_postgresql_conn):\n    registry = Registry()\n    adapter = registry.adapter(lambda: mock_django_postgresql_conn)\n    assert isinstance(adapter, DjangoAdapter)\n\n\ndef test_registry_routes_mysql_to_django_adapter(mock_django_mysql_conn):\n    registry = Registry()\n    adapter = registry.adapter(lambda: mock_django_mysql_conn)\n    assert isinstance(adapter, DjangoAdapter)\n\n\ndef test_registry_routes_sqlite_to_django_adapter(mock_django_sqlite_conn):\n    registry = Registry()\n    adapter = registry.adapter(lambda: mock_django_sqlite_conn)\n    assert isinstance(adapter, DjangoAdapter)\n"
  },
  {
    "path": "tests/storage/adapters/sqlalchemy/test_storage_adaptors_sqlalchemy_adapter.py",
    "content": "from memori.storage.adapters.mongodb._adapter import Adapter as MongoAdapter\nfrom memori.storage.adapters.sqlalchemy._adapter import Adapter as SqlAlchemyAdapter\n\n\ndef test_commit(session):\n    adapter = SqlAlchemyAdapter(lambda: session)\n    adapter.commit()\n\n\ndef test_execute(session):\n    adapter = SqlAlchemyAdapter(lambda: session)\n\n    assert adapter.execute(\"select 1 from dual\").mappings().fetchone() == {\"1\": 1}\n\n\ndef test_flush(session):\n    adapter = SqlAlchemyAdapter(lambda: session)\n    adapter.flush()\n\n\ndef test_get_dialect(session):\n    adapter = SqlAlchemyAdapter(lambda: session)\n    assert adapter.get_dialect() == \"mysql\"\n\n\ndef test_get_dialect_oceanbase(mocker):\n    class FakeDialect:\n        __module__ = \"pyobvector.schema.dialect\"\n        name = \"mysql\"\n\n    mock_bind = mocker.Mock()\n    mock_bind.dialect = FakeDialect()\n    session = mocker.Mock()\n    session.get_bind.return_value = mock_bind\n\n    adapter = SqlAlchemyAdapter(lambda: session)\n\n    assert adapter.get_dialect() == \"oceanbase\"\n\n\ndef test_rollback(session):\n    adapter = SqlAlchemyAdapter(lambda: session)\n    adapter.rollback()\n\n\n# PostgreSQL tests\ndef test_commit_postgres(postgres_session):\n    adapter = SqlAlchemyAdapter(lambda: postgres_session)\n    adapter.commit()\n\n\ndef test_execute_postgres(postgres_session):\n    adapter = SqlAlchemyAdapter(lambda: postgres_session)\n\n    assert adapter.execute(\"select 1 as one\").mappings().fetchone() == {\"one\": 1}\n\n\ndef test_flush_postgres(postgres_session):\n    adapter = SqlAlchemyAdapter(lambda: postgres_session)\n    adapter.flush()\n\n\ndef test_get_dialect_postgres(postgres_session):\n    adapter = SqlAlchemyAdapter(lambda: postgres_session)\n    assert adapter.get_dialect() == \"postgresql\"\n\n\ndef test_rollback_postgres(postgres_session):\n    adapter = SqlAlchemyAdapter(lambda: postgres_session)\n    adapter.rollback()\n\n\n# MongoDB tests\ndef test_mongodb_adapter_execute(mongodb_conn):\n    \"\"\"Test MongoDB adapter execute method.\"\"\"\n    adapter = MongoAdapter(lambda: mongodb_conn)\n\n    # Test find_one operation\n    adapter.execute(\"test_collection\", \"find_one\", {\"test\": \"value\"})\n    # The mock should return the mocked result\n\n    # Test insert_one operation\n    adapter.execute(\"test_collection\", \"insert_one\", {\"test\": \"value\"})\n    # The mock should return the mocked result\n\n\ndef test_mongodb_adapter_get_dialect(mongodb_conn):\n    \"\"\"Test MongoDB adapter get_dialect method.\"\"\"\n    adapter = MongoAdapter(lambda: mongodb_conn)\n    assert adapter.get_dialect() == \"mongodb\"\n\n\ndef test_mongodb_adapter_execute_with_args(mongodb_conn):\n    \"\"\"Test MongoDB adapter execute method with various arguments.\"\"\"\n    adapter = MongoAdapter(lambda: mongodb_conn)\n\n    # Test find operation with projection\n    adapter.execute(\n        \"test_collection\", \"find\", {\"test\": \"value\"}, {\"field\": 1, \"_id\": 0}\n    )\n\n    # Test delete_many operation\n    adapter.execute(\"test_collection\", \"delete_many\", {\"test\": \"value\"})\n\n\ndef test_mongodb_adapter_execute_with_kwargs(mongodb_conn):\n    \"\"\"Test MongoDB adapter execute method with keyword arguments.\"\"\"\n    adapter = MongoAdapter(lambda: mongodb_conn)\n\n    # Test update_one with upsert\n    adapter.execute(\n        \"test_collection\",\n        \"update_one\",\n        {\"test\": \"value\"},\n        {\"$set\": {\"updated\": True}},\n        upsert=True,\n    )\n"
  },
  {
    "path": "tests/storage/cockroachdb/test_storage_cockroachdb_display.py",
    "content": "from memori.storage.cockroachdb._display import Display\n\n\ndef test_cluster_already_started():\n    assert Display().cluster_already_started() == (\n        \"\"\"You already have an active CockroachDB cluster running. To start\n  a new one, execute this command first:\n\n    python -m memori cockroachdb cluster delete\n\"\"\"\n    )\n\n\ndef test_cluster_was_not_started():\n    assert Display().cluster_was_not_started() == (\n        \"\"\"You do not have an active CockroachDB cluster running. To start\n  a new one, execute this command first:\n\n    python -m memori cockroachdb cluster start\n\"\"\"\n    )\n"
  },
  {
    "path": "tests/storage/cockroachdb/test_storage_cockroachdb_files.py",
    "content": "import os\n\nimport pytest\n\nfrom memori.storage.cockroachdb._files import Files\n\n\ndef test_storage_dir_exceptions():\n    try:\n        del os.environ[\"HOME\"]\n    except KeyError:\n        pass\n\n    try:\n        del os.environ[\"MEMORI_HOME\"]\n    except KeyError:\n        pass\n\n    with pytest.raises(RuntimeError) as e:\n        Files().storage_dir()\n\n    assert str(e.value) == \"neither MEMORI_HOME nor HOME environment variable is set\"\n\n\ndef test_storage_dir_home():\n    os.environ[\"HOME\"] = \"/abc\"\n    assert Files().storage_dir() == \"/abc/.memori\"\n\n\ndef test_storage_dir_memori_home():\n    os.environ[\"MEMORI_HOME\"] = \"/def\"\n    assert Files().storage_dir() == \"/def/.memori\"\n\n\ndef test_cluster_dir():\n    os.environ[\"MEMORI_HOME\"] = \"/def\"\n    assert Files().cluster_dir() == \"/def/.memori/cluster\"\n\n\ndef test_cluster_id():\n    os.environ[\"MEMORI_HOME\"] = \"/def\"\n    assert Files().cluster_id() == \"/def/.memori/cluster/id\"\n\n\ndef test_read_id_no_such_file():\n    os.environ[\"MEMORI_HOME\"] = \"/tmp\"\n    assert Files().read_id() is None\n\n\ndef test_write_id():\n    os.environ[\"MEMORI_HOME\"] = \"/tmp\"\n\n    cluster_id = Files().cluster_id()\n\n    try:\n        Files().write_id(\"abcdef\")\n\n        assert os.path.isfile(cluster_id)\n    finally:\n        try:\n            os.unlink(cluster_id)\n        except FileNotFoundError:\n            pass\n\n\ndef test_write_then_read_id():\n    os.environ[\"MEMORI_HOME\"] = \"/tmp\"\n\n    cluster_id = Files().cluster_id()\n\n    try:\n        Files().write_id(\"abcdef\")\n\n        assert os.path.isfile(cluster_id)\n        assert Files().read_id() == \"abcdef\"\n    finally:\n        try:\n            os.unlink(cluster_id)\n        except FileNotFoundError:\n            pass\n\n\ndef test_remove_id():\n    os.environ[\"MEMORI_HOME\"] = \"/tmp\"\n\n    cluster_id = Files().cluster_id()\n\n    try:\n        Files().write_id(\"abcdef\")\n        assert os.path.isfile(cluster_id)\n\n        Files().remove_id()\n        assert not os.path.isfile(cluster_id)\n    finally:\n        try:\n            os.unlink(cluster_id)\n        except FileNotFoundError:\n            pass\n"
  },
  {
    "path": "tests/storage/drivers/conftest.py",
    "content": "from unittest.mock import MagicMock, Mock\n\nimport pytest\n\n\n@pytest.fixture\ndef mock_conn():\n    \"\"\"Create a mock storage adapter connection.\"\"\"\n    conn = MagicMock()\n    conn.execute = MagicMock()\n    conn.flush = MagicMock()\n    conn.commit = MagicMock()\n    return conn\n\n\n@pytest.fixture\ndef mock_single_result():\n    \"\"\"Create a mock result for single row queries (fetchone).\"\"\"\n\n    def _make_result(data):\n        mock_result = Mock()\n        mock_result.mappings.return_value.fetchone.return_value = data\n        return mock_result\n\n    return _make_result\n\n\n@pytest.fixture\ndef mock_multiple_results():\n    \"\"\"Create a mock result for multiple row queries (fetchall).\"\"\"\n\n    def _make_result(data):\n        mock_result = Mock()\n        mock_result.mappings.return_value.fetchall.return_value = data\n        return mock_result\n\n    return _make_result\n\n\n@pytest.fixture\ndef mock_empty_result():\n    \"\"\"Create a mock result for empty queries.\"\"\"\n    mock_result = Mock()\n    mock_result.mappings.return_value.fetchall.return_value = []\n    return mock_result\n"
  },
  {
    "path": "tests/storage/drivers/test_mongodb_driver.py",
    "content": "from datetime import datetime\nfrom unittest.mock import Mock\nfrom uuid import UUID\n\nfrom memori.storage.drivers.mongodb._driver import (\n    Conversation,\n    ConversationMessage,\n    ConversationMessages,\n    Driver,\n    Entity,\n    EntityFact,\n    Process,\n    Schema,\n    SchemaVersion,\n    Session,\n)\n\n\ndef test_driver_initialization(mock_conn):\n    \"\"\"Test that Driver initializes all components correctly.\"\"\"\n    driver = Driver(mock_conn)\n\n    assert isinstance(driver.conversation, Conversation)\n    assert isinstance(driver.entity, Entity)\n    assert isinstance(driver.entity_fact, EntityFact)\n    assert isinstance(driver.process, Process)\n    assert isinstance(driver.schema, Schema)\n    assert isinstance(driver.session, Session)\n\n\ndef test_entity_create(mock_conn):\n    \"\"\"Test creating a entity record.\"\"\"\n    # Mock the find_one to return None (no existing record)\n    mock_conn.execute.side_effect = [\n        None,  # find_one returns None (no existing record)\n        Mock(inserted_id=123),  # insert_one returns mock result\n    ]\n\n    entity = Entity(mock_conn)\n    result = entity.create(\"external-entity-id\")\n\n    assert result == 123\n    assert mock_conn.execute.call_count == 2  # find_one, insert_one\n\n    # Verify find_one query for existing record\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_entity\"\n    assert find_call[0][1] == \"find_one\"\n    assert find_call[0][2] == {\"external_id\": \"external-entity-id\"}\n\n    # Verify insert_one query\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert insert_call[0][0] == \"memori_entity\"\n    assert insert_call[0][1] == \"insert_one\"\n    doc = insert_call[0][2]\n    assert doc[\"external_id\"] == \"external-entity-id\"\n    assert \"uuid\" in doc\n    assert \"date_created\" in doc\n\n\ndef test_entity_create_existing_record(mock_conn):\n    \"\"\"Test creating a entity record when it already exists.\"\"\"\n    # Mock the find_one to return existing record\n    existing_record = Mock()\n    existing_record.get.return_value = 456\n    mock_conn.execute.return_value = existing_record\n\n    entity = Entity(mock_conn)\n    result = entity.create(\"external-entity-id\")\n\n    assert result == 456\n    assert mock_conn.execute.call_count == 1  # Only find_one\n\n\ndef test_entity_generates_uuid(mock_conn):\n    \"\"\"Test that create generates a valid UUID.\"\"\"\n    mock_conn.execute.side_effect = [\n        None,  # find_one returns None\n        Mock(inserted_id=123),  # insert_one returns mock result\n    ]\n\n    entity = Entity(mock_conn)\n    entity.create(\"external-entity-id\")\n\n    # Check that a UUID was generated in the insert_one\n    insert_call = mock_conn.execute.call_args_list[1]\n    doc = insert_call[0][2]\n    uuid_str = doc[\"uuid\"]\n\n    # Verify it's a valid UUID string\n    UUID(uuid_str)  # Will raise ValueError if invalid\n\n\ndef test_process_create(mock_conn):\n    \"\"\"Test creating a process record.\"\"\"\n    mock_conn.execute.side_effect = [\n        None,  # find_one returns None\n        Mock(inserted_id=456),  # insert_one returns mock result\n    ]\n\n    process = Process(mock_conn)\n    result = process.create(\"external-process-id\")\n\n    assert result == 456\n    assert mock_conn.execute.call_count == 2\n\n    # Verify find_one query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_process\"\n    assert find_call[0][1] == \"find_one\"\n    assert find_call[0][2] == {\"external_id\": \"external-process-id\"}\n\n    # Verify insert_one query\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert insert_call[0][0] == \"memori_process\"\n    assert insert_call[0][1] == \"insert_one\"\n    doc = insert_call[0][2]\n    assert doc[\"external_id\"] == \"external-process-id\"\n\n\ndef test_session_create(mock_conn):\n    \"\"\"Test creating a session record.\"\"\"\n    mock_conn.execute.side_effect = [\n        None,  # find_one returns None\n        Mock(inserted_id=789),  # insert_one returns mock result\n    ]\n\n    session = Session(mock_conn)\n    session_uuid = \"test-session-uuid\"\n    result = session.create(session_uuid, entity_id=123, process_id=456)\n\n    assert result == 789\n    assert mock_conn.execute.call_count == 2\n\n    # Verify find_one query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_session\"\n    assert find_call[0][1] == \"find_one\"\n    assert find_call[0][2] == {\"uuid\": \"test-session-uuid\"}\n\n    # Verify insert_one query\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert insert_call[0][0] == \"memori_session\"\n    assert insert_call[0][1] == \"insert_one\"\n    doc = insert_call[0][2]\n    assert doc[\"uuid\"] == \"test-session-uuid\"\n    assert doc[\"entity_id\"] == 123\n    assert doc[\"process_id\"] == 456\n\n\ndef test_conversation_initialization(mock_conn):\n    \"\"\"Test that Conversation initializes its sub-components.\"\"\"\n    conversation = Conversation(mock_conn)\n\n    assert isinstance(conversation.message, ConversationMessage)\n    assert isinstance(conversation.messages, ConversationMessages)\n    assert conversation.conn == mock_conn\n\n\ndef test_conversation_create(mock_conn):\n    \"\"\"Test creating a conversation record when none exists.\"\"\"\n    mock_conn.execute.side_effect = [\n        None,  # find_one returns None (no existing conversation)\n        Mock(inserted_id=101),  # insert_one returns mock result\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101\n    assert mock_conn.execute.call_count == 2\n\n    # Verify find_one query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_conversation\"\n    assert find_call[0][1] == \"find_one\"\n    assert find_call[0][2] == {\"session_id\": 789}\n\n    # Verify insert_one query\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert insert_call[0][0] == \"memori_conversation\"\n    assert insert_call[0][1] == \"insert_one\"\n    doc = insert_call[0][2]\n    assert doc[\"session_id\"] == 789\n    assert doc[\"summary\"] is None\n    assert \"uuid\" in doc\n\n\ndef test_conversation_create_returns_existing_within_timeout(mock_conn):\n    \"\"\"Test returning existing conversation when within timeout period.\"\"\"\n    from datetime import datetime, timedelta, timezone\n\n    last_activity = datetime.now(timezone.utc) - timedelta(minutes=15)\n\n    # Mock: existing conversation and last message\n    existing_conversation = {\n        \"_id\": 999,\n        \"session_id\": 789,\n        \"date_created\": datetime.now(timezone.utc) - timedelta(minutes=20),\n    }\n    last_message = {\"date_created\": last_activity}\n\n    mock_conn.execute.side_effect = [\n        existing_conversation,  # find_one for conversation\n        last_message,  # find_one for last message\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 999  # Returns existing conversation id\n    assert mock_conn.execute.call_count == 2  # Check conversation, check last message\n\n\ndef test_conversation_create_new_when_expired(mock_conn):\n    \"\"\"Test creating new conversation when existing one is expired.\"\"\"\n    from datetime import datetime, timedelta, timezone\n\n    last_activity = datetime.now(timezone.utc) - timedelta(minutes=45)\n\n    # Mock: existing conversation but expired\n    existing_conversation = {\n        \"_id\": 999,\n        \"session_id\": 789,\n        \"date_created\": datetime.now(timezone.utc) - timedelta(minutes=50),\n    }\n    last_message = {\"date_created\": last_activity}\n\n    mock_conn.execute.side_effect = [\n        existing_conversation,  # find_one for conversation\n        last_message,  # find_one for last message (expired)\n        Mock(inserted_id=202),  # insert_one returns new conversation\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 202  # Returns new conversation id\n    assert (\n        mock_conn.execute.call_count == 3\n    )  # Check conversation, check last message, insert new  # Only find_one\n\n\ndef test_conversation_message_create(mock_conn):\n    \"\"\"Test creating a conversation message.\"\"\"\n    message = ConversationMessage(mock_conn)\n    message.create(\n        conversation_id=101, role=\"user\", type=\"text\", content=\"Hello, world!\"\n    )\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify insert_one query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert insert_call[0][0] == \"memori_conversation_message\"\n    assert insert_call[0][1] == \"insert_one\"\n    doc = insert_call[0][2]\n\n    assert doc[\"conversation_id\"] == 101\n    assert doc[\"role\"] == \"user\"\n    assert doc[\"type\"] == \"text\"\n    assert doc[\"content\"] == \"Hello, world!\"\n    assert \"uuid\" in doc\n    assert \"date_created\" in doc\n\n\ndef test_conversation_messages_read(mock_conn):\n    \"\"\"Test reading conversation messages.\"\"\"\n    # Mock the find query to return cursor with messages\n    mock_cursor = [\n        {\"role\": \"user\", \"content\": \"Hello\"},\n        {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n    ]\n    mock_conn.execute.return_value = mock_cursor\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=101)\n\n    assert len(result) == 2\n    assert result[0] == {\"content\": \"Hello\", \"role\": \"user\"}\n    assert result[1] == {\"content\": \"Hi there!\", \"role\": \"assistant\"}\n\n    # Verify find query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_conversation_message\"\n    assert find_call[0][1] == \"find\"\n    assert find_call[0][2] == {\"conversation_id\": 101}\n    assert find_call[0][3] == {\"role\": 1, \"content\": 1, \"_id\": 0}\n\n\ndef test_conversation_messages_read_empty(mock_conn):\n    \"\"\"Test reading messages when none exist.\"\"\"\n    mock_conn.execute.return_value = []\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=999)\n\n    assert result == []\n\n\ndef test_schema_version_create(mock_conn):\n    \"\"\"Test creating a schema version record.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.create(num=1)\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify insert_one query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert insert_call[0][0] == \"memori_schema_version\"\n    assert insert_call[0][1] == \"insert_one\"\n    doc = insert_call[0][2]\n    assert doc[\"num\"] == 1\n\n\ndef test_schema_version_read(mock_conn):\n    \"\"\"Test reading the current schema version.\"\"\"\n    mock_result = {\"num\": 5}\n    mock_conn.execute.return_value = mock_result\n\n    schema_version = SchemaVersion(mock_conn)\n    result = schema_version.read()\n\n    assert result == 5\n\n    # Verify find_one query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_schema_version\"\n    assert find_call[0][1] == \"find_one\"\n    assert find_call[0][2] == {}\n    assert find_call[0][3] == {\"num\": 1, \"_id\": 0}\n\n\ndef test_schema_version_read_none(mock_conn):\n    \"\"\"Test reading schema version when none exists.\"\"\"\n    mock_conn.execute.return_value = None\n\n    schema_version = SchemaVersion(mock_conn)\n    result = schema_version.read()\n\n    assert result is None\n\n\ndef test_schema_version_delete(mock_conn):\n    \"\"\"Test deleting schema version records.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.delete()\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify delete_many query\n    delete_call = mock_conn.execute.call_args_list[0]\n    assert delete_call[0][0] == \"memori_schema_version\"\n    assert delete_call[0][1] == \"delete_many\"\n    assert delete_call[0][2] == {}\n\n\ndef test_schema_initialization(mock_conn):\n    \"\"\"Test that Schema initializes SchemaVersion correctly.\"\"\"\n    schema = Schema(mock_conn)\n\n    assert isinstance(schema.version, SchemaVersion)\n    assert schema.conn == mock_conn\n\n\ndef test_driver_migrations_attribute():\n    \"\"\"Test that Driver has migrations attribute.\"\"\"\n    from memori.storage.drivers.mongodb._driver import Driver\n    from memori.storage.migrations._mongodb import migrations\n\n    assert Driver.migrations == migrations\n\n\ndef test_driver_requires_rollback_on_error_attribute():\n    \"\"\"Test that Driver has requires_rollback_on_error attribute.\"\"\"\n    from memori.storage.drivers.mongodb._driver import Driver\n\n    assert Driver.requires_rollback_on_error is False\n\n\ndef test_driver_registry_registration():\n    \"\"\"Test that Driver is properly registered with the registry.\"\"\"\n    from memori.storage._registry import Registry\n\n    registry = Registry()\n    assert \"mongodb\" in registry._drivers\n\n\ndef test_mongodb_operations_with_datetime(mock_conn):\n    \"\"\"Test that MongoDB operations properly handle datetime fields.\"\"\"\n    mock_conn.execute.side_effect = [\n        None,  # find_one returns None\n        Mock(inserted_id=123),  # insert_one returns mock result\n    ]\n\n    entity = Entity(mock_conn)\n    entity.create(\"external-entity-id\")\n\n    # Verify insert_one query includes date_created\n    insert_call = mock_conn.execute.call_args_list[1]\n    doc = insert_call[0][2]\n\n    assert \"date_created\" in doc\n    assert isinstance(doc[\"date_created\"], datetime)\n    assert doc[\"date_updated\"] is None\n\n\ndef test_mongodb_conversation_message_with_datetime(mock_conn):\n    \"\"\"Test that conversation message creation includes proper datetime fields.\"\"\"\n    message = ConversationMessage(mock_conn)\n    message.create(\n        conversation_id=101, role=\"user\", type=\"text\", content=\"Test message\"\n    )\n\n    # Verify insert_one query includes date_created\n    insert_call = mock_conn.execute.call_args_list[0]\n    doc = insert_call[0][2]\n\n    assert \"date_created\" in doc\n    assert isinstance(doc[\"date_created\"], datetime)\n    assert doc[\"date_updated\"] is None\n\n\ndef test_mongodb_session_with_datetime(mock_conn):\n    \"\"\"Test that session creation includes proper datetime fields.\"\"\"\n    mock_conn.execute.side_effect = [\n        None,  # find_one returns None\n        Mock(inserted_id=789),  # insert_one returns mock result\n    ]\n\n    session = Session(mock_conn)\n    session.create(\"test-uuid\", entity_id=123, process_id=456)\n\n    # Verify insert_one query includes date_created\n    insert_call = mock_conn.execute.call_args_list[1]\n    doc = insert_call[0][2]\n\n    assert \"date_created\" in doc\n    assert isinstance(doc[\"date_created\"], datetime)\n    assert doc[\"date_updated\"] is None\n\n\ndef test_entity_fact_create_new_fact(mock_conn, mocker):\n    \"\"\"Test creating a new entity fact.\"\"\"\n    from unittest.mock import Mock\n\n    mocker.patch(\"memori._utils.generate_uniq\", return_value=\"uniq123\")\n    # Mock bson.Binary for MongoDB\n    mock_binary = Mock()\n    mock_binary.__repr__ = lambda self: \"Binary(...)\"\n    mocker.patch(\n        \"memori.embeddings.format_embedding_for_db\",\n        return_value=mock_binary,\n    )\n\n    mock_conn.execute.return_value = None  # No existing fact\n\n    entity_fact = EntityFact(mock_conn)\n    facts = [\"User likes Python\"]\n    embeddings = [[0.1, 0.2, 0.3]]\n\n    result = entity_fact.create(entity_id=123, facts=facts, fact_embeddings=embeddings)\n\n    assert result == entity_fact\n    assert mock_conn.execute.call_count == 2  # find_one, insert_one\n\n    # Verify find_one query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_entity_fact\"\n    assert find_call[0][1] == \"find_one\"\n    assert find_call[0][2] == {\"entity_id\": 123, \"uniq\": \"uniq123\"}\n\n    # Verify insert_one query\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert insert_call[0][0] == \"memori_entity_fact\"\n    assert insert_call[0][1] == \"insert_one\"\n    doc = insert_call[0][2]\n    assert doc[\"entity_id\"] == 123\n    assert doc[\"content\"] == \"User likes Python\"\n    # content_embedding is now a Mock object representing bson.Binary\n    assert doc[\"content_embedding\"] is not None\n    assert doc[\"num_times\"] == 1\n    assert doc[\"uniq\"] == \"uniq123\"\n    assert \"uuid\" in doc\n    assert \"date_created\" in doc\n    assert isinstance(doc[\"date_created\"], datetime)\n\n\ndef test_entity_fact_create_existing_fact(mock_conn, mocker):\n    \"\"\"Test updating an existing entity fact.\"\"\"\n    from unittest.mock import Mock\n\n    mocker.patch(\"memori._utils.generate_uniq\", return_value=\"uniq123\")\n    mock_binary = Mock()\n    mocker.patch(\n        \"memori.embeddings.format_embedding_for_db\",\n        return_value=mock_binary,\n    )\n\n    # Mock existing fact\n    existing = {\"_id\": 999, \"num_times\": 5}\n    mock_conn.execute.return_value = existing\n\n    entity_fact = EntityFact(mock_conn)\n    facts = [\"User likes Python\"]\n    embeddings = [[0.1, 0.2, 0.3]]\n\n    result = entity_fact.create(entity_id=123, facts=facts, fact_embeddings=embeddings)\n\n    assert result == entity_fact\n    assert mock_conn.execute.call_count == 2  # find_one, update_one\n\n    # Verify find_one query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_entity_fact\"\n    assert find_call[0][1] == \"find_one\"\n\n    # Verify update_one query\n    update_call = mock_conn.execute.call_args_list[1]\n    assert update_call[0][0] == \"memori_entity_fact\"\n    assert update_call[0][1] == \"update_one\"\n    assert update_call[0][2] == {\"_id\": 999}\n    update_doc = update_call[0][3]\n    assert \"$inc\" in update_doc\n    assert update_doc[\"$inc\"][\"num_times\"] == 1\n    assert \"$set\" in update_doc\n    assert \"date_last_time\" in update_doc[\"$set\"]\n    assert isinstance(update_doc[\"$set\"][\"date_last_time\"], datetime)\n\n\ndef test_entity_fact_create_empty_facts(mock_conn):\n    \"\"\"Test creating entity facts with empty list.\"\"\"\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.create(entity_id=123, facts=[], fact_embeddings=None)\n\n    assert result == entity_fact\n    assert mock_conn.execute.call_count == 0\n\n\ndef test_entity_fact_create_multiple_facts(mock_conn, mocker):\n    \"\"\"Test creating multiple entity facts.\"\"\"\n    from unittest.mock import Mock\n\n    mocker.patch(\n        \"memori._utils.generate_uniq\",\n        side_effect=[\"uniq1\", \"uniq2\"],\n    )\n    mock_binary1 = Mock()\n    mock_binary2 = Mock()\n    mocker.patch(\n        \"memori.embeddings.format_embedding_for_db\",\n        side_effect=[mock_binary1, mock_binary2],\n    )\n\n    mock_conn.execute.side_effect = [None, None, None, None]  # No existing facts\n\n    entity_fact = EntityFact(mock_conn)\n    facts = [\"Fact 1\", \"Fact 2\"]\n    embeddings = [[0.1, 0.2], [0.3, 0.4]]\n\n    entity_fact.create(entity_id=123, facts=facts, fact_embeddings=embeddings)\n\n    # Should be 4 calls: find_one, insert_one for each fact\n    assert mock_conn.execute.call_count == 4\n\n\ndef test_entity_fact_create_without_embeddings(mock_conn, mocker):\n    \"\"\"Test creating entity facts without embeddings.\"\"\"\n    from unittest.mock import Mock\n\n    mocker.patch(\"memori._utils.generate_uniq\", return_value=\"uniq123\")\n    mock_binary = Mock()\n    mocker.patch(\n        \"memori.embeddings.format_embedding_for_db\",\n        return_value=mock_binary,\n    )\n\n    mock_conn.execute.return_value = None\n\n    entity_fact = EntityFact(mock_conn)\n    facts = [\"User likes Python\"]\n\n    entity_fact.create(entity_id=123, facts=facts, fact_embeddings=None)\n\n    # Verify embedding was formatted (as Mock object representing bson.Binary)\n    insert_call = mock_conn.execute.call_args_list[1]\n    doc = insert_call[0][2]\n    assert doc[\"content_embedding\"] is not None\n\n\ndef test_entity_fact_get_embeddings(mock_conn):\n    \"\"\"Test retrieving embeddings for an entity.\"\"\"\n    mock_cursor = mock_conn.execute.return_value\n    mock_cursor.sort.return_value = mock_cursor\n    mock_cursor.limit.return_value = [\n        {\"_id\": 1, \"content_embedding\": b\"\\x00\\x01\\x02\\x03\"},\n        {\"_id\": 2, \"content_embedding\": b\"\\x04\\x05\\x06\\x07\"},\n    ]\n\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.get_embeddings(entity_id=123, limit=100)\n\n    assert len(result) == 2\n    assert result[0][\"id\"] == 1\n    assert result[0][\"content_embedding\"] == b\"\\x00\\x01\\x02\\x03\"\n    assert result[1][\"id\"] == 2\n    assert result[1][\"content_embedding\"] == b\"\\x04\\x05\\x06\\x07\"\n\n    # Verify find query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_entity_fact\"\n    assert find_call[0][1] == \"find\"\n    assert find_call[0][2] == {\"entity_id\": 123}\n    assert find_call[0][3] == {\"_id\": 1, \"content_embedding\": 1}\n    mock_cursor.sort.assert_called_once_with(\n        [(\"date_last_time\", -1), (\"num_times\", -1), (\"_id\", -1)]\n    )\n    mock_cursor.limit.assert_called_once_with(100)\n\n\ndef test_entity_fact_get_embeddings_with_limit(mock_conn):\n    \"\"\"Test retrieving embeddings respects the limit.\"\"\"\n    # Return more results than the limit\n    mock_cursor = mock_conn.execute.return_value\n    mock_cursor.sort.return_value = mock_cursor\n    mock_cursor.limit.return_value = [\n        {\"_id\": i, \"content_embedding\": bytes([i])} for i in range(1, 6)\n    ]\n\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.get_embeddings(entity_id=123, limit=5)\n\n    # Should only return first 5 results\n    assert len(result) == 5\n    assert result[0][\"id\"] == 1\n    assert result[4][\"id\"] == 5\n\n\ndef test_entity_fact_get_embeddings_default_limit(mock_conn):\n    \"\"\"Test retrieving embeddings with default limit.\"\"\"\n    mock_conn.execute.return_value = []\n\n    entity_fact = EntityFact(mock_conn)\n    entity_fact.get_embeddings(entity_id=123)\n\n    # Verify default limit is used in slicing (1000)\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_entity_fact\"\n\n\ndef test_entity_fact_get_facts_by_ids(mock_conn):\n    \"\"\"Test retrieving fact content by IDs.\"\"\"\n    mock_cursor = [\n        {\n            \"_id\": 1,\n            \"content\": \"User likes Python\",\n            \"date_created\": \"2026-01-01 10:30:00\",\n        },\n        {\n            \"_id\": 2,\n            \"content\": \"User works as engineer\",\n            \"date_created\": \"2026-01-02 11:15:00\",\n        },\n    ]\n    mock_conn.execute.return_value = mock_cursor\n\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.get_facts_by_ids([1, 2])\n\n    assert len(result) == 2\n    assert result[0][\"id\"] == 1\n    assert result[0][\"content\"] == \"User likes Python\"\n    assert result[0][\"date_created\"] == \"2026-01-01 10:30:00\"\n    assert result[1][\"id\"] == 2\n    assert result[1][\"content\"] == \"User works as engineer\"\n    assert result[1][\"date_created\"] == \"2026-01-02 11:15:00\"\n\n    # Verify find query\n    find_call = mock_conn.execute.call_args_list[0]\n    assert find_call[0][0] == \"memori_entity_fact\"\n    assert find_call[0][1] == \"find\"\n    assert find_call[0][2] == {\"_id\": {\"$in\": [1, 2]}}\n    assert find_call[0][3] == {\"_id\": 1, \"content\": 1, \"date_created\": 1}\n\n\ndef test_entity_fact_get_facts_by_ids_empty(mock_conn):\n    \"\"\"Test retrieving facts with empty IDs list.\"\"\"\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.get_facts_by_ids([])\n\n    assert result == []\n    assert mock_conn.execute.call_count == 0\n"
  },
  {
    "path": "tests/storage/drivers/test_mysql_driver.py",
    "content": "from unittest.mock import MagicMock\nfrom uuid import UUID\n\nfrom memori.storage.drivers.mysql._driver import (\n    Conversation,\n    ConversationMessage,\n    ConversationMessages,\n    Driver,\n    Entity,\n    Process,\n    Schema,\n    SchemaVersion,\n    Session,\n)\n\n\ndef test_driver_initialization(mock_conn):\n    \"\"\"Test that Driver initializes all components correctly.\"\"\"\n    driver = Driver(mock_conn)\n\n    assert isinstance(driver.conversation, Conversation)\n    assert isinstance(driver.entity, Entity)\n    assert isinstance(driver.process, Process)\n    assert isinstance(driver.schema, Schema)\n    assert isinstance(driver.session, Session)\n\n\ndef test_entity_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a entity record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    entity = Entity(mock_conn)\n    result = entity.create(\"external-entity-id\")\n\n    assert result == 123\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT IGNORE INTO memori_entity\" in insert_call[0][0]\n    assert insert_call[0][1][1] == \"external-entity-id\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_entity\" in select_call[0][0]\n    assert select_call[0][1] == (\"external-entity-id\",)\n\n\ndef test_entity_generates_uuid(mock_conn, mock_single_result):\n    \"\"\"Test that create generates a valid UUID.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    entity = Entity(mock_conn)\n    entity.create(\"external-entity-id\")\n\n    # Check that a UUID was generated in the INSERT\n    insert_call = mock_conn.execute.call_args_list[0]\n    uuid_arg = insert_call[0][1][0]\n\n    # Verify it's a valid UUID object\n    assert isinstance(uuid_arg, UUID)\n\n\ndef test_process_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a process record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 456})\n\n    process = Process(mock_conn)\n    result = process.create(\"external-process-id\")\n\n    assert result == 456\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT IGNORE INTO memori_process\" in insert_call[0][0]\n    assert insert_call[0][1][1] == \"external-process-id\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_process\" in select_call[0][0]\n    assert select_call[0][1] == (\"external-process-id\",)\n\n\ndef test_session_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a session record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 789})\n\n    session = Session(mock_conn)\n    session_uuid = \"test-session-uuid\"\n    result = session.create(session_uuid, entity_id=123, process_id=456)\n\n    assert result == 789\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT IGNORE INTO memori_session\" in insert_call[0][0]\n    assert insert_call[0][1] == (session_uuid, 123, 456)\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_session\" in select_call[0][0]\n    assert select_call[0][1] == (session_uuid,)\n\n\ndef test_conversation_initialization(mock_conn):\n    \"\"\"Test that Conversation initializes its sub-components.\"\"\"\n    conversation = Conversation(mock_conn)\n\n    assert isinstance(conversation.message, ConversationMessage)\n    assert isinstance(conversation.messages, ConversationMessages)\n    assert conversation.conn == mock_conn\n\n\ndef test_conversation_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a conversation record when none exists.\"\"\"\n    mock_empty_result = MagicMock()\n    mock_empty_result.mappings.return_value.fetchone.return_value = None\n    mock_conn.execute.side_effect = [\n        mock_empty_result,\n        None,\n        mock_single_result({\"id\": 101}),\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101\n    assert mock_conn.execute.call_count == 3  # Check existing, INSERT, SELECT\n    assert mock_conn.commit.call_count == 1\n\n    # Verify check for existing conversation\n    check_call = mock_conn.execute.call_args_list[0]\n    assert (\n        \"COALESCE(MAX(m.date_created), c.date_created) as last_activity\"\n        in check_call[0][0]\n    )\n    assert check_call[0][1] == (789,)\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert \"INSERT IGNORE INTO memori_conversation\" in insert_call[0][0]\n\n    # Verify the UUID is generated and session_id is passed\n    uuid_arg, session_id_arg = insert_call[0][1]\n    assert isinstance(uuid_arg, UUID)\n    assert session_id_arg == 789\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[2]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_conversation\" in select_call[0][0]\n    assert select_call[0][1] == (789,)\n\n\ndef test_conversation_create_returns_existing_within_timeout(mock_conn):\n    \"\"\"Test returning existing conversation when within timeout period.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=15)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [15.0]  # 15 minutes elapsed\n\n    mock_conn.execute.side_effect = [\n        mock_existing,  # Existing conversation found\n        mock_timeout_check,  # Time check: 15 min < 30 min timeout\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101  # Returns existing conversation id\n    assert mock_conn.execute.call_count == 2  # Check existing, check timeout\n    assert mock_conn.commit.call_count == 0  # No insert, no commit\n\n\ndef test_conversation_create_new_when_expired(mock_conn, mock_single_result):\n    \"\"\"Test creating new conversation when existing one is expired.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=45)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [45.0]  # 45 minutes elapsed\n\n    mock_conn.execute.side_effect = [\n        mock_existing,  # Existing conversation found\n        mock_timeout_check,  # Time check: 45 min > 30 min timeout\n        None,  # INSERT (no return value needed)\n        mock_single_result({\"id\": 202}),  # SELECT returns new conversation id\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 202  # Returns new conversation id\n    assert (\n        mock_conn.execute.call_count == 4\n    )  # Check existing, check timeout, INSERT, SELECT\n    assert mock_conn.commit.call_count == 1  # Committed new conversation\n\n\ndef test_conversation_message_create(mock_conn):\n    \"\"\"Test creating a conversation message.\"\"\"\n    message = ConversationMessage(mock_conn)\n    message.create(\n        conversation_id=101, role=\"user\", type=\"text\", content=\"Hello, world!\"\n    )\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_conversation_message\" in insert_call[0][0]\n\n    # Verify parameters\n    uuid_arg, conv_id, role, type_, content = insert_call[0][1]\n    assert isinstance(uuid_arg, UUID)\n    assert conv_id == 101\n    assert role == \"user\"\n    assert type_ == \"text\"\n    assert content == \"Hello, world!\"\n\n\ndef test_conversation_messages_read(mock_conn, mock_multiple_results):\n    \"\"\"Test reading conversation messages.\"\"\"\n    mock_conn.execute.return_value = mock_multiple_results(\n        [\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n        ]\n    )\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=101)\n\n    assert len(result) == 2\n    assert result[0] == {\"content\": \"Hello\", \"role\": \"user\"}\n    assert result[1] == {\"content\": \"Hi there!\", \"role\": \"assistant\"}\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT role\" in select_call[0][0]\n    assert \"FROM memori_conversation_message\" in select_call[0][0]\n    assert select_call[0][1] == (101,)\n\n\ndef test_conversation_messages_read_empty(mock_conn, mock_empty_result):\n    \"\"\"Test reading messages when none exist.\"\"\"\n    mock_conn.execute.return_value = mock_empty_result\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=999)\n\n    assert result == []\n\n\ndef test_schema_version_create(mock_conn):\n    \"\"\"Test creating a schema version record.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.create(num=1)\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_schema_version\" in insert_call[0][0]\n    assert insert_call[0][1] == (1,)\n\n\ndef test_schema_version_read(mock_conn, mock_single_result):\n    \"\"\"Test reading the current schema version.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"num\": 5})\n\n    schema_version = SchemaVersion(mock_conn)\n    result = schema_version.read()\n\n    assert result == 5\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT num\" in select_call[0][0]\n    assert \"FROM memori_schema_version\" in select_call[0][0]\n\n\ndef test_schema_version_delete(mock_conn):\n    \"\"\"Test deleting schema version records.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.delete()\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify DELETE query\n    delete_call = mock_conn.execute.call_args_list[0]\n    assert \"DELETE FROM memori_schema_version\" in delete_call[0][0]\n\n\ndef test_schema_initialization(mock_conn):\n    \"\"\"Test that Schema initializes SchemaVersion correctly.\"\"\"\n    schema = Schema(mock_conn)\n\n    assert isinstance(schema.version, SchemaVersion)\n    assert schema.conn == mock_conn\n"
  },
  {
    "path": "tests/storage/drivers/test_oceanbase_driver.py",
    "content": "from unittest.mock import MagicMock, patch\nfrom uuid import UUID\n\nfrom memori._utils import generate_uniq\nfrom memori.storage.drivers.mysql._driver import (\n    Conversation,\n    ConversationMessage,\n    ConversationMessages,\n    Entity,\n    Process,\n    Schema,\n    Session,\n)\nfrom memori.storage.drivers.oceanbase._driver import Driver, EntityFact\nfrom memori.storage.migrations._oceanbase import migrations\n\n\ndef test_driver_initialization(mock_conn):\n    \"\"\"Test that OceanBase Driver initializes all components correctly.\"\"\"\n    driver = Driver(mock_conn)\n\n    assert isinstance(driver.conversation, Conversation)\n    assert isinstance(driver.entity, Entity)\n    assert isinstance(driver.process, Process)\n    assert isinstance(driver.schema, Schema)\n    assert isinstance(driver.session, Session)\n    assert driver.entity_fact.__class__ is EntityFact\n\n\ndef test_driver_metadata():\n    \"\"\"Test driver attributes for OceanBase.\"\"\"\n    assert Driver.migrations == migrations\n    assert Driver.requires_rollback_on_error is True\n\n\ndef test_entity_fact_create_uses_formatted_embedding(mock_conn):\n    \"\"\"Test that EntityFact.create uses formatted embedding for OceanBase.\"\"\"\n    mock_conn.get_dialect.return_value = \"oceanbase\"\n\n    entity_fact = EntityFact(mock_conn)\n\n    with patch(\n        \"memori.embeddings.format_embedding_for_db\",\n        return_value=\"formatted-embedding\",\n    ) as format_mock:\n        entity_fact.create(\n            entity_id=123,\n            facts=[\"fact-1\"],\n            fact_embeddings=[[0.1, 0.2, 0.3]],\n        )\n\n    assert format_mock.called\n    assert mock_conn.execute.call_count == 1\n    assert mock_conn.commit.call_count == 1\n\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_entity_fact\" in insert_call[0][0]\n    assert \"ON DUPLICATE KEY UPDATE\" in insert_call[0][0]\n    assert insert_call[0][1][1] == 123\n    assert insert_call[0][1][2] == \"fact-1\"\n    assert insert_call[0][1][3] == \"formatted-embedding\"\n    assert insert_call[0][1][5] == generate_uniq([\"fact-1\"])\n\n\ndef test_entity_create(mock_conn, mock_single_result):\n    \"\"\"Test creating an entity record via OceanBase driver.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    driver = Driver(mock_conn)\n    result = driver.entity.create(\"external-entity-id\")\n\n    assert result == 123\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT IGNORE INTO memori_entity\" in insert_call[0][0]\n    assert insert_call[0][1][1] == \"external-entity-id\"\n\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_entity\" in select_call[0][0]\n    assert select_call[0][1] == (\"external-entity-id\",)\n\n\ndef test_entity_generates_uuid(mock_conn, mock_single_result):\n    \"\"\"Test that entity create generates a valid UUID.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    driver = Driver(mock_conn)\n    driver.entity.create(\"external-entity-id\")\n\n    insert_call = mock_conn.execute.call_args_list[0]\n    uuid_arg = insert_call[0][1][0]\n    assert isinstance(uuid_arg, UUID)\n\n\ndef test_process_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a process record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 456})\n\n    driver = Driver(mock_conn)\n    result = driver.process.create(\"external-process-id\")\n\n    assert result == 456\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT IGNORE INTO memori_process\" in insert_call[0][0]\n    assert insert_call[0][1][1] == \"external-process-id\"\n\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_process\" in select_call[0][0]\n    assert select_call[0][1] == (\"external-process-id\",)\n\n\ndef test_session_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a session record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 789})\n\n    driver = Driver(mock_conn)\n    session_uuid = \"test-session-uuid\"\n    result = driver.session.create(session_uuid, entity_id=123, process_id=456)\n\n    assert result == 789\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT IGNORE INTO memori_session\" in insert_call[0][0]\n    assert insert_call[0][1] == (session_uuid, 123, 456)\n\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_session\" in select_call[0][0]\n    assert select_call[0][1] == (session_uuid,)\n\n\ndef test_conversation_initialization(mock_conn):\n    \"\"\"Test that Conversation initializes its sub-components.\"\"\"\n    driver = Driver(mock_conn)\n    conversation = driver.conversation\n\n    assert isinstance(conversation.message, ConversationMessage)\n    assert isinstance(conversation.messages, ConversationMessages)\n    assert conversation.conn == mock_conn\n\n\ndef test_conversation_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a conversation record when none exists.\"\"\"\n    mock_empty_result = MagicMock()\n    mock_empty_result.mappings.return_value.fetchone.return_value = None\n    mock_conn.execute.side_effect = [\n        mock_empty_result,\n        None,\n        mock_single_result({\"id\": 101}),\n    ]\n\n    driver = Driver(mock_conn)\n    result = driver.conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101\n    assert mock_conn.execute.call_count == 3\n    assert mock_conn.commit.call_count == 1\n\n    check_call = mock_conn.execute.call_args_list[0]\n    assert (\n        \"COALESCE(MAX(m.date_created), c.date_created) as last_activity\"\n        in check_call[0][0]\n    )\n    assert check_call[0][1] == (789,)\n\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert \"INSERT IGNORE INTO memori_conversation\" in insert_call[0][0]\n\n    select_call = mock_conn.execute.call_args_list[2]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_conversation\" in select_call[0][0]\n    assert select_call[0][1] == (789,)\n\n\ndef test_conversation_create_returns_existing_within_timeout(mock_conn):\n    \"\"\"Test returning existing conversation when within timeout period.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=15)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [15.0]\n\n    mock_conn.execute.side_effect = [\n        mock_existing,\n        mock_timeout_check,\n    ]\n\n    driver = Driver(mock_conn)\n    result = driver.conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 0\n\n\ndef test_conversation_create_new_when_expired(mock_conn, mock_single_result):\n    \"\"\"Test creating new conversation when existing one is expired.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=45)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [45.0]\n\n    mock_conn.execute.side_effect = [\n        mock_existing,\n        mock_timeout_check,\n        None,\n        mock_single_result({\"id\": 202}),\n    ]\n\n    driver = Driver(mock_conn)\n    result = driver.conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 202\n    assert mock_conn.execute.call_count == 4\n    assert mock_conn.commit.call_count == 1\n\n\ndef test_conversation_message_create(mock_conn):\n    \"\"\"Test creating a conversation message.\"\"\"\n    driver = Driver(mock_conn)\n    driver.conversation.message.create(\n        conversation_id=101, role=\"user\", type=\"text\", content=\"Hello, world!\"\n    )\n\n    assert mock_conn.execute.call_count == 1\n\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_conversation_message\" in insert_call[0][0]\n\n    uuid_arg, conv_id, role, type_, content = insert_call[0][1]\n    assert isinstance(uuid_arg, UUID)\n    assert conv_id == 101\n    assert role == \"user\"\n    assert type_ == \"text\"\n    assert content == \"Hello, world!\"\n\n\ndef test_conversation_messages_read(mock_conn, mock_multiple_results):\n    \"\"\"Test reading conversation messages.\"\"\"\n    mock_conn.execute.return_value = mock_multiple_results(\n        [\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n        ]\n    )\n\n    driver = Driver(mock_conn)\n    result = driver.conversation.messages.read(conversation_id=101)\n\n    assert len(result) == 2\n    assert result[0] == {\"content\": \"Hello\", \"role\": \"user\"}\n    assert result[1] == {\"content\": \"Hi there!\", \"role\": \"assistant\"}\n\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT role\" in select_call[0][0]\n    assert \"FROM memori_conversation_message\" in select_call[0][0]\n    assert select_call[0][1] == (101,)\n\n\ndef test_conversation_messages_read_empty(mock_conn, mock_empty_result):\n    \"\"\"Test reading messages when none exist.\"\"\"\n    mock_conn.execute.return_value = mock_empty_result\n\n    driver = Driver(mock_conn)\n    result = driver.conversation.messages.read(conversation_id=999)\n\n    assert result == []\n\n\ndef test_schema_version_create(mock_conn):\n    \"\"\"Test creating a schema version record.\"\"\"\n    driver = Driver(mock_conn)\n    driver.schema.version.create(num=1)\n\n    assert mock_conn.execute.call_count == 1\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_schema_version\" in insert_call[0][0]\n    assert insert_call[0][1] == (1,)\n\n\ndef test_schema_version_read(mock_conn, mock_single_result):\n    \"\"\"Test reading the current schema version.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"num\": 5})\n\n    driver = Driver(mock_conn)\n    result = driver.schema.version.read()\n\n    assert result == 5\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT num\" in select_call[0][0]\n    assert \"FROM memori_schema_version\" in select_call[0][0]\n\n\ndef test_schema_version_delete(mock_conn):\n    \"\"\"Test deleting schema version records.\"\"\"\n    driver = Driver(mock_conn)\n    driver.schema.version.delete()\n\n    assert mock_conn.execute.call_count == 1\n    delete_call = mock_conn.execute.call_args_list[0]\n    assert \"DELETE FROM memori_schema_version\" in delete_call[0][0]\n"
  },
  {
    "path": "tests/storage/drivers/test_oracle_driver.py",
    "content": "from unittest.mock import MagicMock\nfrom uuid import UUID\n\nfrom memori.storage.drivers.oracle._driver import (\n    Conversation,\n    ConversationMessage,\n    ConversationMessages,\n    Driver,\n    Entity,\n    Process,\n    Schema,\n    SchemaVersion,\n    Session,\n)\n\n\ndef test_driver_initialization(mock_conn):\n    \"\"\"Test that Driver initializes all components correctly.\"\"\"\n    driver = Driver(mock_conn)\n\n    assert isinstance(driver.conversation, Conversation)\n    assert isinstance(driver.entity, Entity)\n    assert isinstance(driver.process, Process)\n    assert isinstance(driver.schema, Schema)\n    assert isinstance(driver.session, Session)\n\n\ndef test_entity_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a entity record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    entity = Entity(mock_conn)\n    result = entity.create(\"external-entity-id\")\n\n    assert result == 123\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify MERGE query (Oracle uses MERGE instead of INSERT...ON CONFLICT)\n    merge_call = mock_conn.execute.call_args_list[0]\n    assert \"MERGE INTO memori_entity\" in merge_call[0][0]\n    assert \"USING (SELECT :1\" in merge_call[0][0]\n    assert \"FROM DUAL)\" in merge_call[0][0]\n    assert merge_call[0][1][1] == \"external-entity-id\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_entity\" in select_call[0][0]\n    assert select_call[0][1] == (\"external-entity-id\",)\n\n\ndef test_entity_generates_uuid(mock_conn, mock_single_result):\n    \"\"\"Test that create generates a valid UUID.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    entity = Entity(mock_conn)\n    entity.create(\"external-entity-id\")\n\n    # Check that a UUID was generated in the MERGE\n    merge_call = mock_conn.execute.call_args_list[0]\n    uuid_arg = merge_call[0][1][0]\n\n    # Verify it's a valid UUID string\n    UUID(uuid_arg)  # Will raise ValueError if invalid\n\n\ndef test_process_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a process record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 456})\n\n    process = Process(mock_conn)\n    result = process.create(\"external-process-id\")\n\n    assert result == 456\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify MERGE query\n    merge_call = mock_conn.execute.call_args_list[0]\n    assert \"MERGE INTO memori_process\" in merge_call[0][0]\n    assert \"USING (SELECT :1\" in merge_call[0][0]\n    assert \"FROM DUAL)\" in merge_call[0][0]\n    assert merge_call[0][1][1] == \"external-process-id\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_process\" in select_call[0][0]\n    assert select_call[0][1] == (\"external-process-id\",)\n\n\ndef test_session_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a session record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 789})\n\n    session = Session(mock_conn)\n    session_uuid = \"test-session-uuid\"\n    result = session.create(session_uuid, entity_id=123, process_id=456)\n\n    assert result == 789\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify MERGE query\n    merge_call = mock_conn.execute.call_args_list[0]\n    assert \"MERGE INTO memori_session\" in merge_call[0][0]\n    assert \"USING (SELECT :1\" in merge_call[0][0]\n    assert \"FROM DUAL)\" in merge_call[0][0]\n    assert merge_call[0][1] == (session_uuid, 123, 456)\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_session\" in select_call[0][0]\n    assert select_call[0][1] == (session_uuid,)\n\n\ndef test_conversation_initialization(mock_conn):\n    \"\"\"Test that Conversation initializes its sub-components.\"\"\"\n    conversation = Conversation(mock_conn)\n\n    assert isinstance(conversation.message, ConversationMessage)\n    assert isinstance(conversation.messages, ConversationMessages)\n    assert conversation.conn == mock_conn\n\n\ndef test_conversation_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a new conversation when no existing conversation found.\"\"\"\n    mock_no_existing = mock_single_result(None)\n    mock_new_id = mock_single_result({\"id\": 101})\n\n    mock_conn.execute.side_effect = [\n        mock_no_existing,  # No existing conversation\n        None,  # MERGE (no return value needed)\n        mock_new_id,  # SELECT returns new conversation id\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101\n    assert mock_conn.execute.call_count == 3\n    assert mock_conn.commit.call_count == 1\n\n    # Verify check for existing conversation\n    existing_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT c.id\" in existing_call[0][0]\n    assert \"COALESCE(MAX(m.date_created), c.date_created)\" in existing_call[0][0]\n    assert existing_call[0][1] == (789,)\n\n    # Verify MERGE query\n    merge_call = mock_conn.execute.call_args_list[1]\n    assert \"MERGE INTO memori_conversation\" in merge_call[0][0]\n    assert \"USING (SELECT :1\" in merge_call[0][0]\n    assert \"FROM DUAL)\" in merge_call[0][0]\n\n    # Verify the UUID is generated and session_id is passed\n    uuid_arg, session_id_arg = merge_call[0][1]\n    UUID(uuid_arg)  # Validate UUID\n    assert session_id_arg == 789\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[2]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_conversation\" in select_call[0][0]\n    assert select_call[0][1] == (789,)\n\n\ndef test_conversation_create_returns_existing_within_timeout(mock_conn):\n    \"\"\"Test returning existing conversation when within timeout period.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=15)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [15.0]  # 15 minutes elapsed\n\n    mock_conn.execute.side_effect = [\n        mock_existing,  # Existing conversation found\n        mock_timeout_check,  # Time check: 15 min < 30 min timeout\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101  # Returns existing conversation id\n    assert mock_conn.execute.call_count == 2  # Check existing, check timeout\n    assert mock_conn.commit.call_count == 0  # No insert, no commit\n\n\ndef test_conversation_create_new_when_expired(mock_conn, mock_single_result):\n    \"\"\"Test creating new conversation when existing one is expired.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=45)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [45.0]  # 45 minutes elapsed\n\n    mock_conn.execute.side_effect = [\n        mock_existing,  # Existing conversation found\n        mock_timeout_check,  # Time check: 45 min > 30 min timeout\n        None,  # MERGE (no return value needed)\n        mock_single_result({\"id\": 202}),  # SELECT returns new conversation id\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 202  # Returns new conversation id\n    assert (\n        mock_conn.execute.call_count == 4\n    )  # Check existing, check timeout, MERGE, SELECT\n    assert mock_conn.commit.call_count == 1  # Committed new conversation\n\n\ndef test_conversation_message_create(mock_conn):\n    \"\"\"Test creating a conversation message.\"\"\"\n    message = ConversationMessage(mock_conn)\n    message.create(\n        conversation_id=101, role=\"user\", type=\"text\", content=\"Hello, world!\"\n    )\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify INSERT query (messages use INSERT, not MERGE)\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_conversation_message\" in insert_call[0][0]\n\n    # Verify parameters (Oracle uses :1, :2, :3 instead of %s)\n    uuid_arg, conv_id, role, type_, content = insert_call[0][1]\n    UUID(uuid_arg)  # Validate UUID\n    assert conv_id == 101\n    assert role == \"user\"\n    assert type_ == \"text\"\n    assert content == \"Hello, world!\"\n\n\ndef test_conversation_messages_read(mock_conn, mock_multiple_results):\n    \"\"\"Test reading conversation messages.\"\"\"\n    mock_conn.execute.return_value = mock_multiple_results(\n        [\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n        ]\n    )\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=101)\n\n    assert len(result) == 2\n    assert result[0] == {\"content\": \"Hello\", \"role\": \"user\"}\n    assert result[1] == {\"content\": \"Hi there!\", \"role\": \"assistant\"}\n\n    # Verify SELECT query (Oracle uses :1 instead of %s)\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT role\" in select_call[0][0]\n    assert \"FROM memori_conversation_message\" in select_call[0][0]\n    assert select_call[0][1] == (101,)\n\n\ndef test_conversation_messages_read_empty(mock_conn, mock_empty_result):\n    \"\"\"Test reading messages when none exist.\"\"\"\n    mock_conn.execute.return_value = mock_empty_result\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=999)\n\n    assert result == []\n\n\ndef test_schema_version_create(mock_conn):\n    \"\"\"Test creating a schema version record.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.create(num=1)\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify INSERT query (Oracle uses :1 instead of %s)\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_schema_version\" in insert_call[0][0]\n    assert insert_call[0][1] == (1,)\n\n\ndef test_schema_version_read(mock_conn, mock_single_result):\n    \"\"\"Test reading the current schema version.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"num\": 5})\n\n    schema_version = SchemaVersion(mock_conn)\n    result = schema_version.read()\n\n    assert result == 5\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT num\" in select_call[0][0]\n    assert \"FROM memori_schema_version\" in select_call[0][0]\n\n\ndef test_schema_version_delete(mock_conn):\n    \"\"\"Test deleting schema version records.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.delete()\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify DELETE query\n    delete_call = mock_conn.execute.call_args_list[0]\n    assert \"DELETE FROM memori_schema_version\" in delete_call[0][0]\n\n\ndef test_schema_initialization(mock_conn):\n    \"\"\"Test that Schema initializes SchemaVersion correctly.\"\"\"\n    schema = Schema(mock_conn)\n\n    assert isinstance(schema.version, SchemaVersion)\n    assert schema.conn == mock_conn\n"
  },
  {
    "path": "tests/storage/drivers/test_postgresql_driver.py",
    "content": "from unittest.mock import MagicMock\nfrom uuid import UUID\n\nfrom memori.storage.drivers.postgresql._driver import (\n    Conversation,\n    ConversationMessage,\n    ConversationMessages,\n    Driver,\n    Entity,\n    Process,\n    Schema,\n    SchemaVersion,\n    Session,\n)\n\n\ndef test_driver_initialization(mock_conn):\n    \"\"\"Test that Driver initializes all components correctly.\"\"\"\n    driver = Driver(mock_conn)\n\n    assert isinstance(driver.conversation, Conversation)\n    assert isinstance(driver.entity, Entity)\n    assert isinstance(driver.process, Process)\n    assert isinstance(driver.schema, Schema)\n    assert isinstance(driver.session, Session)\n\n\ndef test_entity_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a entity record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    entity = Entity(mock_conn)\n    result = entity.create(\"external-entity-id\")\n\n    assert result == 123\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_entity\" in insert_call[0][0]\n    assert \"ON CONFLICT DO NOTHING\" in insert_call[0][0]\n    assert insert_call[0][1][1] == \"external-entity-id\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_entity\" in select_call[0][0]\n    assert select_call[0][1] == (\"external-entity-id\",)\n\n\ndef test_entity_generates_uuid(mock_conn, mock_single_result):\n    \"\"\"Test that create generates a valid UUID.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    entity = Entity(mock_conn)\n    entity.create(\"external-entity-id\")\n\n    # Check that a UUID was generated in the INSERT\n    insert_call = mock_conn.execute.call_args_list[0]\n    uuid_arg = insert_call[0][1][0]\n\n    # Verify it's a valid UUID string\n    UUID(uuid_arg)  # Will raise ValueError if invalid\n\n\ndef test_process_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a process record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 456})\n\n    process = Process(mock_conn)\n    result = process.create(\"external-process-id\")\n\n    assert result == 456\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_process\" in insert_call[0][0]\n    assert \"ON CONFLICT DO NOTHING\" in insert_call[0][0]\n    assert insert_call[0][1][1] == \"external-process-id\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_process\" in select_call[0][0]\n    assert select_call[0][1] == (\"external-process-id\",)\n\n\ndef test_session_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a session record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 789})\n\n    session = Session(mock_conn)\n    session_uuid = \"test-session-uuid\"\n    result = session.create(session_uuid, entity_id=123, process_id=456)\n\n    assert result == 789\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_session\" in insert_call[0][0]\n    assert \"ON CONFLICT DO NOTHING\" in insert_call[0][0]\n    assert insert_call[0][1] == (session_uuid, 123, 456)\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_session\" in select_call[0][0]\n    assert select_call[0][1] == (session_uuid,)\n\n\ndef test_conversation_initialization(mock_conn):\n    \"\"\"Test that Conversation initializes its sub-components.\"\"\"\n    conversation = Conversation(mock_conn)\n\n    assert isinstance(conversation.message, ConversationMessage)\n    assert isinstance(conversation.messages, ConversationMessages)\n    assert conversation.conn == mock_conn\n\n\ndef test_conversation_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a conversation record when none exists.\"\"\"\n    mock_empty_result = MagicMock()\n    mock_empty_result.mappings.return_value.fetchone.return_value = None\n    mock_conn.execute.side_effect = [\n        mock_empty_result,\n        None,\n        mock_single_result({\"id\": 101}),\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101\n    assert mock_conn.execute.call_count == 3  # Check existing, INSERT, SELECT\n    assert mock_conn.commit.call_count == 1\n\n    # Verify check for existing conversation\n    check_call = mock_conn.execute.call_args_list[0]\n    assert (\n        \"COALESCE(MAX(m.date_created), c.date_created) as last_activity\"\n        in check_call[0][0]\n    )\n    assert check_call[0][1] == (789,)\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert \"INSERT INTO memori_conversation\" in insert_call[0][0]\n    assert \"ON CONFLICT DO NOTHING\" in insert_call[0][0]\n\n    # Verify the UUID is generated and session_id is passed\n    uuid_arg, session_id_arg = insert_call[0][1]\n    UUID(uuid_arg)  # Validate UUID\n    assert session_id_arg == 789\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[2]\n    assert \"SELECT id\" in select_call[0][0]\n    assert \"FROM memori_conversation\" in select_call[0][0]\n    assert select_call[0][1] == (789,)\n\n\ndef test_conversation_create_returns_existing_within_timeout(mock_conn):\n    \"\"\"Test returning existing conversation when within timeout period.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=15)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [15.0]  # 15 minutes elapsed\n\n    mock_conn.execute.side_effect = [\n        mock_existing,  # Existing conversation found\n        mock_timeout_check,  # Time check: 15 min < 30 min timeout\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101  # Returns existing conversation id\n    assert mock_conn.execute.call_count == 2  # Check existing, check timeout\n    assert mock_conn.commit.call_count == 0  # No insert, no commit\n\n\ndef test_conversation_create_new_when_expired(mock_conn, mock_single_result):\n    \"\"\"Test creating new conversation when existing one is expired.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=45)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [45.0]  # 45 minutes elapsed\n\n    mock_conn.execute.side_effect = [\n        mock_existing,  # Existing conversation found\n        mock_timeout_check,  # Time check: 45 min > 30 min timeout\n        None,  # INSERT (no return value needed)\n        mock_single_result({\"id\": 202}),  # SELECT returns new conversation id\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 202  # Returns new conversation id\n    assert (\n        mock_conn.execute.call_count == 4\n    )  # Check existing, check timeout, INSERT, SELECT\n    assert mock_conn.commit.call_count == 1  # Committed new conversation\n\n\ndef test_conversation_message_create(mock_conn):\n    \"\"\"Test creating a conversation message.\"\"\"\n    message = ConversationMessage(mock_conn)\n    message.create(\n        conversation_id=101, role=\"user\", type=\"text\", content=\"Hello, world!\"\n    )\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_conversation_message\" in insert_call[0][0]\n\n    # Verify parameters\n    uuid_arg, conv_id, role, type_, content = insert_call[0][1]\n    UUID(uuid_arg)  # Validate UUID\n    assert conv_id == 101\n    assert role == \"user\"\n    assert type_ == \"text\"\n    assert content == \"Hello, world!\"\n\n\ndef test_conversation_messages_read(mock_conn, mock_multiple_results):\n    \"\"\"Test reading conversation messages.\"\"\"\n    mock_conn.execute.return_value = mock_multiple_results(\n        [\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n        ]\n    )\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=101)\n\n    assert len(result) == 2\n    assert result[0] == {\"content\": \"Hello\", \"role\": \"user\"}\n    assert result[1] == {\"content\": \"Hi there!\", \"role\": \"assistant\"}\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT role\" in select_call[0][0]\n    assert \"FROM memori_conversation_message\" in select_call[0][0]\n    assert select_call[0][1] == (101,)\n\n\ndef test_conversation_messages_read_empty(mock_conn, mock_empty_result):\n    \"\"\"Test reading messages when none exist.\"\"\"\n    mock_conn.execute.return_value = mock_empty_result\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=999)\n\n    assert result == []\n\n\ndef test_schema_version_create(mock_conn):\n    \"\"\"Test creating a schema version record.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.create(num=1)\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"INSERT INTO memori_schema_version\" in insert_call[0][0]\n    assert insert_call[0][1] == (1,)\n\n\ndef test_schema_version_read(mock_conn, mock_single_result):\n    \"\"\"Test reading the current schema version.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"num\": 5})\n\n    schema_version = SchemaVersion(mock_conn)\n    result = schema_version.read()\n\n    assert result == 5\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"SELECT num\" in select_call[0][0]\n    assert \"FROM memori_schema_version\" in select_call[0][0]\n\n\ndef test_schema_version_delete(mock_conn):\n    \"\"\"Test deleting schema version records.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.delete()\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify DELETE query\n    delete_call = mock_conn.execute.call_args_list[0]\n    assert \"DELETE FROM memori_schema_version\" in delete_call[0][0]\n\n\ndef test_schema_initialization(mock_conn):\n    \"\"\"Test that Schema initializes SchemaVersion correctly.\"\"\"\n    schema = Schema(mock_conn)\n\n    assert isinstance(schema.version, SchemaVersion)\n    assert schema.conn == mock_conn\n"
  },
  {
    "path": "tests/storage/drivers/test_sqlite_driver.py",
    "content": "from unittest.mock import MagicMock\nfrom uuid import UUID\n\nfrom memori.storage.drivers.sqlite._driver import (\n    Conversation,\n    ConversationMessage,\n    ConversationMessages,\n    Driver,\n    Entity,\n    EntityFact,\n    Process,\n    Schema,\n    SchemaVersion,\n    Session,\n)\n\n\ndef test_driver_initialization(mock_conn):\n    \"\"\"Test that Driver initializes all components correctly.\"\"\"\n    driver = Driver(mock_conn)\n\n    assert isinstance(driver.conversation, Conversation)\n    assert isinstance(driver.entity, Entity)\n    assert isinstance(driver.entity_fact, EntityFact)\n    assert isinstance(driver.process, Process)\n    assert isinstance(driver.schema, Schema)\n    assert isinstance(driver.session, Session)\n\n\ndef test_entity_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a entity record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    entity = Entity(mock_conn)\n    result = entity.create(\"external-entity-id\")\n\n    assert result == 123\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"insert or ignore into memori_entity\" in insert_call[0][0].lower()\n    assert insert_call[0][1][1] == \"external-entity-id\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"select id\" in select_call[0][0].lower()\n    assert \"from memori_entity\" in select_call[0][0].lower()\n    assert select_call[0][1] == (\"external-entity-id\",)\n\n\ndef test_entity_generates_uuid(mock_conn, mock_single_result):\n    \"\"\"Test that create generates a valid UUID string.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 123})\n\n    entity = Entity(mock_conn)\n    entity.create(\"external-entity-id\")\n\n    # Check that a UUID was generated in the INSERT\n    insert_call = mock_conn.execute.call_args_list[0]\n    uuid_arg = insert_call[0][1][0]\n\n    # SQLite driver uses str(uuid4()), so verify it's a string\n    assert isinstance(uuid_arg, str)\n    # Verify it can be parsed as a UUID\n    UUID(uuid_arg)\n\n\ndef test_process_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a process record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 456})\n\n    process = Process(mock_conn)\n    result = process.create(\"external-process-id\")\n\n    assert result == 456\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"insert or ignore into memori_process\" in insert_call[0][0].lower()\n    assert insert_call[0][1][1] == \"external-process-id\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"select id\" in select_call[0][0].lower()\n    assert \"from memori_process\" in select_call[0][0].lower()\n    assert select_call[0][1] == (\"external-process-id\",)\n\n\ndef test_session_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a session record.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"id\": 789})\n\n    session = Session(mock_conn)\n    session_uuid = \"test-session-uuid\"\n    result = session.create(session_uuid, entity_id=123, process_id=456)\n\n    assert result == 789\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"insert or ignore into memori_session\" in insert_call[0][0].lower()\n    assert insert_call[0][1] == (session_uuid, 123, 456)\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[1]\n    assert \"select id\" in select_call[0][0].lower()\n    assert \"from memori_session\" in select_call[0][0].lower()\n    assert select_call[0][1] == (session_uuid,)\n\n\ndef test_conversation_initialization(mock_conn):\n    \"\"\"Test that Conversation initializes its sub-components.\"\"\"\n    conversation = Conversation(mock_conn)\n\n    assert isinstance(conversation.message, ConversationMessage)\n    assert isinstance(conversation.messages, ConversationMessages)\n    assert conversation.conn == mock_conn\n\n\ndef test_conversation_create(mock_conn, mock_single_result):\n    \"\"\"Test creating a conversation record when none exists.\"\"\"\n    mock_empty_result = MagicMock()\n    mock_empty_result.mappings.return_value.fetchone.return_value = None\n    mock_conn.execute.side_effect = [\n        mock_empty_result,\n        None,\n        mock_single_result({\"id\": 101}),\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101\n    assert mock_conn.execute.call_count == 3  # Check existing, INSERT, SELECT\n    assert mock_conn.commit.call_count == 1\n\n    # Verify check for existing conversation\n    check_call = mock_conn.execute.call_args_list[0]\n    assert (\n        \"coalesce(max(m.date_created), c.date_created) as last_activity\"\n        in check_call[0][0].lower()\n    )\n    assert check_call[0][1] == (789,)\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[1]\n    assert \"insert or ignore into memori_conversation\" in insert_call[0][0].lower()\n\n    # Verify the UUID is generated and session_id is passed\n    uuid_arg, session_id_arg = insert_call[0][1]\n    UUID(uuid_arg)  # Verify it's a valid UUID string\n    assert session_id_arg == 789\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[2]\n    assert \"select id\" in select_call[0][0].lower()\n    assert \"from memori_conversation\" in select_call[0][0].lower()\n    assert select_call[0][1] == (789,)\n\n\ndef test_conversation_create_returns_existing_within_timeout(mock_conn):\n    \"\"\"Test returning existing conversation when within timeout period.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=15)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [15.0]  # 15 minutes elapsed\n\n    mock_conn.execute.side_effect = [\n        mock_existing,  # Existing conversation found\n        mock_timeout_check,  # Time check: 15 min < 30 min timeout\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 101  # Returns existing conversation id\n    assert mock_conn.execute.call_count == 2  # Check existing, check timeout\n    assert mock_conn.commit.call_count == 0  # No insert, no commit\n\n\ndef test_conversation_create_new_when_expired(mock_conn, mock_single_result):\n    \"\"\"Test creating new conversation when existing one is expired.\"\"\"\n    from datetime import datetime, timedelta\n\n    last_activity = datetime.now() - timedelta(minutes=45)\n\n    mock_existing = MagicMock()\n    mock_existing.mappings.return_value.fetchone.return_value = {\n        \"id\": 101,\n        \"last_activity\": last_activity,\n    }\n\n    mock_timeout_check = MagicMock()\n    mock_timeout_check.fetchone.return_value = [45.0]  # 45 minutes elapsed\n\n    mock_conn.execute.side_effect = [\n        mock_existing,  # Existing conversation found\n        mock_timeout_check,  # Time check: 45 min > 30 min timeout\n        None,  # INSERT (no return value needed)\n        mock_single_result({\"id\": 202}),  # SELECT returns new conversation id\n    ]\n\n    conversation = Conversation(mock_conn)\n    result = conversation.create(session_id=789, timeout_minutes=30)\n\n    assert result == 202  # Returns new conversation id\n    assert (\n        mock_conn.execute.call_count == 4\n    )  # Check existing, check timeout, INSERT, SELECT\n    assert mock_conn.commit.call_count == 1  # Committed new conversation\n\n\ndef test_conversation_message_create(mock_conn):\n    \"\"\"Test creating a conversation message.\"\"\"\n    message = ConversationMessage(mock_conn)\n    message.create(\n        conversation_id=101, role=\"user\", type=\"text\", content=\"Hello, world!\"\n    )\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"insert into memori_conversation_message\" in insert_call[0][0].lower()\n\n    # Verify parameters\n    uuid_arg, conv_id, role, type_, content = insert_call[0][1]\n    UUID(uuid_arg)  # Verify it's a valid UUID string\n    assert conv_id == 101\n    assert role == \"user\"\n    assert type_ == \"text\"\n    assert content == \"Hello, world!\"\n\n\ndef test_conversation_messages_read(mock_conn, mock_multiple_results):\n    \"\"\"Test reading conversation messages.\"\"\"\n    mock_conn.execute.return_value = mock_multiple_results(\n        [\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n        ]\n    )\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=101)\n\n    assert len(result) == 2\n    assert result[0] == {\"content\": \"Hello\", \"role\": \"user\"}\n    assert result[1] == {\"content\": \"Hi there!\", \"role\": \"assistant\"}\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"select role\" in select_call[0][0].lower()\n    assert \"from memori_conversation_message\" in select_call[0][0].lower()\n    assert \"order by id\" in select_call[0][0].lower()\n    assert select_call[0][1] == (101,)\n\n\ndef test_conversation_messages_read_empty(mock_conn, mock_empty_result):\n    \"\"\"Test reading messages when none exist.\"\"\"\n    mock_conn.execute.return_value = mock_empty_result\n\n    messages = ConversationMessages(mock_conn)\n    result = messages.read(conversation_id=999)\n\n    assert result == []\n\n\ndef test_schema_version_create(mock_conn):\n    \"\"\"Test creating a schema version record.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.create(num=1)\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify INSERT query\n    insert_call = mock_conn.execute.call_args_list[0]\n    assert \"insert into memori_schema_version\" in insert_call[0][0].lower()\n    assert insert_call[0][1] == (1,)\n\n\ndef test_schema_version_read(mock_conn, mock_single_result):\n    \"\"\"Test reading the current schema version.\"\"\"\n    mock_conn.execute.return_value = mock_single_result({\"num\": 5})\n\n    schema_version = SchemaVersion(mock_conn)\n    result = schema_version.read()\n\n    assert result == 5\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"select num\" in select_call[0][0].lower()\n    assert \"from memori_schema_version\" in select_call[0][0].lower()\n\n\ndef test_schema_version_delete(mock_conn):\n    \"\"\"Test deleting schema version records.\"\"\"\n    schema_version = SchemaVersion(mock_conn)\n    schema_version.delete()\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify DELETE query\n    delete_call = mock_conn.execute.call_args_list[0]\n    assert \"delete from memori_schema_version\" in delete_call[0][0].lower()\n\n\ndef test_schema_initialization(mock_conn):\n    \"\"\"Test that Schema initializes SchemaVersion correctly.\"\"\"\n    schema = Schema(mock_conn)\n\n    assert isinstance(schema.version, SchemaVersion)\n    assert schema.conn == mock_conn\n\n\ndef test_entity_fact_create(mock_conn, mocker):\n    \"\"\"Test creating entity facts.\"\"\"\n    mocker.patch(\"memori._utils.generate_uniq\", return_value=\"uniq123\")\n    mocker.patch(\n        \"memori.embeddings.format_embedding_for_db\",\n        return_value=b\"\\x00\\x01\\x02\\x03\",  # Binary data\n    )\n\n    entity_fact = EntityFact(mock_conn)\n    facts = [\"User likes Python\", \"User works as engineer\"]\n    embeddings = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]\n\n    result = entity_fact.create(entity_id=123, facts=facts, fact_embeddings=embeddings)\n\n    assert result == entity_fact\n    assert mock_conn.execute.call_count == 2\n    assert mock_conn.commit.call_count == 1\n\n    # Verify first INSERT query\n    first_insert = mock_conn.execute.call_args_list[0]\n    assert \"insert into memori_entity_fact\" in first_insert[0][0].lower()\n    assert \"on conflict(entity_id, uniq)\" in first_insert[0][0].lower()\n\n    # Verify parameters for first fact\n    params = first_insert[0][1]\n    assert params[1] == 123  # entity_id\n    assert params[2] == \"User likes Python\"  # content\n    assert params[3] == b\"\\x00\\x01\\x02\\x03\"  # content_embedding (binary)\n    assert params[4] == 1  # num_times\n    assert params[5] == \"uniq123\"  # uniq\n\n\ndef test_entity_fact_create_empty_facts(mock_conn):\n    \"\"\"Test creating entity facts with empty list.\"\"\"\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.create(entity_id=123, facts=[], fact_embeddings=None)\n\n    assert result == entity_fact\n    assert mock_conn.execute.call_count == 0\n\n\ndef test_entity_fact_create_without_embeddings(mock_conn, mocker):\n    \"\"\"Test creating entity facts without embeddings.\"\"\"\n    mocker.patch(\"memori._utils.generate_uniq\", return_value=\"uniq123\")\n    mocker.patch(\n        \"memori.embeddings.format_embedding_for_db\",\n        return_value=b\"\",  # Empty binary data\n    )\n\n    entity_fact = EntityFact(mock_conn)\n    facts = [\"User likes Python\"]\n\n    entity_fact.create(entity_id=123, facts=facts, fact_embeddings=None)\n\n    assert mock_conn.execute.call_count == 1\n\n    # Verify embedding was formatted as empty binary\n    insert_call = mock_conn.execute.call_args_list[0]\n    params = insert_call[0][1]\n    assert params[3] == b\"\"  # content_embedding (empty binary)\n\n\ndef test_entity_fact_get_embeddings(mock_conn, mock_multiple_results):\n    \"\"\"Test retrieving embeddings for an entity.\"\"\"\n    mock_conn.execute.return_value = mock_multiple_results(\n        [\n            {\"id\": 1, \"content_embedding\": b\"\\x00\\x01\\x02\\x03\"},\n            {\"id\": 2, \"content_embedding\": b\"\\x04\\x05\\x06\\x07\"},\n        ]\n    )\n\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.get_embeddings(entity_id=123, limit=100)\n\n    assert len(result) == 2\n    assert result[0][\"id\"] == 1\n    assert result[0][\"content_embedding\"] == b\"\\x00\\x01\\x02\\x03\"\n    assert result[1][\"id\"] == 2\n    assert result[1][\"content_embedding\"] == b\"\\x04\\x05\\x06\\x07\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"select id\" in select_call[0][0].lower()\n    assert \"content_embedding\" in select_call[0][0].lower()\n    assert \"from memori_entity_fact\" in select_call[0][0].lower()\n    assert \"where entity_id = ?\" in select_call[0][0].lower()\n    assert \"order by\" in select_call[0][0].lower()\n    assert \"limit ?\" in select_call[0][0].lower()\n    assert select_call[0][1] == (123, 100)\n\n\ndef test_entity_fact_get_embeddings_default_limit(mock_conn, mock_empty_result):\n    \"\"\"Test retrieving embeddings with default limit.\"\"\"\n    mock_conn.execute.return_value = mock_empty_result\n\n    entity_fact = EntityFact(mock_conn)\n    entity_fact.get_embeddings(entity_id=123)\n\n    # Verify default limit of 1000\n    select_call = mock_conn.execute.call_args_list[0]\n    assert select_call[0][1] == (123, 1000)\n\n\ndef test_entity_fact_get_facts_by_ids(mock_conn, mock_multiple_results):\n    \"\"\"Test retrieving fact content by IDs.\"\"\"\n    mock_conn.execute.return_value = mock_multiple_results(\n        [\n            {\n                \"id\": 1,\n                \"content\": \"User likes Python\",\n                \"date_created\": \"2026-01-01 10:30:00\",\n            },\n            {\n                \"id\": 2,\n                \"content\": \"User works as engineer\",\n                \"date_created\": \"2026-01-02 11:15:00\",\n            },\n        ]\n    )\n\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.get_facts_by_ids([1, 2])\n\n    assert len(result) == 2\n    assert result[0][\"id\"] == 1\n    assert result[0][\"content\"] == \"User likes Python\"\n    assert result[0][\"date_created\"] == \"2026-01-01 10:30:00\"\n    assert result[1][\"id\"] == 2\n    assert result[1][\"content\"] == \"User works as engineer\"\n    assert result[1][\"date_created\"] == \"2026-01-02 11:15:00\"\n\n    # Verify SELECT query\n    select_call = mock_conn.execute.call_args_list[0]\n    assert \"select id\" in select_call[0][0].lower()\n    assert \"content\" in select_call[0][0].lower()\n    assert \"date_created\" in select_call[0][0].lower()\n    assert \"from memori_entity_fact\" in select_call[0][0].lower()\n    assert \"where id in (?,?)\" in select_call[0][0].lower()\n    assert select_call[0][1] == (1, 2)\n\n\ndef test_entity_fact_get_facts_by_ids_empty(mock_conn):\n    \"\"\"Test retrieving facts with empty IDs list.\"\"\"\n    entity_fact = EntityFact(mock_conn)\n    result = entity_fact.get_facts_by_ids([])\n\n    assert result == []\n    assert mock_conn.execute.call_count == 0\n"
  },
  {
    "path": "tests/storage/test_connection.py",
    "content": "from unittest.mock import Mock, patch\n\nimport pytest\n\nfrom memori.storage._connection import connection_context\n\n\ndef test_connection_context_with_none_factory():\n    with connection_context(None) as (conn, adapter, driver):\n        assert conn is None\n        assert adapter is None\n        assert driver is None\n\n\ndef test_connection_context_success():\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_driver = Mock()\n    conn_factory = Mock(return_value=mock_conn)\n\n    with patch(\"memori.storage._connection.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        with connection_context(conn_factory) as (conn, adapter, driver):\n            assert conn == mock_conn\n            assert adapter == mock_adapter\n            assert driver == mock_driver\n\n        conn_factory.assert_called_once()\n\n        # Verify adapter was called with a callable\n        assert mock_registry.adapter.call_count == 1\n        adapter_arg = mock_registry.adapter.call_args[0][0]\n        assert callable(adapter_arg)\n        # Verify the callable returns the same connection instance\n        assert adapter_arg() == mock_conn\n\n        mock_registry.driver.assert_called_once_with(mock_adapter)\n        mock_adapter.commit.assert_called_once()\n        mock_adapter.close.assert_called_once()\n\n\ndef test_connection_context_exception_in_body():\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_driver = Mock()\n    conn_factory = Mock(return_value=mock_conn)\n\n    with patch(\"memori.storage._connection.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        with pytest.raises(ValueError):\n            with connection_context(conn_factory) as (conn, adapter, driver):\n                raise ValueError(\"Test error\")\n\n        mock_adapter.commit.assert_not_called()\n        mock_adapter.rollback.assert_called_once()\n        mock_adapter.close.assert_called_once()\n\n\ndef test_connection_context_close_exception_suppressed():\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_adapter.close.side_effect = Exception(\"Close failed\")\n    mock_driver = Mock()\n    conn_factory = Mock(return_value=mock_conn)\n\n    with patch(\"memori.storage._connection.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        with connection_context(conn_factory) as (conn, adapter, driver):\n            pass\n\n        mock_adapter.commit.assert_called_once()\n        mock_adapter.close.assert_called_once()\n\n\ndef test_connection_context_commit_exception_propagates():\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_adapter.commit.side_effect = Exception(\"Commit failed\")\n    mock_driver = Mock()\n    conn_factory = Mock(return_value=mock_conn)\n\n    with patch(\"memori.storage._connection.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        with pytest.raises(Exception, match=\"Commit failed\"):\n            with connection_context(conn_factory):\n                pass\n\n        mock_adapter.rollback.assert_called_once()\n        mock_adapter.close.assert_called_once()\n"
  },
  {
    "path": "tests/storage/test_connection_factory.py",
    "content": "import pytest\n\nfrom memori.storage.adapters.dbapi._adapter import Adapter as DBAPIAdapter\n\n\ndef test_connection_factory_is_called_eagerly(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg2\"\n    type(mock_conn).__module__ = \"psycopg2\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    call_count = [0]\n\n    def factory():\n        call_count[0] += 1\n        return mock_conn\n\n    adapter = DBAPIAdapter(factory)\n\n    assert call_count[0] == 1\n\n    adapter.commit()\n\n    assert call_count[0] == 1\n    mock_conn.commit.assert_called_once()\n\n\ndef test_connection_factory_cached_after_first_call(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg2\"\n    type(mock_conn).__module__ = \"psycopg2\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_cursor.execute = mocker.MagicMock()\n    mock_cursor.close = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n\n    call_count = [0]\n\n    def factory():\n        call_count[0] += 1\n        return mock_conn\n\n    adapter = DBAPIAdapter(factory)\n\n    adapter.commit()\n    adapter.commit()\n\n    assert call_count[0] == 1\n    assert mock_conn.commit.call_count == 2\n\n\ndef test_non_callable_raises_error():\n    # Test that passing a non-callable raises an error\n    with pytest.raises(TypeError, match=\"conn must be a callable\"):\n        DBAPIAdapter(\"not callable\")\n\n    with pytest.raises(TypeError, match=\"conn must be a callable\"):\n        DBAPIAdapter(123)\n\n    with pytest.raises(TypeError, match=\"conn must be a callable\"):\n        DBAPIAdapter({\"not\": \"callable\"})\n\n\ndef test_connection_lifecycle_close(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\", \"close\"])\n    mock_conn.__module__ = \"psycopg2\"\n    type(mock_conn).__module__ = \"psycopg2\"\n\n    mock_cursor = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n    mock_conn.commit = mocker.MagicMock()\n    mock_conn.rollback = mocker.MagicMock()\n    mock_conn.close = mocker.MagicMock()\n\n    def factory():\n        return mock_conn\n\n    adapter = DBAPIAdapter(factory)\n\n    # Connection is created eagerly\n    mock_conn.commit.assert_not_called()\n\n    adapter.commit()\n    mock_conn.commit.assert_called_once()\n\n    # Close should call close on the connection and set to None\n    adapter.close()\n    mock_conn.close.assert_called_once()\n    assert adapter.conn is None\n\n\ndef test_connection_factory_supports_release_tuple(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\", \"close\"])\n    mock_conn.__module__ = \"psycopg2\"\n    type(mock_conn).__module__ = \"psycopg2\"\n\n    release = mocker.Mock()\n\n    def factory():\n        return mock_conn, release\n\n    adapter = DBAPIAdapter(factory)\n\n    adapter.close()\n    release.assert_called_once()\n    mock_conn.close.assert_not_called()\n    assert adapter.conn is None\n\n\ndef test_connection_factory_supports_context_manager(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\", \"close\"])\n    mock_conn.__module__ = \"psycopg2\"\n    type(mock_conn).__module__ = \"psycopg2\"\n\n    release = mocker.Mock()\n\n    class PoolConn:\n        def __enter__(self):\n            return mock_conn\n\n        def __exit__(self, exc_type, exc, tb):\n            release()\n\n    def factory():\n        return PoolConn()\n\n    adapter = DBAPIAdapter(factory)\n    adapter.close()\n\n    release.assert_called_once()\n    mock_conn.close.assert_not_called()\n    assert adapter.conn is None\n"
  },
  {
    "path": "tests/storage/test_storage_builder.py",
    "content": "from unittest.mock import MagicMock\n\nimport pytest\n\nfrom memori._config import Config\nfrom memori.storage import Manager as StorageManager\nfrom memori.storage._builder import Builder\nfrom memori.storage.drivers.mysql._driver import Driver as MysqlDriver\nfrom memori.storage.drivers.oceanbase._driver import Driver as OceanbaseDriver\nfrom memori.storage.drivers.postgresql._driver import Driver as PostgresqlDriver\n\n\n@pytest.fixture\ndef mock_config():\n    \"\"\"Create a mock Config object with storage.\"\"\"\n    config = Config()\n    config.storage = StorageManager(config)\n    config.storage.adapter = MagicMock()\n    config.storage.driver = MagicMock()\n    return config\n\n\n@pytest.fixture\ndef builder(mock_config):\n    \"\"\"Create a Builder instance with mocked config.\"\"\"\n    return Builder(mock_config)\n\n\ndef test_get_supported_dialects(builder):\n    \"\"\"Test that _get_supported_dialects returns registered dialects.\"\"\"\n    supported = builder._get_supported_dialects()\n\n    # Should contain at least mysql and postgresql\n    assert \"mysql\" in supported\n    assert \"oceanbase\" in supported\n    assert \"postgresql\" in supported\n    assert isinstance(supported, list)\n\n\ndef test_get_dialect_family_exact_match(builder):\n    \"\"\"Test dialect family detection with exact matches.\"\"\"\n    # Test exact matches\n    assert builder._get_dialect_family(\"mysql\") == MysqlDriver.migrations\n    assert builder._get_dialect_family(\"postgresql\") == PostgresqlDriver.migrations\n    assert builder._get_dialect_family(\"cockroachdb\") == PostgresqlDriver.migrations\n\n\ndef test_get_dialect_family_no_match(builder):\n    \"\"\"Test dialect family detection returns None for unknown dialects.\"\"\"\n    assert builder._get_dialect_family(\"invalid\") is None\n    assert builder._get_dialect_family(\"redis\") is None\n    assert builder._get_dialect_family(\"unknown\") is None\n\n\ndef test_requires_rollback_true(builder):\n    \"\"\"Test rollback requirement for dialects that need it.\"\"\"\n    assert builder._requires_rollback(\"postgresql\") is True\n    assert builder._requires_rollback(\"cockroachdb\") is True\n\n\ndef test_get_dialect_family_oceanbase(builder):\n    \"\"\"Test dialect family detection for OceanBase.\"\"\"\n    assert builder._get_dialect_family(\"oceanbase\") == OceanbaseDriver.migrations\n\n\ndef test_requires_rollback_oceanbase(builder):\n    \"\"\"Test rollback requirement for OceanBase.\"\"\"\n    assert builder._requires_rollback(\"oceanbase\") is True\n\n\ndef test_requires_rollback_false(builder):\n    \"\"\"Test rollback requirement for dialects that don't need it.\"\"\"\n    assert builder._requires_rollback(\"mysql\") is False\n\n\ndef test_requires_rollback_unknown_dialect(builder):\n    \"\"\"Test rollback returns False for unknown dialects.\"\"\"\n    assert builder._requires_rollback(\"unknown\") is False\n\n\ndef test_build_unsupported_dialect(mock_config):\n    \"\"\"Test that build raises NotImplementedError for unsupported dialects.\"\"\"\n    mock_config.storage.adapter.get_dialect.return_value = \"invalid\"\n    builder = Builder(mock_config)\n\n    with pytest.raises(NotImplementedError) as exc_info:\n        builder.execute()\n\n    assert \"Unsupported dialect: invalid\" in str(exc_info.value)\n    assert \"Supported dialects:\" in str(exc_info.value)\n\n\ndef test_build_supported_dialect(mock_config):\n    \"\"\"Test that build works with supported dialects.\"\"\"\n    mock_config.storage.adapter.get_dialect.return_value = \"mysql\"\n\n    # Mock the cli to avoid banner output\n    builder = Builder(mock_config)\n    builder.cli = MagicMock()\n\n    # Mock schema version read\n    mock_config.storage.driver.schema.version.read.return_value = len(\n        MysqlDriver.migrations\n    )\n\n    result = builder.execute()\n\n    assert result == builder\n    assert mock_config.storage.driver.schema.version.read.called\n\n\ndef test_create_data_structures_postgresql_rollback(mock_config):\n    \"\"\"Test that create_data_structures triggers rollback for PostgreSQL on error.\"\"\"\n    mock_config.storage.adapter.get_dialect.return_value = \"postgresql\"\n\n    builder = Builder(mock_config)\n    builder.cli = MagicMock()\n\n    # Simulate schema version read failure\n    mock_config.storage.driver.schema.version.read.side_effect = Exception(\n        \"Schema error\"\n    )\n    mock_config.storage.driver.schema.version.read.return_value = len(\n        PostgresqlDriver.migrations\n    )\n\n    builder.create_data_structures()\n\n    # Verify rollback was called for postgresql\n    assert mock_config.storage.adapter.rollback.called\n\n\ndef test_create_data_structures_cockroachdb_rollback(mock_config):\n    \"\"\"Test that create_data_structures triggers rollback for CockroachDB on error.\"\"\"\n    mock_config.storage.adapter.get_dialect.return_value = \"cockroachdb\"\n\n    builder = Builder(mock_config)\n    builder.cli = MagicMock()\n\n    # Simulate schema version read failure\n    mock_config.storage.driver.schema.version.read.side_effect = Exception(\n        \"Schema error\"\n    )\n\n    builder.create_data_structures()\n\n    # Verify rollback was called for cockroachdb\n    assert mock_config.storage.adapter.rollback.called\n\n\ndef test_create_data_structures_mysql_no_rollback(mock_config):\n    \"\"\"Test that create_data_structures does not trigger rollback for MySQL on error.\"\"\"\n    mock_config.storage.adapter.get_dialect.return_value = \"mysql\"\n\n    builder = Builder(mock_config)\n    builder.cli = MagicMock()\n\n    # Simulate schema version read failure\n    mock_config.storage.driver.schema.version.read.side_effect = Exception(\n        \"Schema error\"\n    )\n\n    builder.create_data_structures()\n\n    # Verify rollback was NOT called for mysql\n    assert not mock_config.storage.adapter.rollback.called\n\n\ndef test_create_data_structures_no_migration_mapping(mock_config):\n    \"\"\"Test that create_data_structures raises error for unmapped dialect.\"\"\"\n    mock_config.storage.adapter.get_dialect.return_value = \"unknown_dialect\"\n\n    builder = Builder(mock_config)\n    builder.cli = MagicMock()\n\n    # Mock to bypass the initial schema version check\n    mock_config.storage.driver.schema.version.read.return_value = 0\n\n    with pytest.raises(NotImplementedError) as exc_info:\n        builder.create_data_structures()\n\n    assert \"No migration mapping found for dialect: unknown_dialect\" in str(\n        exc_info.value\n    )\n"
  },
  {
    "path": "tests/storage/test_storage_init.py",
    "content": "from unittest.mock import MagicMock, patch\n\n\ndef test_import_optional_module_success():\n    \"\"\"Test that _import_optional_module successfully imports existing modules.\"\"\"\n    with patch(\"importlib.import_module\") as mock_import:\n        from memori.storage import _import_optional_module\n\n        mock_module = MagicMock()\n        mock_import.return_value = mock_module\n\n        _import_optional_module(\"sys\")\n\n        mock_import.assert_called_with(\"sys\")\n\n\ndef test_import_optional_module_handles_import_error():\n    \"\"\"Test that _import_optional_module gracefully handles non-existent modules without errors.\"\"\"\n    with patch(\"importlib.import_module\") as mock_import:\n        from memori.storage import _import_optional_module\n\n        mock_import.side_effect = ImportError(\"Module not found\")\n\n        _import_optional_module(\"non.existent.module\")\n\n        mock_import.assert_called_with(\"non.existent.module\")\n\n\ndef test_storage_module_initializes_with_manager_available():\n    \"\"\"Test that storage module initializes correctly when all expected adapters and drivers are present.\"\"\"\n    import memori.storage\n\n    assert hasattr(memori.storage, \"Manager\")\n    assert \"Manager\" in memori.storage.__all__\n\n\ndef test_storage_module_has_import_optional_module_function():\n    \"\"\"Test that storage module has the _import_optional_module function.\"\"\"\n    import memori.storage\n\n    assert hasattr(memori.storage, \"_import_optional_module\")\n    assert callable(memori.storage._import_optional_module)\n"
  },
  {
    "path": "tests/storage/test_storage_manager.py",
    "content": "from unittest.mock import Mock, patch\n\nfrom memori._config import Config\nfrom memori.storage._manager import Manager\n\n\ndef test_manager_start_sets_cockroachdb_flag_for_cockroachdb():\n    config = Config()\n    manager = Manager(config)\n\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_adapter.get_dialect.return_value = \"cockroachdb\"\n    mock_driver = Mock()\n\n    with patch(\"memori.storage._manager.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        manager.start(mock_conn)\n\n    assert config.storage_config.cockroachdb is True\n\n\ndef test_manager_start_sets_cockroachdb_flag_for_postgresql():\n    config = Config()\n    manager = Manager(config)\n\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_adapter.get_dialect.return_value = \"postgresql\"\n    mock_driver = Mock()\n\n    with patch(\"memori.storage._manager.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        manager.start(mock_conn)\n\n    assert config.storage_config.cockroachdb is False\n\n\ndef test_manager_start_sets_cockroachdb_flag_for_mysql():\n    config = Config()\n    manager = Manager(config)\n\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_adapter.get_dialect.return_value = \"mysql\"\n    mock_driver = Mock()\n\n    with patch(\"memori.storage._manager.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        manager.start(mock_conn)\n\n    assert config.storage_config.cockroachdb is False\n\n\ndef test_manager_start_sets_cockroachdb_flag_for_oceanbase():\n    config = Config()\n    manager = Manager(config)\n\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_adapter.get_dialect.return_value = \"oceanbase\"\n    mock_driver = Mock()\n\n    with patch(\"memori.storage._manager.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        manager.start(mock_conn)\n\n    assert config.storage_config.cockroachdb is False\n\n\ndef test_manager_start_sets_cockroachdb_flag_for_mongodb():\n    config = Config()\n    manager = Manager(config)\n\n    mock_conn = Mock()\n    mock_adapter = Mock()\n    mock_adapter.get_dialect.return_value = \"mongodb\"\n    mock_driver = Mock()\n\n    with patch(\"memori.storage._manager.Registry\") as mock_registry_class:\n        mock_registry = Mock()\n        mock_registry.adapter.return_value = mock_adapter\n        mock_registry.driver.return_value = mock_driver\n        mock_registry_class.return_value = mock_registry\n\n        manager.start(mock_conn)\n\n    assert config.storage_config.cockroachdb is False\n\n\ndef test_manager_start_with_none_conn_does_not_set_flag():\n    config = Config()\n    manager = Manager(config)\n\n    original_value = config.storage_config.cockroachdb\n    manager.start(None)\n\n    assert config.storage_config.cockroachdb == original_value\n"
  },
  {
    "path": "tests/storage/test_storage_registry.py",
    "content": "import pytest\n\nfrom memori._exceptions import UnsupportedDatabaseError\nfrom memori.storage._registry import Registry\nfrom memori.storage.adapters.sqlalchemy._adapter import (\n    Adapter as SqlAlchemyStorageAdapter,\n)\nfrom memori.storage.drivers.mysql._driver import Driver as MysqlStorageDriver\nfrom memori.storage.drivers.oceanbase._driver import Driver as OceanbaseStorageDriver\nfrom memori.storage.drivers.postgresql._driver import Driver as PostgresqlStorageDriver\n\n\ndef test_storage_adapter_sqlalchemy(session):\n    assert isinstance(Registry().adapter(lambda: session), SqlAlchemyStorageAdapter)\n\n\ndef test_storage_driver_mysql(session):\n    assert isinstance(\n        Registry().driver(Registry().adapter(lambda: session)), MysqlStorageDriver\n    )\n\n\ndef test_storage_driver_postgresql(postgres_session):\n    assert isinstance(\n        Registry().driver(Registry().adapter(lambda: postgres_session)),\n        PostgresqlStorageDriver,\n    )\n\n\ndef test_storage_driver_mariadb(mocker):\n    mariadb_session = mocker.Mock()\n    mariadb_session.get_bind.return_value.dialect.name = \"mariadb\"\n    type(mariadb_session).__module__ = \"sqlalchemy.orm.session\"\n\n    adapter = Registry().adapter(lambda: mariadb_session)\n    driver = Registry().driver(adapter)\n\n    assert isinstance(driver, MysqlStorageDriver)\n\n\ndef test_storage_driver_cockroachdb(mocker):\n    cockroachdb_session = mocker.Mock()\n    cockroachdb_session.get_bind.return_value.dialect.name = \"cockroachdb\"\n    type(cockroachdb_session).__module__ = \"sqlalchemy.orm.session\"\n\n    adapter = Registry().adapter(lambda: cockroachdb_session)\n    driver = Registry().driver(adapter)\n\n    assert isinstance(driver, PostgresqlStorageDriver)\n\n\ndef test_storage_driver_oceanbase(mocker):\n    oceanbase_adapter = mocker.Mock()\n    oceanbase_adapter.get_dialect.return_value = \"oceanbase\"\n\n    driver = Registry().driver(oceanbase_adapter)\n\n    assert isinstance(driver, OceanbaseStorageDriver)\n\n\ndef test_storage_adapter_raises_for_unsupported_connection():\n    \"\"\"Test that unsupported database connection raises UnsupportedDatabaseError.\"\"\"\n\n    class UnsupportedConnection:\n        pass\n\n    with pytest.raises(UnsupportedDatabaseError, match=r\"Unsupported database\"):\n        Registry().adapter(UnsupportedConnection())\n\n\ndef test_storage_driver_raises_for_unsupported_dialect(mocker):\n    \"\"\"Test that unsupported database dialect raises RuntimeError.\"\"\"\n\n    fake_adapter = mocker.Mock()\n    fake_adapter.get_dialect.return_value = \"unsupported_db\"\n\n    with pytest.raises(RuntimeError, match=\"Unsupported database dialect\"):\n        Registry().driver(fake_adapter)\n"
  },
  {
    "path": "tests/test_cli.py",
    "content": "import subprocess\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom memori.__main__ import main\nfrom memori._cli import Cli\nfrom memori._config import Config\n\n\n@pytest.fixture\ndef mock_config():\n    config = Config()\n    config.version = \"3.1.2\"\n    return config\n\n\ndef test_cli_banner_contains_key_elements(capsys, mock_config):\n    cli = Cli(config=mock_config)\n    cli.banner()\n    captured = capsys.readouterr()\n    assert \"Memori\" in captured.out or \"memori\" in captured.out.lower()\n    assert mock_config.version in captured.out\n    assert \"memorilabs.ai\" in captured.out\n\n\n@pytest.mark.integration\ndef test_entrypoint_smoke_run():\n    result = subprocess.run(\n        [sys.executable, \"-m\", \"memori\", \"--help\"],\n        capture_output=True,\n        text=True,\n        timeout=30,\n    )\n    assert result.returncode == 0\n    assert \"usage\" in result.stdout.lower()\n\n\nclass TestCliEntrypoint:\n    def run_main_with_args(self, args):\n        with mock.patch.object(sys, \"argv\", [\"memori\"] + args):\n            try:\n                main()\n            except SystemExit as e:\n                return e.code\n            return 0\n\n    def test_cli_signup_missing_email_shows_error(self, capsys):\n        exit_code = self.run_main_with_args([\"sign-up\"])\n        assert exit_code != 0\n        captured = capsys.readouterr()\n        output = captured.out.lower()\n        assert \"usage\" in output or \"email\" in output\n\n    def test_cli_no_args_shows_branding(self, capsys):\n        self.run_main_with_args([])\n        captured = capsys.readouterr()\n        output = captured.out.lower()\n        assert \"memori\" in output or \"memorilabs\" in output\n        assert \"usage\" in output\n\n    @pytest.mark.parametrize(\n        \"args\",\n        [\n            [\"--help\"],\n            [\"-h\"],\n            [\"help\"],\n        ],\n    )\n    def test_help_variations_show_all_commands(self, args, capsys):\n        self.run_main_with_args(args)\n        captured = capsys.readouterr()\n        output = captured.out.lower()\n        assert \"usage\" in output\n        assert \"cockroachdb\" in output\n        assert \"quota\" in output\n        assert \"sign-up\" in output\n        assert \"setup\" in output\n\n    def test_invalid_command_shows_help(self, capsys):\n        self.run_main_with_args([\"invalid-command\"])\n        captured = capsys.readouterr()\n        output = captured.out.lower()\n        assert \"usage\" in output\n        assert \"cockroachdb\" in output\n        assert \"sign-up\" in output\n\n    def test_branding_displayed(self, capsys):\n        self.run_main_with_args([])\n        captured = capsys.readouterr()\n        output = captured.out.lower()\n        assert \"memori\" in output\n        assert \"memorilabs.ai\" in output\n\n    def test_cockroachdb_missing_subcommand_shows_usage(self, capsys):\n        exit_code = self.run_main_with_args([\"cockroachdb\"])\n        assert exit_code != 0\n        captured = capsys.readouterr()\n        assert \"usage\" in captured.out.lower()\n\n    @mock.patch(\"memori.__main__.CockroachDBClusterManager\")\n    def test_cockroachdb_cluster_start_dispatches_correctly(\n        self, mock_manager_cls, capsys\n    ):\n        mock_instance = mock_manager_cls.return_value\n        mock_instance.execute.return_value = None\n        exit_code = self.run_main_with_args([\"cockroachdb\", \"cluster\", \"start\"])\n        assert exit_code in (0, None)\n        mock_manager_cls.assert_called()\n        mock_instance.execute.assert_called()\n        captured = capsys.readouterr()\n        assert \"usage: python -m memori cockroachdb cluster\" not in captured.out\n"
  },
  {
    "path": "tests/test_config.py",
    "content": "import os\n\nfrom memori._config import Config\n\n\ndef test_is_test_mode():\n    config = Config()\n    os.environ.pop(\"MEMORI_TEST_MODE\", None)\n\n    assert config.is_test_mode() is False\n\n    try:\n        os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n        assert config.is_test_mode() is True\n    finally:\n        os.environ.pop(\"MEMORI_TEST_MODE\", None)\n\n\ndef test_reset_cache():\n    config = Config()\n    config.cache.conversation_id = 123\n    config.cache.entity_id = 456\n    config.cache.process_id = 789\n    config.cache.session_id = 987\n\n    config.reset_cache()\n\n    assert config.cache.conversation_id is None\n    assert config.cache.entity_id is None\n    assert config.cache.process_id is None\n    assert config.cache.session_id is None\n\n\ndef test_augmentation_default():\n    config = Config()\n    assert config.augmentation is None\n\n\ndef test_storage_initialization():\n    config = Config()\n    assert config.storage_config is not None\n    assert hasattr(config.storage_config, \"cockroachdb\")\n    assert config.storage_config.cockroachdb is False\n\n\ndef test_recall_env_overrides(monkeypatch):\n    monkeypatch.setenv(\"MEMORI_RECALL_EMBEDDINGS_LIMIT\", \"1234\")\n    monkeypatch.setenv(\"MEMORI_EMBEDDINGS_MODEL\", \"google/embeddinggemma-300m\")\n\n    config = Config()\n    assert config.recall_embeddings_limit == 1234\n    assert config.embeddings.model == \"google/embeddinggemma-300m\"\n"
  },
  {
    "path": "tests/test_init.py",
    "content": "import pytest\n\nfrom memori import Memori\nfrom memori._exceptions import MissingMemoriApiKeyError\n\n\ndef test_cloud_false_when_conn_provided(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg\"\n    type(mock_conn).__module__ = \"psycopg\"\n    mock_cursor = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n\n    mem = Memori(conn=lambda: mock_conn)\n    assert mem.config.cloud is False\n\n\ndef test_cloud_true_when_no_conn(monkeypatch):\n    monkeypatch.delenv(\"MEMORI_COCKROACHDB_CONNECTION_STRING\", raising=False)\n    monkeypatch.setenv(\"MEMORI_API_KEY\", \"test-api-key\")\n    monkeypatch.setenv(\"MEMORI_TEST_MODE\", \"1\")\n    mem = Memori()\n    assert mem.config.cloud is True\n\n\ndef test_cloud_raises_error_when_no_api_key(monkeypatch):\n    monkeypatch.delenv(\"MEMORI_COCKROACHDB_CONNECTION_STRING\", raising=False)\n    monkeypatch.delenv(\"MEMORI_API_KEY\", raising=False)\n    with pytest.raises(MissingMemoriApiKeyError) as e:\n        Memori()\n    assert e.value.env_var == \"MEMORI_API_KEY\"\n    assert \"MEMORI_API_KEY\" in str(e.value)\n\n\ndef test_cloud_false_when_connection_string_set(monkeypatch, mocker):\n    monkeypatch.setenv(\n        \"MEMORI_COCKROACHDB_CONNECTION_STRING\",\n        \"postgresql://user:pass@localhost:26257/defaultdb?sslmode=disable\",\n    )\n\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg\"\n    type(mock_conn).__module__ = \"psycopg\"\n    mock_cursor = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n\n    mocker.patch(\"psycopg.connect\", return_value=mock_conn)\n\n    mem = Memori(conn=None)\n    assert mem.config.cloud is False\n\n\ndef test_attribution_exceptions(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg\"\n    type(mock_conn).__module__ = \"psycopg\"\n    mock_cursor = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n\n    with pytest.raises(RuntimeError) as e:\n        Memori(conn=lambda: mock_conn).attribution(entity_id=\"a\" * 101)\n\n    assert str(e.value) == \"entity_id cannot be greater than 100 characters\"\n\n    with pytest.raises(RuntimeError) as e:\n        Memori(conn=lambda: mock_conn).attribution(process_id=\"a\" * 101)\n\n    assert str(e.value) == \"process_id cannot be greater than 100 characters\"\n\n\ndef test_new_session(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg\"\n    type(mock_conn).__module__ = \"psycopg\"\n    mock_cursor = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n\n    mem = Memori(conn=lambda: mock_conn)\n\n    session_id = mem.config.session_id\n    assert session_id is not None\n\n    mem.new_session()\n\n    assert mem.config.session_id is not None\n    assert mem.config.session_id != session_id\n\n\ndef test_set_session(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg\"\n    type(mock_conn).__module__ = \"psycopg\"\n    mock_cursor = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n\n    mem = Memori(conn=lambda: mock_conn).set_session(\n        \"66cf2a0b-7503-4dcd-b717-b29c826fa1db\"\n    )\n    assert mem.config.session_id == \"66cf2a0b-7503-4dcd-b717-b29c826fa1db\"\n\n\ndef test_set_session_resets_cache(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg\"\n    type(mock_conn).__module__ = \"psycopg\"\n    mock_cursor = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n\n    mem = Memori(conn=lambda: mock_conn)\n    mem.config.cache.conversation_id = 123\n    mem.config.cache.session_id = 456\n\n    mem.new_session()\n\n    assert mem.config.cache.conversation_id is None\n    assert mem.config.cache.session_id is None\n\n\ndef test_embed_texts_uses_config_defaults(mocker):\n    mock_conn = mocker.Mock(spec=[\"cursor\", \"commit\", \"rollback\"])\n    mock_conn.__module__ = \"psycopg\"\n    type(mock_conn).__module__ = \"psycopg\"\n    mock_cursor = mocker.MagicMock()\n    mock_conn.cursor = mocker.MagicMock(return_value=mock_cursor)\n\n    mem = Memori(conn=lambda: mock_conn)\n    mem.config.embeddings.model = \"test-model\"\n\n    mock_embed = mocker.patch(\"memori.embed_texts\", return_value=[[1.0, 2.0, 3.0]])\n\n    out = mem.embed_texts(\"hello\")\n\n    assert out == [[1.0, 2.0, 3.0]]\n    mock_embed.assert_called_once_with(\n        \"hello\",\n        model=\"test-model\",\n        async_=False,\n    )\n\n\ndef test_recall_defaults_to_config_limit_in_cloud(monkeypatch, mocker):\n    monkeypatch.delenv(\"MEMORI_COCKROACHDB_CONNECTION_STRING\", raising=False)\n    monkeypatch.setenv(\"MEMORI_API_KEY\", \"test-api-key\")\n    monkeypatch.setenv(\"MEMORI_TEST_MODE\", \"1\")\n\n    mem = Memori().attribution(entity_id=\"entity-id\", process_id=\"process-id\")\n    mem.config.recall_facts_limit = 10\n\n    post = mocker.patch(\n        \"memori.memory.recall.Api.post\",\n        autospec=True,\n        return_value={\"facts\": [], \"messages\": []},\n    )\n\n    mem.recall(\"test query\")\n\n    assert post.call_args[0][1] == \"cloud/recall\"\n    payload = post.call_args[0][2]\n    assert payload[\"limit\"] == 10\n\n    mem.recall(\"test query\", limit=3)\n    payload = post.call_args[0][2]\n    assert payload[\"limit\"] == 3\n"
  },
  {
    "path": "tests/test_legacy_package_warning.py",
    "content": "import warnings\nfrom importlib.metadata import PackageNotFoundError\nfrom unittest.mock import patch\n\nfrom memori._exceptions import (\n    MemoriLegacyPackageWarning,\n    warn_if_legacy_memorisdk_installed,\n)\n\n\ndef test_warn_if_legacy_memorisdk_not_installed():\n    \"\"\"Test that no warning is emitted when memorisdk is not installed.\"\"\"\n    with patch(\"memori._exceptions.distribution\", side_effect=PackageNotFoundError()):\n        with warnings.catch_warnings(record=True) as warning_list:\n            warnings.simplefilter(\"always\")\n            warn_if_legacy_memorisdk_installed()\n\n            legacy_warnings = [\n                w\n                for w in warning_list\n                if issubclass(w.category, MemoriLegacyPackageWarning)\n            ]\n            assert len(legacy_warnings) == 0, (\n                \"Should not emit warning when memorisdk is not installed\"\n            )\n\n\ndef test_warn_if_legacy_memorisdk_installed():\n    \"\"\"Test that warning is emitted when memorisdk is installed.\"\"\"\n    with patch(\"memori._exceptions.distribution\"):\n        with warnings.catch_warnings(record=True) as warning_list:\n            warnings.simplefilter(\"always\")\n            warn_if_legacy_memorisdk_installed()\n\n            legacy_warnings = [\n                w\n                for w in warning_list\n                if issubclass(w.category, MemoriLegacyPackageWarning)\n            ]\n            assert len(legacy_warnings) == 1, (\n                \"Should emit warning when memorisdk is installed\"\n            )\n            assert \"memorisdk\" in str(legacy_warnings[0].message)\n            assert \"pip uninstall memorisdk\" in str(legacy_warnings[0].message)\n            assert \"pip install memori\" in str(legacy_warnings[0].message)\n\n\ndef test_warning_message_content():\n    \"\"\"Test that the warning message contains helpful migration instructions.\"\"\"\n    with patch(\"memori._exceptions.distribution\"):\n        with warnings.catch_warnings(record=True) as warning_list:\n            warnings.simplefilter(\"always\")\n            warn_if_legacy_memorisdk_installed()\n\n            legacy_warnings = [\n                w\n                for w in warning_list\n                if issubclass(w.category, MemoriLegacyPackageWarning)\n            ]\n            assert len(legacy_warnings) == 1\n            message = str(legacy_warnings[0].message)\n            assert \"memorisdk\" in message\n            assert \"deprecated\" in message\n            assert \"pip uninstall memorisdk\" in message\n            assert \"pip install memori\" in message\n\n\ndef test_legacy_warning_class_is_user_warning():\n    \"\"\"Test that MemoriLegacyPackageWarning is a UserWarning subclass.\"\"\"\n    assert issubclass(MemoriLegacyPackageWarning, UserWarning), (\n        \"MemoriLegacyPackageWarning should be a UserWarning subclass\"\n    )\n\n\ndef test_warn_function_imported_in_init():\n    \"\"\"Test that warn_if_legacy_memorisdk_installed is available in __init__.py.\"\"\"\n    import inspect\n\n    import memori\n\n    source = inspect.getsource(memori)\n    assert \"warn_if_legacy_memorisdk_installed\" in source, (\n        \"warn_if_legacy_memorisdk_installed should be imported in __init__.py\"\n    )\n\n\ndef test_no_warning_when_only_memori_installed():\n    \"\"\"Test that importing memori package doesn't emit warning when memorisdk is not installed.\"\"\"\n\n    def mock_distribution(pkg):\n        if pkg == \"memorisdk\":\n            raise PackageNotFoundError()\n        return None\n\n    with patch(\"memori._exceptions.distribution\", side_effect=mock_distribution):\n        with warnings.catch_warnings(record=True) as warning_list:\n            warnings.simplefilter(\"always\")\n            warn_if_legacy_memorisdk_installed()\n\n            legacy_warnings = [\n                w\n                for w in warning_list\n                if issubclass(w.category, MemoriLegacyPackageWarning)\n            ]\n            assert len(legacy_warnings) == 0, (\n                \"Should not emit warning when only memori is installed\"\n            )\n"
  },
  {
    "path": "tests/test_llm_auto_registration.py",
    "content": "\"\"\"Tests for automatic LLM client detection and registration via llm.register().\"\"\"\n\nimport pytest\n\nfrom memori import Memori\nfrom memori._exceptions import UnsupportedLLMProviderError\nfrom memori.llm._base import BaseClient\nfrom memori.llm._registry import Registry\n\n\n@pytest.fixture\ndef memori_instance(mocker):\n    \"\"\"Create a Memori instance with mocked storage.\"\"\"\n    mock_conn = mocker.MagicMock()\n    mocker.patch(\"memori.storage.Manager.start\", return_value=mocker.MagicMock())\n    mocker.patch(\n        \"memori.memory.augmentation.Manager.start\", return_value=mocker.MagicMock()\n    )\n    return Memori(conn=mock_conn)\n\n\ndef test_llm_register_raises_if_provider_cannot_be_determined(\n    memori_instance, monkeypatch\n):\n    class DummyClientHandler(BaseClient):\n        def register(self, client, _provider=None):\n            return self\n\n    def _matches_any(_client):\n        return True\n\n    monkeypatch.setattr(Registry, \"_clients\", {_matches_any: DummyClientHandler})\n\n    with pytest.raises(\n        UnsupportedLLMProviderError,\n        match=r\"provider could not be determined during registration\",\n    ):\n        memori_instance.llm.register(object())\n\n\ndef test_llm_register_auto_detects_openai_client(memori_instance, mocker):\n    \"\"\"Test that llm.register() auto-detects OpenAI client.\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"openai\"\n    mock_client._version = \"2.8.1\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.base_url\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = memori_instance.llm.register(mock_client)\n\n    assert result is memori_instance\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n\n\ndef test_llm_register_auto_detects_anthropic_client(memori_instance, mocker):\n    \"\"\"Test that llm.register() auto-detects Anthropic client.\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"anthropic\"\n    mock_client.messages.create = mocker.MagicMock()\n    mock_client.beta.messages.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    mock_anthropic_module = mocker.MagicMock()\n    mock_anthropic_module.__version__ = \"0.75.0\"\n    mocker.patch.dict(\"sys.modules\", {\"anthropic\": mock_anthropic_module})\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = memori_instance.llm.register(mock_client)\n\n    assert result is memori_instance\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n\n\ndef test_llm_register_auto_detects_google_genai_client(memori_instance, mocker):\n    \"\"\"Test that llm.register() auto-detects Google genai client.\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"google.genai.client\"\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.aio\n\n    mock_genai_module = mocker.MagicMock()\n    mock_genai_module.__version__ = \"1.52.0\"\n\n    mock_google_module = mocker.MagicMock()\n    mock_google_module.genai = mock_genai_module\n\n    mocker.patch.dict(\n        \"sys.modules\", {\"google\": mock_google_module, \"google.genai\": mock_genai_module}\n    )\n\n    result = memori_instance.llm.register(mock_client)\n\n    assert result is memori_instance\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n\n\ndef test_llm_register_auto_detects_google_generativeai_client(memori_instance, mocker):\n    \"\"\"Test that llm.register() auto-detects Google generativeai client (legacy).\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"google.generativeai\"\n    mock_client.models.generate_content = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.aio\n\n    mock_genai_module = mocker.MagicMock()\n    mock_genai_module.__version__ = \"1.52.0\"\n\n    mock_google_module = mocker.MagicMock()\n    mock_google_module.genai = mock_genai_module\n\n    mocker.patch.dict(\n        \"sys.modules\", {\"google\": mock_google_module, \"google.genai\": mock_genai_module}\n    )\n\n    result = memori_instance.llm.register(mock_client)\n\n    assert result is memori_instance\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n\n\ndef test_llm_register_auto_detects_xai_client(memori_instance, mocker):\n    \"\"\"Test that llm.register() auto-detects XAI client.\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"xai_sdk.client\"\n    mock_client.chat.create = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.chat.completions\n\n    mock_xai_sdk_module = mocker.MagicMock()\n    mock_xai_sdk_module.__version__ = \"1.4.1\"\n    mocker.patch.dict(\"sys.modules\", {\"xai_sdk\": mock_xai_sdk_module})\n\n    result = memori_instance.llm.register(mock_client)\n\n    assert result is memori_instance\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n\n\ndef test_llm_register_auto_detects_pydantic_ai_client(memori_instance, mocker):\n    \"\"\"Test that llm.register() auto-detects Pydantic AI client.\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"pydantic_ai.agent\"\n    mock_client._version = \"1.0.0\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    del mock_client._memori_installed\n\n    result = memori_instance.llm.register(mock_client)\n\n    assert result is memori_instance\n    assert hasattr(mock_client, \"_memori_installed\")\n    assert mock_client._memori_installed is True\n\n\ndef test_llm_register_returns_memori_instance(memori_instance, mocker):\n    \"\"\"Test that llm.register() returns the Memori instance for chaining.\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"openai\"\n    mock_client._version = \"2.8.1\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.base_url\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    result = memori_instance.llm.register(mock_client)\n\n    assert result is memori_instance\n    assert isinstance(result, Memori)\n\n\ndef test_llm_register_allows_chaining(memori_instance, mocker):\n    \"\"\"Test that llm.register() can be chained with attribution().\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"openai\"\n    mock_client._version = \"2.8.1\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.base_url\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    # Should not raise any exceptions\n    result = memori_instance.llm.register(mock_client).attribution(\n        entity_id=\"test_entity\", process_id=\"test_process\"\n    )\n\n    assert isinstance(result, Memori)\n    assert result.config.entity_id == \"test_entity\"\n    assert result.config.process_id == \"test_process\"\n\n\ndef test_llm_register_handles_multiple_registrations(memori_instance, mocker):\n    \"\"\"Test that multiple calls to llm.register() with the same client don't cause issues.\"\"\"\n    mock_client = mocker.MagicMock()\n    type(mock_client).__module__ = \"openai\"\n    mock_client._version = \"2.8.1\"\n    mock_client.chat.completions.create = mocker.MagicMock()\n    mock_client.beta.chat.completions.parse = mocker.MagicMock()\n    del mock_client._memori_installed\n    del mock_client.base_url\n\n    mocker.patch(\"asyncio.get_running_loop\", side_effect=RuntimeError)\n\n    # First registration\n    memori_instance.llm.register(mock_client)\n    assert mock_client._memori_installed is True\n\n    # Second registration should not cause issues\n    memori_instance.llm.register(mock_client)\n    assert mock_client._memori_installed is True\n\n\ndef test_llm_register_agno_openai_chat(memori_instance, mocker):\n    \"\"\"Test that llm.register() works with Agno OpenAI models.\"\"\"\n    mock_model = mocker.MagicMock()\n    mock_agno_register = mocker.patch.object(memori_instance.agno, \"register\")\n\n    result = memori_instance.llm.register(openai_chat=mock_model)\n\n    assert result is memori_instance\n    mock_agno_register.assert_called_once_with(\n        openai_chat=mock_model, claude=None, gemini=None, xai=None\n    )\n\n\ndef test_llm_register_agno_claude(memori_instance, mocker):\n    \"\"\"Test that llm.register() works with Agno Claude models.\"\"\"\n    mock_model = mocker.MagicMock()\n    mock_agno_register = mocker.patch.object(memori_instance.agno, \"register\")\n\n    result = memori_instance.llm.register(claude=mock_model)\n\n    assert result is memori_instance\n    mock_agno_register.assert_called_once_with(\n        openai_chat=None, claude=mock_model, gemini=None, xai=None\n    )\n\n\ndef test_llm_register_langchain_chatopenai(memori_instance, mocker):\n    \"\"\"Test that llm.register() works with LangChain ChatOpenAI models.\"\"\"\n    mock_model = mocker.MagicMock()\n    mock_langchain_register = mocker.patch.object(memori_instance.langchain, \"register\")\n\n    result = memori_instance.llm.register(chatopenai=mock_model)\n\n    assert result is memori_instance\n    mock_langchain_register.assert_called_once_with(\n        chatbedrock=None,\n        chatgooglegenai=None,\n        chatopenai=mock_model,\n        chatvertexai=None,\n    )\n\n\ndef test_llm_register_raises_when_mixing_client_and_framework(memori_instance, mocker):\n    \"\"\"Test that llm.register() raises error when mixing direct client and framework.\"\"\"\n    mock_client = mocker.MagicMock()\n    mock_model = mocker.MagicMock()\n\n    with pytest.raises(\n        RuntimeError,\n        match=\"Cannot mix direct client registration with framework registration\",\n    ):\n        memori_instance.llm.register(client=mock_client, openai_chat=mock_model)\n\n\ndef test_llm_register_raises_when_mixing_agno_and_langchain(memori_instance, mocker):\n    \"\"\"Test that llm.register() raises error when mixing Agno and LangChain.\"\"\"\n    mock_agno_model = mocker.MagicMock()\n    mock_langchain_model = mocker.MagicMock()\n\n    with pytest.raises(\n        RuntimeError,\n        match=\"Cannot register both Agno and LangChain clients in the same call\",\n    ):\n        memori_instance.llm.register(\n            openai_chat=mock_agno_model, chatopenai=mock_langchain_model\n        )\n\n\ndef test_llm_register_raises_when_no_arguments(memori_instance):\n    \"\"\"Test that llm.register() raises error when called with no arguments.\"\"\"\n    with pytest.raises(\n        RuntimeError, match=\"No client or framework model provided to register\"\n    ):\n        memori_instance.llm.register()\n"
  },
  {
    "path": "tests/test_network.py",
    "content": "import os\nimport ssl\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport aiohttp\nimport pytest\nimport requests\nfrom requests.adapters import HTTPAdapter\n\nfrom memori._config import Config\nfrom memori._exceptions import (\n    MemoriApiClientError,\n    MemoriApiError,\n    MemoriApiRequestRejectedError,\n    MemoriApiValidationError,\n    QuotaExceededError,\n)\nfrom memori._network import Api, ApiSubdomain, _ApiRetryRecoverable\n\n\n@pytest.fixture\ndef api():\n    os.environ[\"MEMORI_API_URL_BASE\"] = \"https://test.api.com\"\n    return Api(Config())\n\n\n@pytest.fixture\ndef config():\n    return Config()\n\n\nclass TestApiRetryRecoverable:\n    def test_is_retry_returns_true_for_5xx_errors(self):\n        retry = _ApiRetryRecoverable()\n        assert retry.is_retry(\"GET\", 500) is True\n        assert retry.is_retry(\"GET\", 503) is True\n        assert retry.is_retry(\"GET\", 599) is True\n\n    def test_is_retry_returns_false_for_non_5xx_errors(self):\n        retry = _ApiRetryRecoverable()\n        assert retry.is_retry(\"GET\", 200) is False\n        assert retry.is_retry(\"GET\", 404) is False\n        assert retry.is_retry(\"GET\", 400) is False\n\n\nclass TestApiInitialization:\n    def test_init_uses_default_api_key_and_base_url(self):\n        if \"MEMORI_API_URL_BASE\" in os.environ:\n            del os.environ[\"MEMORI_API_URL_BASE\"]\n        if \"MEMORI_TEST_MODE\" in os.environ:\n            del os.environ[\"MEMORI_TEST_MODE\"]\n\n        api = Api(Config())\n        assert api._Api__x_api_key == \"96a7ea3e-11c2-428c-b9ae-5a168363dc80\"  # type: ignore[attr-defined]\n        assert api._Api__base == \"https://api.memorilabs.ai\"  # type: ignore[attr-defined]\n\n    def test_init_uses_custom_subdomain_base_url(self):\n        if \"MEMORI_API_URL_BASE\" in os.environ:\n            del os.environ[\"MEMORI_API_URL_BASE\"]\n        if \"MEMORI_TEST_MODE\" in os.environ:\n            del os.environ[\"MEMORI_TEST_MODE\"]\n\n        api = Api(Config(), subdomain=ApiSubdomain.COLLECTOR)\n        assert api._Api__x_api_key == \"96a7ea3e-11c2-428c-b9ae-5a168363dc80\"  # type: ignore[attr-defined]\n        assert api._Api__base == \"https://collector.memorilabs.ai\"  # type: ignore[attr-defined]\n\n    def test_init_uses_custom_base_url(self):\n        if \"MEMORI_TEST_MODE\" in os.environ:\n            del os.environ[\"MEMORI_TEST_MODE\"]\n        os.environ[\"MEMORI_API_URL_BASE\"] = \"https://custom.api.com\"\n        api = Api(Config())\n        assert api._Api__x_api_key == \"c18b1022-7fe2-42af-ab01-b1f9139184f0\"  # type: ignore[attr-defined]\n        assert api._Api__base == \"https://custom.api.com\"  # type: ignore[attr-defined]\n\n    def test_init_uses_staging_when_test_mode_enabled(self):\n        if \"MEMORI_API_URL_BASE\" in os.environ:\n            del os.environ[\"MEMORI_API_URL_BASE\"]\n        os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n        try:\n            api = Api(Config())\n            assert api._Api__x_api_key == \"c18b1022-7fe2-42af-ab01-b1f9139184f0\"  # type: ignore[attr-defined]\n            assert api._Api__base == \"https://staging-api.memorilabs.ai\"  # type: ignore[attr-defined]\n        finally:\n            del os.environ[\"MEMORI_TEST_MODE\"]\n\n    def test_init_uses_staging_subdomain_when_test_mode_enabled(self):\n        if \"MEMORI_API_URL_BASE\" in os.environ:\n            del os.environ[\"MEMORI_API_URL_BASE\"]\n        os.environ[\"MEMORI_TEST_MODE\"] = \"1\"\n\n        try:\n            api = Api(Config(), subdomain=ApiSubdomain.DEFAULT)\n            assert api._Api__x_api_key == \"c18b1022-7fe2-42af-ab01-b1f9139184f0\"  # type: ignore[attr-defined]\n            assert api._Api__base == \"https://staging-api.memorilabs.ai\"  # type: ignore[attr-defined]\n        finally:\n            del os.environ[\"MEMORI_TEST_MODE\"]\n\n    def test_init_uses_production_when_test_mode_disabled(self):\n        if \"MEMORI_API_URL_BASE\" in os.environ:\n            del os.environ[\"MEMORI_API_URL_BASE\"]\n        os.environ[\"MEMORI_TEST_MODE\"] = \"0\"\n\n        try:\n            api = Api(Config())\n            assert api._Api__x_api_key == \"96a7ea3e-11c2-428c-b9ae-5a168363dc80\"  # type: ignore[attr-defined]\n            assert api._Api__base == \"https://api.memorilabs.ai\"  # type: ignore[attr-defined]\n        finally:\n            del os.environ[\"MEMORI_TEST_MODE\"]\n\n\nclass TestApiUrl:\n    def test_url_construction(self, api):\n        assert api.url(\"test/route\") == \"https://test.api.com/v1/test/route\"\n\n    def test_url_with_empty_route(self, api):\n        assert api.url(\"\") == \"https://test.api.com/v1/\"\n\n\nclass TestApiHeaders:\n    def test_headers_without_api_key(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        headers = api.headers()\n        assert \"X-Memori-API-Key\" in headers\n        assert \"Authorization\" not in headers\n\n    def test_headers_with_api_key(self, api):\n        os.environ[\"MEMORI_API_KEY\"] = \"test-api-key-123\"\n        headers = api.headers()\n        assert headers[\"Authorization\"] == \"Bearer test-api-key-123\"\n        assert \"X-Memori-API-Key\" in headers\n\n\nclass TestApiPost:\n    def test_post_success(self, api, mocker):\n        mock_response = MagicMock()\n        mock_response.json.return_value = {\"result\": \"success\"}\n        mock_response.raise_for_status = MagicMock()\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response\n        mocker.patch.object(api, \"_Api__session\", return_value=mock_session)\n\n        result = api.post(\"test/endpoint\", json={\"data\": \"test\"})\n\n        assert result == {\"result\": \"success\"}\n        mock_session.post.assert_called_once()\n        mock_response.raise_for_status.assert_called_once()\n\n    def test_post_raises_http_error(self, api, mocker):\n        mock_response = MagicMock()\n        mock_response.raise_for_status.side_effect = requests.HTTPError(\"404\")\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response\n        mocker.patch.object(api, \"_Api__session\", return_value=mock_session)\n\n        with pytest.raises(requests.HTTPError):\n            api.post(\"test/endpoint\")\n\n\nclass TestApiPostAsync:\n    @pytest.mark.asyncio\n    async def test_post_async_success(self, api, mocker):\n        mock_response = {\"result\": \"async success\"}\n        mock_request_async = AsyncMock(return_value=mock_response)\n        mocker.patch.object(api, \"_Api__request_async\", mock_request_async)\n\n        result = await api.post_async(\"test/endpoint\", json={\"data\": \"test\"})\n        assert result == {\"result\": \"async success\"}\n\n    @pytest.mark.asyncio\n    async def test_post_async_with_retry_on_5xx_error(self, api, mocker):\n        error = aiohttp.ClientResponseError(\n            request_info=MagicMock(), history=(), status=503\n        )\n        success_response = {\"result\": \"success after retry\"}\n\n        mock_response_ctx_fail = MagicMock()\n        mock_response_ctx_fail.__aenter__.return_value = MagicMock()\n        mock_response_ctx_fail.__aenter__.return_value.raise_for_status.side_effect = (\n            error\n        )\n        mock_response_ctx_fail.__aexit__.return_value = None\n\n        mock_response_ctx_success = MagicMock()\n        mock_response_success = MagicMock()\n        mock_response_success.raise_for_status = MagicMock()\n        mock_response_success.json = AsyncMock(return_value=success_response)\n        mock_response_ctx_success.__aenter__.return_value = mock_response_success\n        mock_response_ctx_success.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.request.side_effect = [\n            mock_response_ctx_fail,\n            mock_response_ctx_success,\n        ]\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with patch(\"asyncio.sleep\", new_callable=AsyncMock) as mock_sleep:\n                result = await api.post_async(\"test/endpoint\")\n                assert result == success_response\n                assert mock_sleep.call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_post_async_raises_on_4xx_error(self, api):\n        error = aiohttp.ClientResponseError(\n            request_info=MagicMock(), history=(), status=404\n        )\n\n        mock_response = MagicMock()\n        mock_response.raise_for_status.side_effect = error\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.request.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(aiohttp.ClientResponseError):\n                await api.post_async(\"test/endpoint\")\n\n    @pytest.mark.asyncio\n    async def test_post_async_raises_after_max_retries(self, api):\n        error = aiohttp.ClientResponseError(\n            request_info=MagicMock(), history=(), status=503\n        )\n\n        mock_response = MagicMock()\n        mock_response.raise_for_status.side_effect = error\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.request.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with patch(\"asyncio.sleep\", new_callable=AsyncMock):\n                with pytest.raises(aiohttp.ClientResponseError):\n                    await api.post_async(\"test/endpoint\")\n\n\nclass TestApiAugmentation:\n    @pytest.mark.asyncio\n    async def test_augmentation_async_success(self, api, mocker):\n        mock_response_data = {\n            \"conversation\": {\"summary\": \"Test summary\"},\n            \"entity\": {\"facts\": [\"fact1\", \"fact2\"]},\n            \"process\": {\"attributes\": []},\n        }\n\n        mock_response = MagicMock()\n        mock_response.status = 200\n        mock_response.json = AsyncMock(return_value=mock_response_data)\n        mock_response.raise_for_status = MagicMock()\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        payload = {\n            \"conversation\": {\n                \"messages\": [{\"role\": \"user\", \"content\": \"test\"}],\n                \"summary\": \"Test summary\",\n            },\n            \"meta\": {},\n        }\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            result = await api.augmentation_async(payload)\n\n        assert result is not None\n        assert result == mock_response_data\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_includes_storage_in_payload(self, api, mocker):\n        mock_response_data = {\n            \"conversation\": {\"summary\": \"Test summary\"},\n            \"entity\": {\"facts\": []},\n            \"process\": {\"attributes\": []},\n        }\n\n        mock_response = MagicMock()\n        mock_response.status = 200\n        mock_response.json = AsyncMock(return_value=mock_response_data)\n        mock_response.raise_for_status = MagicMock()\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        payload = {\n            \"conversation\": {\n                \"messages\": [{\"role\": \"user\", \"content\": \"test\"}],\n                \"summary\": \"Test summary\",\n            },\n            \"meta\": {\n                \"storage\": {\"cockroachdb\": True, \"dialect\": \"cockroachdb\"},\n            },\n        }\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            await api.augmentation_async(payload)\n\n        mock_session.post.assert_called_once()\n        call_args = mock_session.post.call_args\n        sent_payload = call_args[1][\"json\"]\n\n        assert \"meta\" in sent_payload\n        assert \"storage\" in sent_payload[\"meta\"]\n        assert sent_payload[\"meta\"][\"storage\"][\"cockroachdb\"] is True\n        assert sent_payload[\"meta\"][\"storage\"][\"dialect\"] == \"cockroachdb\"\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_mysql_dialect(self, api, mocker):\n        mock_response_data = {\n            \"conversation\": {\"summary\": \"Test summary\"},\n            \"entity\": {\"facts\": []},\n            \"process\": {\"attributes\": []},\n        }\n\n        mock_response = MagicMock()\n        mock_response.status = 200\n        mock_response.json = AsyncMock(return_value=mock_response_data)\n        mock_response.raise_for_status = MagicMock()\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        payload = {\n            \"conversation\": {\n                \"messages\": [{\"role\": \"user\", \"content\": \"test\"}],\n                \"summary\": \"Test summary\",\n            },\n            \"meta\": {\n                \"storage\": {\"cockroachdb\": False, \"dialect\": \"mysql\"},\n            },\n        }\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            await api.augmentation_async(payload)\n\n        call_args = mock_session.post.call_args\n        sent_payload = call_args[1][\"json\"]\n\n        assert sent_payload[\"meta\"][\"storage\"][\"cockroachdb\"] is False\n        assert sent_payload[\"meta\"][\"storage\"][\"dialect\"] == \"mysql\"\n\n\nclass TestApiPostAsyncGenericException:\n    @pytest.mark.asyncio\n    async def test_post_async_retries_on_generic_exception(self, api):\n        \"\"\"Test that generic exceptions (not ClientResponseError) also trigger retry logic.\"\"\"\n        generic_error = RuntimeError(\"Network timeout\")\n        success_response = {\"result\": \"success after retry\"}\n\n        mock_response_ctx_fail = MagicMock()\n        mock_response_ctx_fail.__aenter__.side_effect = generic_error\n        mock_response_ctx_fail.__aexit__.return_value = None\n\n        mock_response_ctx_success = MagicMock()\n        mock_response_success = MagicMock()\n        mock_response_success.raise_for_status = MagicMock()\n        mock_response_success.json = AsyncMock(return_value=success_response)\n        mock_response_ctx_success.__aenter__.return_value = mock_response_success\n        mock_response_ctx_success.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.request.side_effect = [\n            mock_response_ctx_fail,\n            mock_response_ctx_success,\n        ]\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with patch(\"asyncio.sleep\", new_callable=AsyncMock) as mock_sleep:\n                result = await api.post_async(\"test/endpoint\")\n                assert result == success_response\n                assert mock_sleep.call_count == 1\n\n    @pytest.mark.asyncio\n    async def test_post_async_raises_generic_exception_after_max_retries(self, api):\n        \"\"\"Test that generic exceptions raise after max retries.\"\"\"\n        generic_error = RuntimeError(\"Persistent network error\")\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.side_effect = generic_error\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.request.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with patch(\"asyncio.sleep\", new_callable=AsyncMock):\n                with pytest.raises(RuntimeError, match=\"Persistent network error\"):\n                    await api.post_async(\"test/endpoint\")\n\n\nclass TestApiSession:\n    def test_session_creates_session_with_retry_adapter(self, api):\n        \"\"\"Test that __session creates a properly configured requests Session.\"\"\"\n        session = api._Api__session()\n\n        assert isinstance(session, requests.Session)\n\n        # Check that adapters are mounted\n        https_adapter = session.get_adapter(\"https://example.com\")\n        http_adapter = session.get_adapter(\"http://example.com\")\n\n        assert https_adapter is not None\n        assert http_adapter is not None\n        assert isinstance(https_adapter, HTTPAdapter)\n        assert isinstance(http_adapter, HTTPAdapter)\n\n\nclass TestApiQuotaEnforcement:\n    @pytest.mark.asyncio\n    async def test_augmentation_async_raises_quota_exceeded_with_api_message(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        api_error_message = \"You have exceeded your quota. Please upgrade your plan.\"\n\n        mock_response = MagicMock()\n        mock_response.status = 429\n        mock_response.json = AsyncMock(return_value={\"message\": api_error_message})\n        mock_response.raise_for_status = MagicMock()\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(QuotaExceededError) as exc_info:\n                await api.augmentation_async({\"test\": \"payload\"})\n            assert str(exc_info.value) == api_error_message\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_raises_quota_exceeded_with_default_message(\n        self, api\n    ):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        mock_response = MagicMock()\n        mock_response.status = 429\n        mock_response.json = AsyncMock(return_value={})\n        mock_response.raise_for_status = MagicMock()\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(QuotaExceededError) as exc_info:\n                await api.augmentation_async({\"test\": \"payload\"})\n            assert (\n                \"your IP address is over quota; register for an API key now: \"\n                + \"https://app.memorilabs.ai/signup\"\n                in str(exc_info.value)\n            )\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_raises_quota_exceeded_when_json_fails(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        mock_response = MagicMock()\n        mock_response.status = 429\n        mock_response.json = AsyncMock(side_effect=Exception(\"JSON parse error\"))\n        mock_response.raise_for_status = MagicMock()\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(QuotaExceededError) as exc_info:\n                await api.augmentation_async({\"test\": \"payload\"})\n            assert (\n                \"your IP address is over quota; register for an API key now: \"\n                + \"https://app.memorilabs.ai/signup\"\n                in str(exc_info.value)\n            )\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_returns_empty_dict_for_authenticated_429(\n        self, api\n    ):\n        os.environ[\"MEMORI_API_KEY\"] = \"test-key\"\n\n        mock_response = MagicMock()\n        mock_response.status = 429\n        mock_response.raise_for_status = MagicMock()\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            result = await api.augmentation_async({\"test\": \"payload\"})\n            assert result == {}\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_raises_validation_error_for_422(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        msg = \"Invalid payload\"\n\n        mock_response = MagicMock()\n        mock_response.status = 422\n        mock_response.json = AsyncMock(return_value={\"message\": msg})\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(MemoriApiValidationError) as exc_info:\n                await api.augmentation_async({\"test\": \"payload\"})\n            assert str(exc_info.value) == msg\n            assert exc_info.value.status_code == 422\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_raises_request_rejected_error_for_433(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        msg = \"Request rejected\"\n\n        mock_response = MagicMock()\n        mock_response.status = 433\n        mock_response.json = AsyncMock(return_value={\"message\": msg})\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(MemoriApiRequestRejectedError) as exc_info:\n                await api.augmentation_async({\"test\": \"payload\"})\n            assert str(exc_info.value) == msg\n            assert exc_info.value.status_code == 433\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_raises_client_error_for_other_4xx(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        msg = \"Bad request\"\n\n        mock_response = MagicMock()\n        mock_response.status = 400\n        mock_response.json = AsyncMock(return_value={\"message\": msg})\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(MemoriApiClientError) as exc_info:\n                await api.augmentation_async({\"test\": \"payload\"})\n            assert str(exc_info.value) == msg\n            assert exc_info.value.status_code == 400\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_raises_helpful_message_on_ssl_error(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.side_effect = ssl.SSLError(\n            \"certificate verify failed\"\n        )\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(MemoriApiError) as exc_info:\n                await api.augmentation_async({\"test\": \"payload\"})\n            assert \"SSL/TLS certificate error\" in str(exc_info.value)\n\n    @pytest.mark.asyncio\n    async def test_augmentation_async_raises_other_errors_for_anonymous(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n\n        error = aiohttp.ClientResponseError(\n            request_info=MagicMock(), history=(), status=500\n        )\n\n        mock_response = MagicMock()\n        mock_response.status = 500\n        mock_response.raise_for_status = MagicMock(side_effect=error)\n\n        mock_response_ctx = MagicMock()\n        mock_response_ctx.__aenter__.return_value = mock_response\n        mock_response_ctx.__aexit__.return_value = None\n\n        mock_session = MagicMock()\n        mock_session.post.return_value = mock_response_ctx\n        mock_session.__aenter__.return_value = mock_session\n        mock_session.__aexit__.return_value = None\n\n        with patch(\"aiohttp.ClientSession\", return_value=mock_session):\n            with pytest.raises(aiohttp.ClientResponseError) as exc_info:\n                await api.augmentation_async({\"test\": \"payload\"})\n            assert exc_info.value.status == 500\n\n    def test_is_anonymous_returns_true_without_api_key(self, api):\n        if \"MEMORI_API_KEY\" in os.environ:\n            del os.environ[\"MEMORI_API_KEY\"]\n        assert api._is_anonymous() is True\n\n    def test_is_anonymous_returns_false_with_api_key(self, api):\n        os.environ[\"MEMORI_API_KEY\"] = \"test-key\"\n        assert api._is_anonymous() is False\n"
  },
  {
    "path": "tests/test_search.py",
    "content": "r\"\"\"\n __  __                           _\n|  \\/  | ___ _ __ ___   ___  _ __(_)\n| |\\/| |/ _ \\ '_ ` _ \\ / _ \\| '__| |\n| |  | |  __/ | | | | | (_) | |  | |\n|_|  |_|\\___|_| |_| |_|\\___/|_|  |_|\n                 perfectam memoriam\n                      memorilabs.ai\n\"\"\"\n\nimport json\nimport struct\nfrom collections.abc import Iterator, Mapping\nfrom unittest.mock import MagicMock\n\nimport numpy as np\n\nfrom memori.search import (\n    FactCandidate,\n    find_similar_embeddings,\n    parse_embedding,\n    search_facts,\n)\n\n\nclass _MappingRow(Mapping[str, object]):\n    def __init__(self, data: dict[str, object]) -> None:\n        self._data = data\n\n    def __getitem__(self, key: str) -> object:\n        return self._data[key]\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self._data)\n\n    def __len__(self) -> int:\n        return len(self._data)\n\n\ndef test_parse_embedding_from_bytes_postgresql():\n    embedding = [1.0, 2.0, 3.0]\n    raw = struct.pack(f\"<{len(embedding)}f\", *embedding)\n    result = parse_embedding(raw)\n    np.testing.assert_array_almost_equal(result, embedding, decimal=5)\n\n\ndef test_parse_embedding_from_memoryview_postgresql():\n    embedding = [1.0, 2.0, 3.0]\n    raw = memoryview(struct.pack(f\"<{len(embedding)}f\", *embedding))\n    result = parse_embedding(raw)\n    np.testing.assert_array_almost_equal(result, embedding, decimal=5)\n\n\ndef test_parse_embedding_from_json_string_mysql():\n    embedding = [1.0, 2.0, 3.0]\n    raw = json.dumps(embedding)\n    result = parse_embedding(raw)\n    np.testing.assert_array_almost_equal(result, embedding, decimal=5)\n\n\ndef test_parse_embedding_from_list_mongodb():\n    embedding = [1.0, 2.0, 3.0]\n    result = parse_embedding(embedding)\n    np.testing.assert_array_almost_equal(result, embedding, decimal=5)\n\n\ndef test_parse_embedding_from_numpy_array():\n    embedding = np.array([1.0, 2.0, 3.0], dtype=np.float32)\n    result = parse_embedding(embedding)\n    np.testing.assert_array_almost_equal(result, embedding, decimal=5)\n\n\ndef test_parse_embedding_maintains_float32_dtype():\n    embedding = [1.0, 2.0, 3.0]\n    raw = json.dumps(embedding)\n    result = parse_embedding(raw)\n    assert result.dtype == np.float32\n\n\ndef test_find_similar_embeddings_basic():\n    embeddings = [\n        (1, [1.0, 0.0, 0.0]),\n        (2, [0.0, 1.0, 0.0]),\n        (3, [0.0, 0.0, 1.0]),\n    ]\n    query = [1.0, 0.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=2)\n\n    assert len(result) == 2\n    assert result[0][0] == 1\n    assert result[0][1] > 0.9\n\n\ndef test_find_similar_embeddings_cosine_similarity():\n    embeddings = [\n        (1, [1.0, 0.0]),\n        (2, [0.707, 0.707]),\n        (3, [0.0, 1.0]),\n    ]\n    query = [1.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=3)\n\n    assert len(result) == 3\n    assert result[0][0] == 1\n    assert result[1][0] == 2\n    assert result[2][0] == 3\n    assert result[0][1] > result[1][1] > result[2][1]\n\n\ndef test_find_similar_embeddings_empty_list():\n    result = find_similar_embeddings([], [1.0, 0.0], limit=5)\n    assert result == []\n\n\ndef test_find_similar_embeddings_limit_larger_than_embeddings():\n    embeddings = [(1, [1.0, 0.0]), (2, [0.0, 1.0])]\n    query = [1.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=10)\n\n    assert len(result) == 2\n\n\ndef test_find_similar_embeddings_limit_smaller_than_embeddings():\n    embeddings = [\n        (1, [1.0, 0.0]),\n        (2, [0.0, 1.0]),\n        (3, [0.5, 0.5]),\n    ]\n    query = [1.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=1)\n\n    assert len(result) == 1\n    assert result[0][0] == 1\n\n\ndef test_find_similar_embeddings_skips_malformed():\n    embeddings = [\n        (1, [1.0, 0.0, 0.0]),\n        (2, \"not_valid_json\"),\n        (3, [0.0, 0.0, 1.0]),\n    ]\n    query = [1.0, 0.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=5)\n\n    assert len(result) == 2\n    assert result[0][0] in [1, 3]\n    assert result[1][0] in [1, 3]\n\n\ndef test_find_similar_embeddings_all_malformed():\n    embeddings = [\n        (1, \"invalid\"),\n        (2, \"also_invalid\"),\n    ]\n    query = [1.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=5)\n\n    assert result == []\n\n\ndef test_find_similar_embeddings_dimension_mismatch():\n    embeddings = [\n        (1, [1.0, 0.0]),\n        (2, [0.0, 1.0]),\n    ]\n    query = [1.0, 0.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=5)\n\n    assert result == []\n\n\ndef test_find_similar_embeddings_mixed_dimensions():\n    embeddings = [\n        (1, [1.0, 0.0, 0.0]),  # 3D\n        (2, [0.0, 1.0]),  # 2D\n        (3, [0.0, 0.0, 1.0]),  # 3D\n    ]\n    query = [1.0, 0.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=5)\n\n    # Should ignore mismatched dimensions, not crash.\n    assert [fact_id for fact_id, _ in result] == [1, 3]\n\n\ndef test_find_similar_embeddings_mixed_formats():\n    embeddings = [\n        (1, json.dumps([1.0, 0.0, 0.0])),\n        (2, struct.pack(\"<3f\", 0.0, 1.0, 0.0)),\n        (3, [0.0, 0.0, 1.0]),\n    ]\n    query = [1.0, 0.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=3)\n\n    assert len(result) == 3\n    assert result[0][0] == 1\n\n\ndef test_find_similar_embeddings_returns_similarity_scores():\n    embeddings = [(1, [1.0, 0.0])]\n    query = [1.0, 0.0]\n    result = find_similar_embeddings(embeddings, query, limit=1)\n\n    assert len(result) == 1\n    assert isinstance(result[0][1], float)\n    assert 0.0 <= result[0][1] <= 1.0\n\n\ndef test_find_similar_embeddings_high_dimensional():\n    dim = 768\n    embeddings = [\n        (1, [1.0 if i == 0 else 0.0 for i in range(dim)]),\n        (2, [1.0 if i == 1 else 0.0 for i in range(dim)]),\n    ]\n    query = [1.0 if i == 0 else 0.0 for i in range(dim)]\n    result = find_similar_embeddings(embeddings, query, limit=2)\n\n    assert len(result) == 2\n    assert result[0][0] == 1\n\n\ndef test_search_entity_facts_success():\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": 1, \"content_embedding\": [1.0, 0.0, 0.0]},\n        {\"id\": 2, \"content_embedding\": [0.0, 1.0, 0.0]},\n        {\"id\": 3, \"content_embedding\": [0.0, 0.0, 1.0]},\n    ]\n    mock_driver.get_facts_by_ids.return_value = [\n        {\"id\": 1, \"content\": \"Fact one\", \"date_created\": \"2026-01-01 10:30:00\"},\n        {\"id\": 2, \"content\": \"Fact two\", \"date_created\": \"2026-01-02 11:15:00\"},\n    ]\n\n    query_embedding = [1.0, 0.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=2,\n        embeddings_limit=1000,\n    )\n\n    assert len(result) == 2\n    assert result[0].id == 1\n    assert result[0].content == \"Fact one\"\n    assert result[0].date_created == \"2026-01-01 10:30:00\"\n    assert isinstance(result[0].similarity, float)\n    assert isinstance(result[0].rank_score, float)\n\n    mock_driver.get_embeddings.assert_called_once_with(42, 1000)\n    mock_driver.get_facts_by_ids.assert_called_once()\n\n\ndef test_search_entity_facts_no_embeddings():\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = []\n\n    query_embedding = [1.0, 0.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=5,\n        embeddings_limit=1000,\n    )\n\n    assert result == []\n    mock_driver.get_embeddings.assert_called_once_with(42, 1000)\n    mock_driver.get_facts_by_ids.assert_not_called()\n\n\ndef test_search_entity_facts_no_similar_results():\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": 1, \"content_embedding\": \"invalid_json\"},\n    ]\n\n    query_embedding = [1.0, 0.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=5,\n        embeddings_limit=1000,\n    )\n\n    assert result == []\n    mock_driver.get_facts_by_ids.assert_not_called()\n\n\ndef test_search_entity_facts_respects_limit():\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": i, \"content_embedding\": [1.0 if j == i else 0.0 for j in range(5)]}\n        for i in range(5)\n    ]\n    mock_driver.get_facts_by_ids.return_value = [\n        {\"id\": i, \"content\": f\"Fact {i}\"} for i in range(3)\n    ]\n\n    query_embedding = [1.0, 0.0, 0.0, 0.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=3,\n        embeddings_limit=1000,\n    )\n\n    assert len(result) <= 3\n\n\ndef test_search_entity_facts_returns_required_keys():\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": 1, \"content_embedding\": [1.0, 0.0, 0.0]},\n    ]\n    mock_driver.get_facts_by_ids.return_value = [\n        {\"id\": 1, \"content\": \"Fact one\", \"date_created\": \"2026-01-01 10:30:00\"},\n    ]\n\n    query_embedding = [1.0, 0.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=5,\n        embeddings_limit=1000,\n    )\n\n    assert len(result) == 1\n    assert result[0].id == 1\n    assert result[0].content == \"Fact one\"\n    assert result[0].date_created == \"2026-01-01 10:30:00\"\n    assert isinstance(result[0].similarity, float)\n\n\ndef test_search_entity_facts_handles_missing_content():\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": 1, \"content_embedding\": [1.0, 0.0, 0.0]},\n        {\"id\": 2, \"content_embedding\": [0.0, 1.0, 0.0]},\n    ]\n    mock_driver.get_facts_by_ids.return_value = [\n        {\"id\": 1, \"content\": \"Fact one\"},\n    ]\n\n    query_embedding = [1.0, 0.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=2,\n        embeddings_limit=1000,\n    )\n\n    assert len(result) == 1\n    assert result[0].id == 1\n\n\ndef test_search_entity_facts_maintains_similarity_order():\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": 1, \"content_embedding\": [1.0, 0.0, 0.0]},\n        {\"id\": 2, \"content_embedding\": [0.707, 0.707, 0.0]},\n        {\"id\": 3, \"content_embedding\": [0.0, 1.0, 0.0]},\n    ]\n    mock_driver.get_facts_by_ids.return_value = [\n        {\"id\": 1, \"content\": \"Most similar\"},\n        {\"id\": 2, \"content\": \"Somewhat similar\"},\n        {\"id\": 3, \"content\": \"Least similar\"},\n    ]\n\n    query_embedding = [1.0, 0.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=3,\n        embeddings_limit=1000,\n    )\n\n    assert len(result) == 3\n    assert result[0].id == 1\n    assert result[0].similarity > result[1].similarity\n    assert result[1].similarity > result[2].similarity\n\n\ndef test_search_entity_facts_can_rerank_with_query_text(mocker):\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": 1, \"content_embedding\": [1.0, 0.0]},\n        {\"id\": 2, \"content_embedding\": [0.0, 1.0]},\n    ]\n    mock_driver.get_facts_by_ids.return_value = [\n        {\"id\": 1, \"content\": \"Completely unrelated\"},\n        {\"id\": 2, \"content\": \"This mentions blue explicitly\"},\n    ]\n\n    # Force semantic order to prefer id=1, then let lexical rerank pick id=2.\n    mocker.patch(\n        \"memori.search._api.find_similar_embeddings\",\n        return_value=[(1, 0.9), (2, 0.8)],\n    )\n\n    query_embedding = [1.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=1,\n        embeddings_limit=1000,\n        query_text=\"blue\",\n    )\n    assert len(result) == 1\n    assert result[0].id == 2\n\n\ndef test_search_entity_facts_with_different_db_formats():\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": 1, \"content_embedding\": json.dumps([1.0, 0.0, 0.0])},\n        {\"id\": 2, \"content_embedding\": struct.pack(\"<3f\", 0.0, 1.0, 0.0)},\n        {\"id\": 3, \"content_embedding\": [0.0, 0.0, 1.0]},\n    ]\n    mock_driver.get_facts_by_ids.return_value = [\n        {\"id\": 1, \"content\": \"Fact one\"},\n        {\"id\": 2, \"content\": \"Fact two\"},\n        {\"id\": 3, \"content\": \"Fact three\"},\n    ]\n\n    query_embedding = [1.0, 0.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=3,\n        embeddings_limit=1000,\n    )\n\n    assert len(result) == 3\n    assert result[0].id == 1\n\n\ndef test_search_entity_facts_accepts_mapping_rows_for_content(mocker):\n    mock_driver = MagicMock()\n    mock_driver.get_embeddings.return_value = [\n        {\"id\": 1, \"content_embedding\": [1.0, 0.0]},\n        {\"id\": 2, \"content_embedding\": [0.0, 1.0]},\n    ]\n    mock_driver.get_facts_by_ids.return_value = [\n        _MappingRow({\"id\": 1, \"content\": \"USER_ID has favorite color blue\"}),\n        _MappingRow({\"id\": 2, \"content\": \"USER_ID lives in Paris\"}),\n    ]\n\n    # Force semantic order so lexical has something to rerank.\n    mocker.patch(\n        \"memori.search._api.find_similar_embeddings\",\n        return_value=[(2, 0.8), (1, 0.7)],\n    )\n\n    query_embedding = [1.0, 0.0]\n    result = search_facts(\n        mock_driver,\n        42,\n        query_embedding,\n        limit=2,\n        embeddings_limit=1000,\n        query_text=\"favorite color\",\n    )\n\n    assert len(result) == 2\n    assert result[0].id == 1\n    assert result[0].similarity >= 0.0\n\n\ndef test_search_facts_candidates_success():\n    candidates = [\n        FactCandidate(\n            id=1,\n            content=\"Fact A\",\n            score=0.99,\n            date_created=\"2026-01-01 10:30:00\",\n        ),\n        FactCandidate(\n            id=2,\n            content=\"Fact B\",\n            score=0.5,\n            date_created=\"2026-01-02 11:15:00\",\n        ),\n    ]\n\n    result = search_facts(candidates=candidates, limit=1)\n\n    assert len(result) == 1\n    assert result[0].id == 1\n    assert result[0].content == \"Fact A\"\n    assert isinstance(result[0].similarity, float)\n    assert result[0].date_created == \"2026-01-01 10:30:00\"\n\n\ndef test_search_facts_candidates_can_rerank_with_query_text():\n    candidates = [\n        FactCandidate(\n            id=1,\n            content=\"Completely unrelated\",\n            score=0.9,\n            date_created=\"2026-01-01 10:30:00\",\n        ),\n        FactCandidate(\n            id=2,\n            content=\"This mentions blue explicitly\",\n            score=0.8,\n            date_created=\"2026-01-02 11:15:00\",\n        ),\n    ]\n\n    result = search_facts(\n        candidates=candidates,\n        limit=1,\n        query_text=\"blue\",\n    )\n    assert len(result) == 1\n    assert result[0].id == 2\n    assert result[0].date_created == \"2026-01-02 11:15:00\"\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "from memori._utils import bytes_to_json, generate_uniq, merge_chunk\n\n\ndef test_merge_chunk():\n    merged = merge_chunk({}, {\"a\": 1, \"b\": [1, 2], \"c\": {\"x\": \"foo\"}})\n    merged = merge_chunk(merged, {\"b\": [3], \"c\": {\"y\": \"bar\"}, \"d\": 5})\n    merged = merge_chunk(merged, {\"a\": 2, \"c\": {\"x\": \"baz\"}})\n\n    assert merged == {\"a\": 2, \"b\": [1, 2, 3], \"c\": {\"x\": \"baz\", \"y\": \"bar\"}, \"d\": 5}\n\n\ndef test_bytes_to_json():\n    assert bytes_to_json(\n        {\n            b\"name\": b\"Alice\",\n            \"info\": {\n                b\"age\": 47,\n                b\"email\": b\"alice@domain.com\",\n                \"tags\": [b\"abc\", b\"def\"],\n            },\n            \"body\": b'{\"ghi\": \"jkl\"}',\n        }\n    ) == {\n        \"body\": {\"ghi\": \"jkl\"},\n        \"info\": {\n            \"age\": 47,\n            \"email\": \"alice@domain.com\",\n            \"tags\": [\n                \"abc\",\n                \"def\",\n            ],\n        },\n        \"name\": \"Alice\",\n    }\n\n\ndef test_generate_uniq():\n    assert generate_uniq(None) is None  # type: ignore[arg-type]\n    assert generate_uniq([]) is None\n\n    assert (\n        generate_uniq([\"Abc. Def\", \"ghi\", \"123\"])\n        == \"84596a42b9169065d9f1bf2015e508beab38dd6af0814cc20572cf3b256f763d\"\n    )\n    assert (\n        generate_uniq([\"abc def\", \"GHI\", \"123 \"])\n        == \"84596a42b9169065d9f1bf2015e508beab38dd6af0814cc20572cf3b256f763d\"\n    )\n"
  }
]