[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Report a bug or issue with memU\ntitle: \"[BUG] \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to report a bug in memU! Please fill in the following details.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: A clear and detailed description of the bug.\n      placeholder: \"Enter a clear and concise description of what the bug is.\"\n    validations:\n      required: true\n\n  - type: input\n    id: environment\n    attributes:\n      label: Environment\n      description: The environment where this bug occurred (e.g., operating system, CPU arch, etc.).\n      placeholder: \"Enter details about the environment.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to reproduce\n      description: What are the steps to reproduce this issue?\n      placeholder: \"Enter step-by-step instructions to reproduce the issue.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected behavior\n      description: What should have happened instead?\n      placeholder: \"Describe what you expected to happen.\"\n    validations:\n      required: true\n\n  - type: input\n    id: version\n    attributes:\n      label: Version\n      description: What version of memU are you using?\n      placeholder: e.g., v1.0.0\n    validations:\n      required: true\n\n  - type: dropdown\n    id: severity\n    attributes:\n      label: Severity\n      description: How severe is the bug?\n      options:\n        - Critical\n        - Major\n        - Minor\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional_info\n    attributes:\n      label: Additional Information\n      description: Any other context or screenshots related to the bug.\n      placeholder: \"Enter additional context or information.\"\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: GitHub Discussions\n    url: https://github.com/NevaMind-AI/memU/discussions\n    about: For questions and general discussions about memU\n  - name: Documentation\n    url: https://github.com/NevaMind-AI/memU/blob/main/README.md\n    about: Check out the memU documentation\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Request a feature for memU\ntitle: \"[FEATURE] \"\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to request a feature for memU! Please fill in the following details.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: A clear and detailed description of the feature request.\n      placeholder: \"Enter a clear and concise description of what the feature request is.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: motivation\n    attributes:\n      label: Motivation\n      description: Why do we need this feature? What problem does it solve?\n      placeholder: \"Explain the use case and why this feature would be valuable.\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: platform\n    attributes:\n      label: Platform\n      description: Which platform(s) does this feature apply to?\n      options:\n        - Platform Independent\n        - Linux\n        - macOS\n        - Windows\n    validations:\n      required: true\n\n  - type: dropdown\n    id: priority\n    attributes:\n      label: Priority\n      description: How important is this feature?\n      options:\n        - Critical\n        - Major\n        - Minor\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional_info\n    attributes:\n      label: Additional Information\n      description: Any other context or screenshots related to the feature request.\n      placeholder: \"Enter additional context or information.\"\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/hackathon_task.yml",
    "content": "name: Hackathon Task\ndescription: The official designated task for the memU track in the 2026 New Year Challenge\ntitle: \"[2026NewYearChallenge] \"\nlabels: [\"hackathon-task\"]\nassignees: []\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for taking the time to join the challenge, and best of luck!\n\n  - type: dropdown\n    id: label_hint\n    attributes:\n      label: Label\n      description: Pick the most appropriate label for this task.\n      options:\n        - hackathon-task\n    validations:\n      required: true\n\n  - type: input\n    id: assignees_hint\n    attributes:\n      label: Assignees\n      description: GitHub usernames to assign (comma-separated). If empty, maintainers will triage.\n      placeholder: \"e.g., alice,bob\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description*\n      description: What will this task implement?\n      placeholder: \"Enter a clear and concise description of the task you want to implement.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: requirements\n    attributes:\n      label: Requirements (optional)\n      description: What will your planned pull request include? (APIs, modules, tests, docs, etc.)\n      placeholder: |\n        - [ ] Implementation details\n        - [ ] Tests\n        - [ ] Docs / examples\n        - [ ] Compatibility notes\n    validations:\n      required: false\n\n  - type: textarea\n    id: review_criteria\n    attributes:\n      label: Review Criteria (optional)\n      description: How will this task be evaluated?\n      placeholder: |\n        Example:\n        - Correctness (passes tests, no regressions)\n        - Quality (readability, modularity)\n        - DX (good docs / examples)\n        - Impact (useful for memU users)\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/improvement_suggestion.yml",
    "content": "name: Improvement Suggestion\ndescription: Suggest an improvement for memU\ntitle: \"[IMPROVEMENT] \"\nlabels: [\"enhancement\", \"improvement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to suggest an improvement for memU! Please fill in the following details.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: A clear and detailed description of the improvement suggestion.\n      placeholder: \"Enter a clear and concise description of what you'd like to improve.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: current_behavior\n    attributes:\n      label: Current Behavior\n      description: How does it work currently?\n      placeholder: \"Describe the current behavior or implementation.\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: proposed_improvement\n    attributes:\n      label: Proposed Improvement\n      description: How would you like it to work?\n      placeholder: \"Describe your proposed improvement.\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: benefits\n    attributes:\n      label: Benefits\n      description: What are the benefits of this improvement?\n      placeholder: \"Explain why this improvement would be valuable.\"\n    validations:\n      required: false\n\n  - type: input\n    id: version\n    attributes:\n      label: Version\n      description: What version of memU are you using?\n      placeholder: e.g., v1.0.0\n    validations:\n      required: false\n\n  - type: textarea\n    id: additional_info\n    attributes:\n      label: Additional Information\n      description: Any other context or screenshots related to the improvement.\n      placeholder: \"Enter additional context or information.\"\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## 📝 Pull Request Summary\n\nPlease provide a short summary explaining the purpose of this PR.\n\n---\n\n## ✅ What does this PR do?\n- Clearly describe the change introduced.\n- Mention the motivation or problem it solves.\n\n---\n\n## 🤔 Why is this change needed?\n- Explain the context or user impact.\n- Link any relevant issue or discussion.\n\n---\n\n## 🔍 Type of Change\nPlease check what applies:\n\n- [ ] Bug fix\n- [ ] New feature\n- [ ] Documentation update\n- [ ] Refactor / cleanup\n- [ ] Other (please explain)\n\n---\n\n## ✅ PR Quality Checklist\n\n- [ ] PR title follows the conventional format (feat:, fix:, docs:)\n- [ ] Changes are limited in scope and easy to review\n- [ ] Documentation updated where applicable\n- [ ] No breaking changes (or clearly documented)\n- [ ] Related issues or discussions linked\n\n---\n\n## 📌 Optional\n\n- [ ] Screenshots or examples added (if applicable)\n- [ ] Edge cases considered\n- [ ] Follow-up tasks mentioned\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name\n    strategy:\n      matrix:\n        python-version: [\"3.13\"]\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install uv & set Python\n        uses: astral-sh/setup-uv@v7\n        with:\n          python-version: ${{ matrix.python-version }}\n          enable-cache: true\n          cache-python: true\n\n      - name: Ensure Python ${{ matrix.python-version }}\n        run: uv python install ${{ matrix.python-version }}\n\n      - name: Sync dependencies\n        run: |\n          uv sync --frozen\n          uv run pre-commit install\n\n      - name: Run style checks\n        run: uv run make check\n\n      - name: Run tests\n        run: uv run make test\n"
  },
  {
    "path": ".github/workflows/pr-title.yml",
    "content": "name: pr-title\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - reopened\n      - synchronize\n\npermissions:\n  contents: read\n  statuses: write\n\njobs:\n  validate:\n    name: Validate PR title\n    runs-on: ubuntu-latest\n    steps:\n      - uses: amannn/action-semantic-pull-request@v6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          types: |\n            build\n            chore\n            ci\n            docs\n            feat\n            fix\n            perf\n            refactor\n            revert\n            style\n            test\n      - name: Disallow trailing ellipsis\n        shell: bash\n        env:\n          PR_TITLE: ${{ github.event.pull_request.title }}\n        run: |\n          if [[ \"$PR_TITLE\" == *... ]]; then\n            echo \"::error title=Invalid PR title::PR title must not end with an ellipsis (...)\"\n            exit 1\n          fi\n"
  },
  {
    "path": ".github/workflows/release-please.yml",
    "content": "on:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: write\n  issues: write\n  pull-requests: write\n\nname: release-please\n\njobs:\n  release-please:\n    runs-on: ubuntu-latest\n    outputs:\n      releases_created: ${{ steps.release.outputs.releases_created }}\n      tag_name: ${{ steps.release.outputs.tag_name }}\n    steps:\n      - uses: googleapis/release-please-action@v4\n        id: release\n        with:\n          release-type: python\n          target-branch: main\n\n  build-wheels:\n    name: build wheels (${{ matrix.label }})\n    runs-on: ${{ matrix.os }}\n    needs: release-please\n    if: ${{ needs.release-please.outputs.releases_created == 'true' && needs.release-please.outputs.tag_name != '' }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - os: ubuntu-latest\n            label: linux-x86_64\n            target: x86_64-unknown-linux-gnu\n            extra-args: \"--compatibility manylinux_2_39\"\n            python-version: \"3.13\"\n          - os: ubuntu-latest\n            label: linux-aarch64\n            target: aarch64-unknown-linux-gnu\n            extra-args: \"--compatibility manylinux_2_39\"\n            python-version: \"3.13\"\n          - os: macos-15-intel\n            label: macos-x86_64\n            target: \"\"\n            extra-args: \"\"\n            python-version: \"3.13\"\n          - os: macos-latest\n            label: macos-aarch64\n            target: \"\"\n            extra-args: \"\"\n            python-version: \"3.13\"\n          - os: windows-latest\n            label: windows-x86_64\n            target: \"\"\n            extra-args: \"\"\n            python-version: \"3.13\"\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install maturin\n        run: uv tool install maturin\n\n      - name: Setup Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: ${{ matrix.target }}\n\n      - name: Setup cross-compilation for Linux ARM64\n        if: matrix.label == 'linux-aarch64'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu\n\n      - name: Build wheel\n        run: uvx maturin build --release --out dist ${{ matrix.extra-args }} ${{ matrix.target && format('--target {0}', matrix.target) || '' }}\n        env:\n          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc\n\n      - name: Upload wheel artifact\n        uses: actions/upload-artifact@v6\n        with:\n          name: wheels-${{ matrix.label }}\n          path: dist/*.whl\n          if-no-files-found: error\n\n  build-sdist:\n    name: build sdist\n    runs-on: ubuntu-latest\n    needs: release-please\n    if: ${{ needs.release-please.outputs.releases_created == 'true' && needs.release-please.outputs.tag_name != '' }}\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          python-version: \"3.13\"\n\n      - name: Setup Rust\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Install maturin\n        run: uv tool install maturin\n\n      - name: Build sdist\n        run: uvx maturin sdist --out dist\n\n      - name: Upload sdist artifact\n        uses: actions/upload-artifact@v6\n        with:\n          name: sdist\n          path: dist/*.tar.gz\n          if-no-files-found: error\n\n  publish:\n    name: publish release artifacts\n    runs-on: ubuntu-latest\n    needs:\n      - release-please\n      - build-wheels\n      - build-sdist\n    if: ${{ needs.release-please.outputs.releases_created == 'true' && needs.release-please.outputs.tag_name != '' }}\n    environment: pypi\n    permissions:\n      id-token: write\n      contents: write\n    steps:\n      - name: Download wheel artifacts\n        uses: actions/download-artifact@v7\n        with:\n          pattern: wheels-*\n          merge-multiple: true\n          path: dist\n\n      - name: Download sdist artifact\n        uses: actions/download-artifact@v7\n        with:\n          name: sdist\n          path: dist\n\n      - name: Upload binaries to release\n        uses: svenstaro/upload-release-action@v2\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          file: dist/*\n          tag: ${{ needs.release-please.outputs.tag_name }}\n          file_glob: true\n\n      - name: Publish to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          packages-dir: dist/\n"
  },
  {
    "path": ".gitignore",
    "content": "data/\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.env.*\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be added to the global gitignore or merged into this project gitignore.  For a PyCharm\n#  project, it is common to ignore sensitive information such as project configurations.\n.idea/\n\n# Database files\n*.db\n*.sqlite\n*.sqlite3\npersona_memory.db\npersona_conversations.db\nbasic_memory_demo.db\ndemo_memory.db\ndemo_conversations.db\nchatbot_memory.db\nchatbot_conversations.db\nintegration_memory.db\nintegration_conversations.db\nconversation_demo.db\n\n# Log files\n*.log\n\n# Temporary files\ntmp/\ntemp/\n.tmp/\n\n# OS specific\n.DS_Store\n.DS_Store?\n._*\n.Spotlight-V100\n.Trashes\nehthumbs.db\nThumbs.db\n\n# IDE\n.vscode/\n*.swp\n*.swo\n*~\n\n# Model files (if using local models)\nmodels/\n*.bin\n*.pt\n*.pth\n*.onnx\n\n# API keys and secrets\n.env.local\n.env.*.local\nsecrets.txt\napi_keys.txt\n.cursor\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: \"v6.0.0\"\n    hooks:\n      - id: check-case-conflict\n      - id: check-merge-conflict\n      - id: check-toml\n      - id: check-yaml\n      - id: check-json\n      - id: pretty-format-json\n        args: [--autofix, --no-sort-keys]\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: \"v0.14.3\"\n    hooks:\n      - id: ruff\n        args: [--exit-non-zero-on-fix]\n      - id: ruff-format\n"
  },
  {
    "path": ".python-version",
    "content": "3.13\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\nGuidance for AI coding agents working in this repository.\n\n## Goal\n\nImplement features and fix bugs with minimal regression risk, while preserving memU's architecture:\n\n- `MemoryService` as composition root\n- workflow-based execution (`memorize`, `retrieve`, CRUD/patch)\n- pluggable storage backends (`inmemory`, `sqlite`, `postgres`)\n- profile-based LLM routing (`default`, `embedding`, custom profiles)\n\nSee `docs/architecture.md` for the current architectural view.\n\n## Where to Change Code\n\n- Service/runtime wiring: `src/memu/app/service.py`\n- Memorize flow: `src/memu/app/memorize.py`\n- Retrieve flow: `src/memu/app/retrieve.py`\n- CRUD/Patch flow: `src/memu/app/crud.py`\n- Config models/defaults: `src/memu/app/settings.py`\n- Workflow engine: `src/memu/workflow/*`\n- Storage abstraction/factory: `src/memu/database/interfaces.py`, `src/memu/database/factory.py`\n- In-memory: `src/memu/database/inmemory/*`\n- SQLite: `src/memu/database/sqlite/*`\n- Postgres: `src/memu/database/postgres/*`\n- LLM clients/wrappers/interceptors: `src/memu/llm/*`\n- Integrations: `src/memu/integrations/*`, `src/memu/client/*`\n- Tests: `tests/*`\n\n## Implementation Rules\n\n- Keep changes small and localized.\n- Do not change public API signatures unless explicitly required.\n- Preserve async behavior and existing workflow step contracts (`requires`/`produces` keys).\n- If adding a new capability, prefer integrating through an existing pipeline step or a new clearly named step.\n- Maintain backend parity where appropriate (if a repository contract changes, update all relevant backends).\n- Validate `where`/scope behavior against `UserConfig.model`; do not bypass scope filtering.\n- Keep type hints and mypy compatibility intact.\n\n## Feature Work Checklist\n\n1. Locate affected flow(s): memorize, retrieve, CRUD, or integration layer.\n2. Update config models/defaults if behavior is configurable.\n3. Wire behavior through `MemoryService` pipelines and step config (LLM profiles/capabilities).\n4. Implement backend/repository changes for all impacted providers.\n5. Add/extend tests for happy path and edge cases.\n6. Update docs when behavior changes (`README.md`, `docs/*`, examples if needed).\n7. If the change is architectural, add/update ADRs under `docs/adr/`.\n\n## Bug Fix Checklist\n\n1. Reproduce with an existing or new failing test.\n2. Implement the smallest safe fix at the correct layer.\n3. Add a regression test that fails before and passes after.\n4. Check cross-backend effects (`inmemory`, `sqlite`, `postgres`) and retrieval modes (`rag`, `llm`) when relevant.\n5. Verify no unintended API/output shape changes.\n\n## Testing and Validation\n\nUse `uv` for all local runs.\n\n- Setup: `make install`\n- Run all tests: `make test`\n- Run focused tests: `uv run python -m pytest tests/<target_test>.py`\n- Full quality checks: `make check`\n\nAt minimum, run targeted tests for touched code. Run `make check` for broad or cross-cutting changes.\nIf you cannot run a required check, state it explicitly in your final summary.\n\n## Done Criteria\n\nBefore finishing, ensure:\n\n- Code compiles and tests for changed behavior pass.\n- New behavior is covered by tests.\n- Docs are updated for user-visible or architectural changes.\n- No unrelated files were modified.\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [1.5.0](https://github.com/NevaMind-AI/memU/compare/v1.4.0...v1.5.0) (2026-03-17)\n\n\n### Features\n\n* add non-propagate option for memory patch ([#386](https://github.com/NevaMind-AI/memU/issues/386)) ([3b67458](https://github.com/NevaMind-AI/memU/commit/3b67458c6325b4fcdaccbc34f6e0fea491b7ba96))\n* **http:** add HTTP proxy support for LLM & embedding clients ([#310](https://github.com/NevaMind-AI/memU/issues/310)) ([a3d45e2](https://github.com/NevaMind-AI/memU/commit/a3d45e2f4174702fd8fd83cd13862285cd47cad9))\n\n\n### Bug Fixes\n\n* httpx base_url path discarded when endpoint starts with / ([#328](https://github.com/NevaMind-AI/memU/issues/328), [#336](https://github.com/NevaMind-AI/memU/issues/336), [#341](https://github.com/NevaMind-AI/memU/issues/341), [#329](https://github.com/NevaMind-AI/memU/issues/329)) ([#344](https://github.com/NevaMind-AI/memU/issues/344)) ([9e31ef2](https://github.com/NevaMind-AI/memU/commit/9e31ef235d46317f383613825090c52943cb55a2))\n\n\n### Documentation\n\n* add architecture explanation and adrs ([#353](https://github.com/NevaMind-AI/memU/issues/353)) ([8676a27](https://github.com/NevaMind-AI/memU/commit/8676a2721917f4e0baee6d01a9d6b95bd642be97))\n* add pull request template to improve contribution quality ([#261](https://github.com/NevaMind-AI/memU/issues/261)) ([2256119](https://github.com/NevaMind-AI/memU/commit/2256119fb2d177890c7f738c926f437a355e7359))\n* Update Discord link in README.md ([#377](https://github.com/NevaMind-AI/memU/issues/377)) ([163d050](https://github.com/NevaMind-AI/memU/commit/163d050299b77143226e9727f67d4826c9a69f92))\n* update memU Bot section with GitHub reference and \"Now open source\" ([#366](https://github.com/NevaMind-AI/memU/issues/366)) ([dc6880a](https://github.com/NevaMind-AI/memU/commit/dc6880a793d40b04cd599298a110af5f0f0b9cb5))\n\n## [1.4.0](https://github.com/NevaMind-AI/memU/compare/v1.3.0...v1.4.0) (2026-02-06)\n\n\n### Features\n\n* Add inline memory item references in category summaries ([#202](https://github.com/NevaMind-AI/memU/issues/202)) ([#205](https://github.com/NevaMind-AI/memU/issues/205)) ([5213571](https://github.com/NevaMind-AI/memU/commit/5213571b218d85784e0771f7a721eafd7da1c1ff))\n* Add salience-aware memory with reinforcement tracking ([#186](https://github.com/NevaMind-AI/memU/issues/186)) ([#206](https://github.com/NevaMind-AI/memU/issues/206)) ([2bdbcce](https://github.com/NevaMind-AI/memU/commit/2bdbcce1a87ae1017d5930901fb0ae8d2924dcee))\n* Add Tool Memory type with specialized metadata and statistics ([#247](https://github.com/NevaMind-AI/memU/issues/247)) ([4e8a035](https://github.com/NevaMind-AI/memU/commit/4e8a03578641afd0e07f9700629dff8d91d2b3fb))\n\n\n### Bug Fixes\n\n* add chat() API and stop misusing summarize() ([#208](https://github.com/NevaMind-AI/memU/issues/208)) ([be0a5c7](https://github.com/NevaMind-AI/memU/commit/be0a5c73250f0a21ad5e0d39b0e66e3018809e0f))\n* remove unused type: ignore comment and add lazyllm mypy override ([#275](https://github.com/NevaMind-AI/memU/issues/275)) ([0e490f7](https://github.com/NevaMind-AI/memU/commit/0e490f7333feecffaef0901cccb1c9a5dbb7bafb))\n* **video:** cleanup temp files on extraction failure ([#295](https://github.com/NevaMind-AI/memU/issues/295)) ([16b65e5](https://github.com/NevaMind-AI/memU/commit/16b65e54aef69dfe617835ca534c12752e06eda8))\n\n\n### Documentation\n\n* file system in readme ([#301](https://github.com/NevaMind-AI/memU/issues/301)) ([ee27d8a](https://github.com/NevaMind-AI/memU/commit/ee27d8aa5e42d249963b08d5775338080b0255f8))\n* fix name ([#291](https://github.com/NevaMind-AI/memU/issues/291)) ([fd0d179](https://github.com/NevaMind-AI/memU/commit/fd0d17998f6c0c9ba5c7e91964c39ee909f2b366))\n* fix readme lint ([#290](https://github.com/NevaMind-AI/memU/issues/290)) ([a7a5516](https://github.com/NevaMind-AI/memU/commit/a7a55169ecbc1579ae87dc68c5e09b1d62f07b56))\n* lint error ([#287](https://github.com/NevaMind-AI/memU/issues/287)) ([7f61dc7](https://github.com/NevaMind-AI/memU/commit/7f61dc72ffbadf849f227cf314bba7ce7964761b))\n* readme memubot ([#289](https://github.com/NevaMind-AI/memU/issues/289)) ([2f30798](https://github.com/NevaMind-AI/memU/commit/2f30798367e3591107e1e66a184d0dd4ad6c2f93))\n* Update README.md ([#300](https://github.com/NevaMind-AI/memU/issues/300)) ([1075d7c](https://github.com/NevaMind-AI/memU/commit/1075d7c9ec3a272846c9fdb8c22ec58ff73cf6e7))\n\n## [1.3.0](https://github.com/NevaMind-AI/memU/compare/v1.2.0...v1.3.0) (2026-01-29)\n\n\n### Features\n\n* add happened at and extra fields to memory item ([#262](https://github.com/NevaMind-AI/memU/issues/262)) ([77938e9](https://github.com/NevaMind-AI/memU/commit/77938e9c282e1c0eda11088675c35975d85c4ff0))\n* add proactive example ([#268](https://github.com/NevaMind-AI/memU/issues/268)) ([d3d1de1](https://github.com/NevaMind-AI/memU/commit/d3d1de1d9b0f45d9b14479cbaa4462458b172005))\n* add Sealos support agent use case (Track G) ([#255](https://github.com/NevaMind-AI/memU/issues/255)) ([8fbdf3c](https://github.com/NevaMind-AI/memU/commit/8fbdf3c301f74f2aa85604837e00bb305b8801ec))\n* integrate LazyLLM to provide more llm services ([#265](https://github.com/NevaMind-AI/memU/issues/265)) ([c03f639](https://github.com/NevaMind-AI/memU/commit/c03f639677d6c897b75dfe28d0cd92d5b5270957))\n* **integrations:** Add LangGraph Adapter for MemU (Track A) ([#258](https://github.com/NevaMind-AI/memU/issues/258)) ([50b5502](https://github.com/NevaMind-AI/memU/commit/50b5502ebcacd86401f98b1bb7e5a6577fab7126))\n* **llm:** add Grok (xAI) integration ([#179](https://github.com/NevaMind-AI/memU/issues/179)) ([#236](https://github.com/NevaMind-AI/memU/issues/236)) ([1e16309](https://github.com/NevaMind-AI/memU/commit/1e1630948af535f8ed9d608e6c4f9d2748d4ce8e))\n* Openrouter integration as backend provider ([#182](https://github.com/NevaMind-AI/memU/issues/182)) ([cba667a](https://github.com/NevaMind-AI/memU/commit/cba667a56daca5093c9b79a598c7d2ffda813756))\n\n\n### Bug Fixes\n\n* memory type & proactive example ([#272](https://github.com/NevaMind-AI/memU/issues/272)) ([710f14d](https://github.com/NevaMind-AI/memU/commit/710f14d4b171c5cde609a9dc2caf454681ab94b3))\n* proactive examples ([#273](https://github.com/NevaMind-AI/memU/issues/273)) ([603ae12](https://github.com/NevaMind-AI/memU/commit/603ae122b94741bb350656086960c4e2bb868c2a))\n\n\n### Documentation\n\n* Add link to memU bot ([#276](https://github.com/NevaMind-AI/memU/issues/276)) ([2f84231](https://github.com/NevaMind-AI/memU/commit/2f842311ed592c1a45cdb9e24f7a128da97a0a39))\n* multilingual readme ([#271](https://github.com/NevaMind-AI/memU/issues/271)) ([200f47a](https://github.com/NevaMind-AI/memU/commit/200f47a15ff7d05fc435abe1d2cefbb3774548fe))\n* Update memU bot link to include full URL ([#277](https://github.com/NevaMind-AI/memU/issues/277)) ([6874eaf](https://github.com/NevaMind-AI/memU/commit/6874eaf278e910d07a4427dd4c06f9c4c21283ce))\n* update readme ([#266](https://github.com/NevaMind-AI/memU/issues/266)) ([16ae534](https://github.com/NevaMind-AI/memU/commit/16ae534675a5e0711256a5e2147b9190fd8b2524))\n* update README ([#270](https://github.com/NevaMind-AI/memU/issues/270)) ([b531d39](https://github.com/NevaMind-AI/memU/commit/b531d39e5538449b31cb212aea1deea24e12180f))\n\n## [1.2.0](https://github.com/NevaMind-AI/memU/compare/v1.1.2...v1.2.0) (2026-01-14)\n\n\n### Features\n\n* clear memory ([#239](https://github.com/NevaMind-AI/memU/issues/239)) ([7da36da](https://github.com/NevaMind-AI/memU/commit/7da36da57d7013df213e537b1f238f5a526e69d9))\n* **database:** add SQLite storage backend ([#201](https://github.com/NevaMind-AI/memU/issues/201)) ([4b899d7](https://github.com/NevaMind-AI/memU/commit/4b899d7b768fcd48b6456234800033e3b2e3173c))\n* improve issue template ([#199](https://github.com/NevaMind-AI/memU/issues/199)) ([abe0f1b](https://github.com/NevaMind-AI/memU/commit/abe0f1b77d5f8294ff45c9f57f5aeb6de33977d6))\n* optimize topk pick function ([#196](https://github.com/NevaMind-AI/memU/issues/196)) ([b474c54](https://github.com/NevaMind-AI/memU/commit/b474c54a7eea06ba56844d6482d5e78b3cb3f020))\n* workflow step interceptor ([#240](https://github.com/NevaMind-AI/memU/issues/240)) ([bf2ac96](https://github.com/NevaMind-AI/memU/commit/bf2ac96f1cb26b196f27b543d4c3e157f408f343))\n\n\n### Bug Fixes\n\n* allow nullable resource_id in Postgres items ([#232](https://github.com/NevaMind-AI/memU/issues/232)) ([ae2ffbb](https://github.com/NevaMind-AI/memU/commit/ae2ffbb0bcd69170875a47cebf322b71a15f4bd3))\n* correct coverage source to track memu package instead of tests ([#220](https://github.com/NevaMind-AI/memU/issues/220)) ([2460bbd](https://github.com/NevaMind-AI/memU/commit/2460bbd2a2c4df5b3664db81d89886b926b51031))\n* correct typo in PromptBlock class label attribute ([#231](https://github.com/NevaMind-AI/memU/issues/231)) ([d69053c](https://github.com/NevaMind-AI/memU/commit/d69053cece3467dd7c8cbf0634e72447649095f7))\n* default embed size ([#192](https://github.com/NevaMind-AI/memU/issues/192)) ([144fd32](https://github.com/NevaMind-AI/memU/commit/144fd32cfd6917b08f29cf56f251f239c360d2ae))\n* readme partners link & github issue link ([#198](https://github.com/NevaMind-AI/memU/issues/198)) ([849f881](https://github.com/NevaMind-AI/memU/commit/849f881f004b0772f372069e40f937731c114c89))\n\n\n### Documentation\n\n* add Chinese translation of README ([#212](https://github.com/NevaMind-AI/memU/issues/212)) ([d15ecf9](https://github.com/NevaMind-AI/memU/commit/d15ecf97647d8fd9f1d2ae95008a08bbba93a44a))\n* add How to Contribute section to README ([#215](https://github.com/NevaMind-AI/memU/issues/215)) ([bf44de7](https://github.com/NevaMind-AI/memU/commit/bf44de722965fd611c0dc585712eac587caa9e6c))\n* **readme:** update Chinese README with fixes and improvements ([#221](https://github.com/NevaMind-AI/memU/issues/221)) ([75a8f65](https://github.com/NevaMind-AI/memU/commit/75a8f651bb4b491ab5b0363de789c0dcd0291287))\n\n## [1.1.2](https://github.com/NevaMind-AI/memU/compare/v1.1.1...v1.1.2) (2026-01-08)\n\n\n### Bug Fixes\n\n* custom memory type default prompt ([#169](https://github.com/NevaMind-AI/memU/issues/169)) ([5a0032f](https://github.com/NevaMind-AI/memU/commit/5a0032fc0f29229524d0356d454a3f5991e04f7b))\n* prompt & item fallback ([#183](https://github.com/NevaMind-AI/memU/issues/183)) ([bc95ade](https://github.com/NevaMind-AI/memU/commit/bc95adeb26789104c0bbe199e126cf05def27941))\n\n\n### Documentation\n\n* add docs folder ([#181](https://github.com/NevaMind-AI/memU/issues/181)) ([919d2ca](https://github.com/NevaMind-AI/memU/commit/919d2caef23107d539c36f30c44d4fb5aa38b324))\n* fix issue template dropdown ([#167](https://github.com/NevaMind-AI/memU/issues/167)) ([14d0333](https://github.com/NevaMind-AI/memU/commit/14d03331ed14f1e593fdebbf561189a3775651be))\n* issue template fix ([#165](https://github.com/NevaMind-AI/memU/issues/165)) ([5d91237](https://github.com/NevaMind-AI/memU/commit/5d912372e1bf70d7ff573cd5b92e917118048e25))\n\n## [1.1.1](https://github.com/NevaMind-AI/memU/compare/v1.1.0...v1.1.1) (2026-01-07)\n\n\n### Bug Fixes\n\n* ensure both Linux x86_64 and ARM64 wheels are built ([#162](https://github.com/NevaMind-AI/memU/issues/162)) ([51c9ea4](https://github.com/NevaMind-AI/memU/commit/51c9ea4335a1e9eac227c79783d7aa8cca2b883b))\n\n\n### Documentation\n\n* add custom LLM and embedding configuration guide ([#160](https://github.com/NevaMind-AI/memU/issues/160)) ([29c414a](https://github.com/NevaMind-AI/memU/commit/29c414a84d1c9c2f989e2165d0b6508c3fe62862))\n* add template ([#158](https://github.com/NevaMind-AI/memU/issues/158)) ([b79a78d](https://github.com/NevaMind-AI/memU/commit/b79a78d25b96e670888c18b8f09277db33803865))\n\n## [1.1.0](https://github.com/NevaMind-AI/memU/compare/v1.0.1...v1.1.0) (2026-01-07)\n\n\n### Features\n\n* add Linux ARM64 (aarch64) build target ([#156](https://github.com/NevaMind-AI/memU/issues/156)) ([0c90fcf](https://github.com/NevaMind-AI/memU/commit/0c90fcfb19fc3e91b951f4ba7454798e4b83e42c))\n\n\n### Bug Fixes\n\n* llm client profile & wrapper ([#157](https://github.com/NevaMind-AI/memU/issues/157)) ([e55c668](https://github.com/NevaMind-AI/memU/commit/e55c66847eac3ceaf276587d58b76d953d7be9f8))\n\n\n### Documentation\n\n* remove legacy docs ([#154](https://github.com/NevaMind-AI/memU/issues/154)) ([11fda41](https://github.com/NevaMind-AI/memU/commit/11fda41dda8a5c38b790d3c2fb7053b57b16606e))\n\n## [1.0.1](https://github.com/NevaMind-AI/memU/compare/v1.0.0...v1.0.1) (2026-01-06)\n\n\n### Bug Fixes\n\n* get embedding client ([#152](https://github.com/NevaMind-AI/memU/issues/152)) ([76716a4](https://github.com/NevaMind-AI/memU/commit/76716a4f00127a3cc21444996f32a9cf810c9282))\n* Readme incomplete ([#148](https://github.com/NevaMind-AI/memU/issues/148)) ([f8bc748](https://github.com/NevaMind-AI/memU/commit/f8bc748b9ebfb652ca8a5e06edbbe7eb648ed4f6))\n\n## [1.0.0](https://github.com/NevaMind-AI/memU/compare/v0.9.0...v1.0.0) (2026-01-05)\n\n\n### ⚠ BREAKING CHANGES\n\n* v1.0.0 ([#147](https://github.com/NevaMind-AI/memU/issues/147))\n\n### Bug Fixes\n\n* v1.0.0 ([#147](https://github.com/NevaMind-AI/memU/issues/147)) ([23f37ee](https://github.com/NevaMind-AI/memU/commit/23f37ee403ecaf88b48af0144e3d701c22ccfddd))\n\n\n### Documentation\n\n* add readme cloud api ([#144](https://github.com/NevaMind-AI/memU/issues/144)) ([42fd5ba](https://github.com/NevaMind-AI/memU/commit/42fd5babe6748ef54254cf114a54bdefea304f07))\n* fix api doc link ([#146](https://github.com/NevaMind-AI/memU/issues/146)) ([23ce7d1](https://github.com/NevaMind-AI/memU/commit/23ce7d19b1d68f4fd0a5a5ac6b756f69fede8d09))\n\n## [0.9.0](https://github.com/NevaMind-AI/memU/compare/v0.8.0...v0.9.0) (2026-01-04)\n\n\n### Features\n\n* add GitHub issue templates (bug report, designer feedback, feat… ([#132](https://github.com/NevaMind-AI/memU/issues/132)) ([aee22ee](https://github.com/NevaMind-AI/memU/commit/aee22ee94f275749f69367b83a02a2e819cfd001))\n* add LLM wrapper and interceptors for LLM calls ([#131](https://github.com/NevaMind-AI/memU/issues/131)) ([416e102](https://github.com/NevaMind-AI/memU/commit/416e1029e7752f173c133ebc83bc42801a313059))\n* patch & crud workflows ([#127](https://github.com/NevaMind-AI/memU/issues/127)) ([3cd3dc6](https://github.com/NevaMind-AI/memU/commit/3cd3dc65ae9488207ff8fb0c81e4adb0d22c0f91))\n\n\n### Bug Fixes\n\n* category summary & category user scopes ([#125](https://github.com/NevaMind-AI/memU/issues/125)) ([a24efd5](https://github.com/NevaMind-AI/memU/commit/a24efd57df2e305fa3eae8622ea889318979c2f7))\n* config & resource caption ([#142](https://github.com/NevaMind-AI/memU/issues/142)) ([ea4be13](https://github.com/NevaMind-AI/memU/commit/ea4be1396c0f55b02d706819f6c2b4d5c6e68be8))\n* remove duplicate and unnecessary issue templates ([#133](https://github.com/NevaMind-AI/memU/issues/133)) ([559ec14](https://github.com/NevaMind-AI/memU/commit/559ec14d2561ac09162bbb93f178e0f74cc58b23))\n\n\n### Documentation\n\n* fix README and CONTRIBUTING to match actual codebase ([#130](https://github.com/NevaMind-AI/memU/issues/130)) ([65d7ef4](https://github.com/NevaMind-AI/memU/commit/65d7ef414563395c6cfa0a75343864671c11b62d))\n* modify readme.md 0102 ([#136](https://github.com/NevaMind-AI/memU/issues/136)) ([f114ee4](https://github.com/NevaMind-AI/memU/commit/f114ee46c8639b256472a8e989695b2f2215f4d4))\n\n## [0.8.0](https://github.com/NevaMind-AI/memU/compare/v0.7.0...v0.8.0) (2025-12-24)\n\n\n### Features\n\n* add configurable batch_size for embedding API calls ([#114](https://github.com/NevaMind-AI/memU/issues/114)) ([060067a](https://github.com/NevaMind-AI/memU/commit/060067a5ebe8ad36c4ca4ad2cc6033303c3f1a36))\n* add conversation created at ([#120](https://github.com/NevaMind-AI/memU/issues/120)) ([825df56](https://github.com/NevaMind-AI/memU/commit/825df5640037fa2345c3153c3df89174059551b3))\n* add workflow implementation and postgres store ([#122](https://github.com/NevaMind-AI/memU/issues/122)) ([a175811](https://github.com/NevaMind-AI/memU/commit/a1758110f859f3725960d308fa0344a1056be52c))\n\n\n### Bug Fixes\n\n* example 3 output ([#117](https://github.com/NevaMind-AI/memU/issues/117)) ([65ef7c6](https://github.com/NevaMind-AI/memU/commit/65ef7c6a497047f0f86938cf77ee4602a630216b))\n* postgres model definitions & database initialization ([#124](https://github.com/NevaMind-AI/memU/issues/124)) ([7d5e0cb](https://github.com/NevaMind-AI/memU/commit/7d5e0cb02837a6e2071d3c3344ef6dad613f9a43))\n\n\n### Documentation\n\n* Add memU-experiment link to README ([#119](https://github.com/NevaMind-AI/memU/issues/119)) ([2d908e1](https://github.com/NevaMind-AI/memU/commit/2d908e17ee2e15b95d4eb2f35dd09f924d57f8cf))\n* clearer introduction and new agent examples ([#115](https://github.com/NevaMind-AI/memU/issues/115)) ([da27875](https://github.com/NevaMind-AI/memU/commit/da2787536d91490f7a96a0c7379abd1ad1c6d9ac))\n\n## [0.7.0](https://github.com/NevaMind-AI/memU/compare/v0.6.0...v0.7.0) (2025-12-05)\n\n\n### Features\n\n* add user model and user context store ([#113](https://github.com/NevaMind-AI/memU/issues/113)) ([7c37fb1](https://github.com/NevaMind-AI/memU/commit/7c37fb166b0a85bf7c89d0b109cbaa882fa80064))\n* add valcano model support ([#110](https://github.com/NevaMind-AI/memU/issues/110)) ([704c302](https://github.com/NevaMind-AI/memU/commit/704c3024946e8d4a1d1b13ffef33126bb68bd9c4))\n\n\n### Bug Fixes\n\n* resource caption miss problem ([#111](https://github.com/NevaMind-AI/memU/issues/111)) ([85586f5](https://github.com/NevaMind-AI/memU/commit/85586f52d6bd6355ae440ad06d8cc6779c689ce8))\n\n\n### Documentation\n\n* fix example file path ([#105](https://github.com/NevaMind-AI/memU/issues/105)) ([21aad6a](https://github.com/NevaMind-AI/memU/commit/21aad6a7f070e7666ac6a41c91980a4fa9696918))\n* fix readme test case ([#107](https://github.com/NevaMind-AI/memU/issues/107)) ([228306c](https://github.com/NevaMind-AI/memU/commit/228306c897402c633f57c7aa31e8c6cd4e995d5d))\n* highlight OpenAI key ([#106](https://github.com/NevaMind-AI/memU/issues/106)) ([5b6ce54](https://github.com/NevaMind-AI/memU/commit/5b6ce54def5aa7743a85bec629da65bfa9d8333b))\n* highlight readme openai key ([#103](https://github.com/NevaMind-AI/memU/issues/103)) ([fc4154a](https://github.com/NevaMind-AI/memU/commit/fc4154a707220ced4359e7296218451c43cf0681))\n\n## [0.6.0](https://github.com/NevaMind-AI/memU/compare/v0.5.0...v0.6.0) (2025-11-20)\n\n\n### Features\n\n* retrieve args change conversation_history to queries ([#98](https://github.com/NevaMind-AI/memU/issues/98)) ([6370c6e](https://github.com/NevaMind-AI/memU/commit/6370c6e968c9d0922120bf2a41e8b4206bab87cb))\n\n## [0.5.0](https://github.com/NevaMind-AI/memU/compare/v0.4.0...v0.5.0) (2025-11-18)\n\n\n### Features\n\n* add usecase examples ([#94](https://github.com/NevaMind-AI/memU/issues/94)) ([47b5b39](https://github.com/NevaMind-AI/memU/commit/47b5b390e065ccac1cd2173fa2d6c41549e01063))\n\n## [0.4.0](https://github.com/NevaMind-AI/memU/compare/v0.3.0...v0.4.0) (2025-11-18)\n\n\n### Features\n\n* add non-RAG retrieve solution ([#84](https://github.com/NevaMind-AI/memU/issues/84)) ([fb96e54](https://github.com/NevaMind-AI/memU/commit/fb96e5405f1c0e3477929b7d9874e624dd0453cb))\n\n\n### Bug Fixes\n\n* correct binary name after making a release ([#91](https://github.com/NevaMind-AI/memU/issues/91)) ([0fa721a](https://github.com/NevaMind-AI/memU/commit/0fa721aaae294c6f230221af11084a0a6c1f478d))\n\n\n### Documentation\n\n* fix several words in README ([#89](https://github.com/NevaMind-AI/memU/issues/89)) ([1e3baf9](https://github.com/NevaMind-AI/memU/commit/1e3baf92d5a08ec51a7eda3249bbd4b40530f56c))\n* revise README for clarity and roadmap inclusion ([#86](https://github.com/NevaMind-AI/memU/issues/86)) ([2235b09](https://github.com/NevaMind-AI/memU/commit/2235b099acc7e92ac52b2613fa731f85259d58fd))\n* update images and refine punctuation in README ([#88](https://github.com/NevaMind-AI/memU/issues/88)) ([cc375e8](https://github.com/NevaMind-AI/memU/commit/cc375e89a9b49bda0b7057eee696d74c4c1d9cee))\n\n## [0.3.0](https://github.com/NevaMind-AI/memU/compare/v0.2.2...v0.3.0) (2025-11-17)\n\n\n### Features\n\n* initialize the memorize and retrieve workflows with the new 3-layer architecture ([#81](https://github.com/NevaMind-AI/memU/issues/81)) ([4a2e86c](https://github.com/NevaMind-AI/memU/commit/4a2e86c0e8bc3e50c82c4fde33d07ad741c8a65e))\n\n\n### Documentation\n\n* add German translation for README ([f6d6ab1](https://github.com/NevaMind-AI/memU/commit/f6d6ab1a51b76bd4312542422aebac89410b8da9))\n* add German translation for README ([76c3fc4](https://github.com/NevaMind-AI/memU/commit/76c3fc4227a8e349fbcf1e5bfec29d68e984ff7a))\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to MemU\n\nThank you for your interest in contributing to MemU! This document provides guidelines and information for contributors.\n\n## 🌟 Ways to Contribute\n\nWe welcome all types of contributions:\n\n- 🐛 **Bug Reports** - Help us identify and fix issues\n- 💡 **Feature Requests** - Suggest new capabilities and improvements\n- 📝 **Documentation** - Improve guides, examples, and API docs\n- 🔧 **Code Contributions** - Add features, fix bugs, optimize performance\n- 🧪 **Testing** - Write tests, improve coverage, test edge cases\n- 🎨 **UI/UX** - Enhance user experience and interface design\n- 🌐 **Translations** - Help make MemU accessible globally\n- 📢 **Community** - Help others in discussions and support channels\n\n## 🚀 Quick Start for Contributors\n\n### Prerequisites\n- Python 3.13+\n- Git\n- [uv](https://github.com/astral-sh/uv) (Python package manager)\n- A code editor (VS Code recommended)\n\n### Development Setup\n\n```bash\n# 1. Fork the repository on GitHub\n# 2. Clone your fork locally\ngit clone https://github.com/YOUR_USERNAME/MemU.git\ncd MemU\n\n# 3. Install uv if you haven't already\ncurl -LsSf https://astral.sh/uv/install.sh | sh\n\n# 4. Install development dependencies and pre-commit hooks\nmake install\n\n# 5. Verify setup by running tests\nmake test\n```\n\n### Available Commands\n\n```bash\nmake install           # Create virtual environment and install dependencies with uv\nmake test              # Run tests with pytest and coverage\nmake check             # Run all checks (lock file, pre-commit, mypy, deptry)\n```\n\n## 🔧 Development Guidelines\n\n### Code Style\n- Follow **PEP 8** Python style guidelines\n- Use **Ruff** for code formatting and linting (line length: 120)\n- Use **type hints** for all functions and methods\n- Write **docstrings** for public APIs\n\n### Code Quality Standards\n- Maintain **test coverage > 80%**\n- All code must pass **linting** (ruff, mypy)\n- Use **meaningful variable and function names**\n- Keep functions **focused and small**\n- Follow **SOLID principles**\n\n### Testing\n```bash\n# Run all tests with coverage\nmake test\n\n# Run tests directly with pytest\nuv run python -m pytest\n\n# Run with coverage report\nuv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=html\n\n# Run specific test file\nuv run python -m pytest tests/rust_entry_test.py\n\n# Run tests with specific marker\nuv run python -m pytest -m \"not slow\"\n```\n\n## 📝 Submitting Changes\n\n### Before You Start\n1. **Search existing issues** to avoid duplicates\n2. **Create an issue** for new features or major changes\n3. **Discuss your approach** in the issue before implementing\n\n### Creating Issues\n\nWhen reporting bugs, please include:\n- **Environment details** (Python version, OS, MemU version)\n- **Reproduction steps** with minimal code example\n- **Expected vs actual behavior**\n- **Error messages** or stack traces\n\nFor feature requests, please describe:\n- **The problem** you're trying to solve\n- **Proposed solution** or approach\n- **Alternative solutions** you've considered\n- **Use cases** and examples\n\n### Pull Request Process\n\n1. **Create a feature branch**\n   ```bash\n   git checkout -b feature/amazing-feature\n   # or for bug fixes\n   git checkout -b bugfix/fix-memory-leak\n   ```\n\n2. **Make your changes**\n   - Write clear, descriptive commit messages\n   - Keep commits focused and atomic\n   - Add tests for new functionality\n   - Update documentation as needed\n\n3. **Test your changes**\n   ```bash\n   make test\n   make lint\n   make coverage\n   ```\n\n4. **Submit pull request**\n   - Use descriptive title and description\n   - Reference related issues (e.g., \"Fixes #123\")\n   - Include testing instructions\n   - Add screenshots for UI changes\n\n### Commit Message Format\n\nUse conventional commit format:\n\n```\ntype(scope): description\n\nExamples:\nfeat(memory): add semantic search functionality\nfix(llm): resolve OpenAI timeout issues\ndocs(readme): update installation instructions\ntest(agent): add unit tests for memory retrieval\nrefactor(core): restructure memory storage logic\n```\n\n**Types:**\n- `feat`: New feature\n- `fix`: Bug fix\n- `docs`: Documentation changes\n- `test`: Adding or fixing tests\n- `refactor`: Code restructuring without feature changes\n- `perf`: Performance improvements\n- `chore`: Maintenance tasks\n\n## 🎯 Current Priorities\n\nWe're currently focusing on:\n\n| Priority | Area | Description |\n|----------|------|-------------|\n| 🔥 **High** | Multi-modal Support | Images, audio, video memory processing |\n| 🔥 **High** | Performance | Memory retrieval optimization, caching |\n| 🔥 **High** | LLM Providers | Additional provider integrations |\n| 🟡 **Medium** | Enterprise Features | SSO, RBAC, audit logging |\n| 🟡 **Medium** | Mobile SDKs | React Native, Flutter support |\n| 🟢 **Low** | UI Improvements | Dashboard, memory visualization |\n\n## 🏷️ Issue Labels\n\n| Label | Description |\n|-------|-------------|\n| `good first issue` | Perfect for newcomers |\n| `help wanted` | Extra attention needed |\n| `bug` | Something isn't working |\n| `enhancement` | New feature request |\n| `documentation` | Improvements to docs |\n| `performance` | Performance optimization |\n| `breaking change` | Requires version bump |\n| `priority:high` | Urgent issues |\n| `priority:medium` | Important issues |\n| `priority:low` | Nice to have |\n\n## 📋 Code Review Process\n\n### For Contributors\n- Be open to feedback and constructive criticism\n- Respond promptly to review comments\n- Make requested changes in new commits (don't force push)\n- Ask questions if feedback is unclear\n\n### For Reviewers\n- Be constructive and respectful in feedback\n- Focus on code quality, maintainability, and project goals\n- Suggest improvements with explanations\n- Approve when ready, request changes when needed\n\n## 🔒 Security\n\n**Reporting Security Issues:**\n- **DO NOT** create public issues for security vulnerabilities\n- Email security issues privately to [contact@nevamind.ai](mailto:contact@nevamind.ai)\n- Include detailed reproduction steps and impact assessment\n- We'll acknowledge receipt within 24 hours\n\n## 📄 License and Attribution\n\nBy contributing to MemU, you agree that:\n- Your contributions will be licensed under the **Apache License 2.0**\n- You have the right to contribute the code/content\n- Your contribution doesn't violate any third-party rights\n\n## 🌍 Community Guidelines\n\n- Be respectful and inclusive\n- Follow our [Code of Conduct](CODE_OF_CONDUCT.md)\n- Help others learn and grow\n- Share knowledge and best practices\n- Celebrate diverse perspectives and experiences\n\n## 📞 Getting Help\n\n| Channel | Best For |\n|---------|----------|\n| 💬 [Discord](https://discord.gg/memu) | Real-time chat, quick questions |\n| 🗣️ [GitHub Discussions](https://github.com/NevaMind-AI/MemU/discussions) | Feature discussions, Q&A |\n| 🐛 [GitHub Issues](https://github.com/NevaMind-AI/MemU/issues) | Bug reports, feature requests |\n| 📧 [Email](mailto:contact@nevamind.ai) | Private inquiries |\n\n## 🎉 Recognition\n\nContributors are recognized in:\n- README.md contributors section\n- Release notes for significant contributions\n- Our [Contributors](https://github.com/NevaMind-AI/MemU/graphs/contributors) page\n\nThank you for helping make MemU better! 🚀\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"memu\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\nname = \"_core\"\n# \"cdylib\" is necessary to produce a shared library for Python to import from.\ncrate-type = [\"cdylib\"]\n\n[dependencies]\n# \"extension-module\" tells pyo3 we want to build an extension module (skips linking against libpython.so)\n# \"abi3-py313\" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.13\npyo3 = { version = \"0.27.1\", features = [\"extension-module\", \"abi3-py313\"] }\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and 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\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (which shall not include communication that is conspicuously\n      marked or otherwise designated in writing by the copyright owner\n      as \"Not a Contribution\").\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based upon (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and derivative works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control\n      systems, and issue tracking systems that are managed by, or on behalf\n      of, the Licensor for the purpose of discussing and improving the Work,\n      but excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution\".\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to use, reproduce, modify, merge, publish,\n      distribute, sublicense, and/or sell copies of the Work, and to\n      permit persons to whom the Work is furnished to do so, subject to\n      the following conditions:\n\n      The above copyright notice and this permission notice shall be\n      included in all copies or substantial portions of the Work.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, trademark, patent,\n          attribution and other notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright notice to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Support. You may choose to offer, and to\n      charge a fee for, warranty, support, indemnity or other liability\n      obligations and/or rights consistent with this License. However, in\n      accepting such obligations, You may act only on Your own behalf and\n      on Your sole responsibility, not on behalf of any other Contributor,\n      and only if You agree to indemnify, defend, and hold each Contributor\n      harmless for any liability incurred by, or claims asserted against,\n      such Contributor by reason of your accepting any such warranty or\n      support.\n\n   END OF TERMS AND CONDITIONS\n\n   Copyright 2024 MemU Team\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md\nrecursive-include memu *.py\nprune example\ninclude setup_postgres_env.sh\nexclude .env\nexclude .env.example\nexclude setup.py.backup\nexclude */__pycache__/*\nexclude *.pyc\nexclude *.pyo\nexclude .git/*\nexclude .github/*\nexclude server/*\nexclude docs/*\nexclude scripts/*\nexclude docker-compose.yml\nexclude Dockerfile\nexclude .dockerignore\nexclude PROJECT_RELEASE_SUMMARY.md\nexclude Makefile\nexclude .pre-commit-config.yaml\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: install\ninstall:\n\t@echo \"🚀 Creating virtual environment using uv\"\n\t@uv sync\n\t@uv run pre-commit install\n\n.PHONY: check\ncheck:\n\t@echo \"🚀 Checking lock file consistency with 'pyproject.toml'\"\n\t@uv lock --locked\n\t@echo \"🚀 Linting code: Running pre-commit\"\n\t@uv run pre-commit run -a\n\t@echo \"🚀 Static type checking: Running mypy\"\n\t@uv run mypy\n\t@echo \"🚀 Checking for obsolete dependencies: Running deptry\"\n\t@uv run deptry src\n\n\n.PHONY: test\ntest:\n\t@echo \"🚀 Testing code: Running pytest\"\n\t@uv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=xml\n"
  },
  {
    "path": "README.md",
    "content": "![MemU Banner](assets/banner.png)\n\n<div align=\"center\">\n\n# memU\n\n### 24/7 Always-On Proactive Memory for AI Agents\n\n[![PyPI version](https://badge.fury.io/py/memu-py.svg)](https://badge.fury.io/py/memu-py)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.com/invite/hQZntfGsbJ)\n[![Twitter](https://img.shields.io/badge/Twitter-Follow-1DA1F2?logo=x&logoColor=white)](https://x.com/memU_ai)\n\n<a href=\"https://trendshift.io/repositories/17374\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/17374\" alt=\"NevaMind-AI%2FmemU | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n**[English](readme/README_en.md) | [中文](readme/README_zh.md) | [日本語](readme/README_ja.md) | [한국어](readme/README_ko.md) | [Español](readme/README_es.md) | [Français](readme/README_fr.md)**\n\n</div>\n\n---\n\nmemU is a memory framework built for **24/7 proactive agents**.\nIt is designed for long-running use and greatly **reduces the LLM token cost** of keeping agents always online, making always-on, evolving agents practical in production systems.\nmemU **continuously captures and understands user intent**. Even without a command, the agent can tell what you are about to do and act on it by itself.\n\n---\n\n## 🤖 [OpenClaw (Moltbot, Clawdbot) Alternative](https://github.com/NevaMind-AI/memUBot)\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/memUbot.png\" />\n\n**[memU Bot](https://github.com/NevaMind-AI/memUBot)** — Now open source. The enterprise-ready OpenClaw. Your proactive AI assistant that remembers everything.\n\n- **Download-and-use and simple** to get started (one-click install, &lt; 3 min).\n- Builds long-term memory to **understand user intent** and act proactively (24/7).\n- **Cuts LLM token cost** with smaller context (~1/10 of comparable usage).\n\nTry now: [memu.bot](https://memu.bot) · Source: [memUBot on GitHub](https://github.com/NevaMind-AI/memUBot)\n\n---\n\n## 🗃️ Memory as File System, File System as Memory\n\nmemU treats **memory like a file system**—structured, hierarchical, and instantly accessible.\n\n| File System | memU Memory |\n|-------------|-------------|\n| 📁 Folders | 🏷️ Categories (auto-organized topics) |\n| 📄 Files | 🧠 Memory Items (extracted facts, preferences, skills) |\n| 🔗 Symlinks | 🔄 Cross-references (related memories linked) |\n| 📂 Mount points | 📥 Resources (conversations, documents, images) |\n\n**Why this matters:**\n- **Navigate memories** like browsing directories—drill down from broad categories to specific facts\n- **Mount new knowledge** instantly—conversations and documents become queryable memory\n- **Cross-link everything**—memories reference each other, building a connected knowledge graph\n- **Persistent & portable**—export, backup, and transfer memory like files\n\n```\nmemory/\n├── preferences/\n│   ├── communication_style.md\n│   └── topic_interests.md\n├── relationships/\n│   ├── contacts/\n│   └── interaction_history/\n├── knowledge/\n│   ├── domain_expertise/\n│   └── learned_skills/\n└── context/\n    ├── recent_conversations/\n    └── pending_tasks/\n```\n\nJust as a file system turns raw bytes into organized data, memU transforms raw interactions into **structured, searchable, proactive intelligence**.\n\n---\n\n## ⭐️ Star the repository\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/star.gif\" />\nIf you find memU useful or interesting, a GitHub Star ⭐️ would be greatly appreciated.\n\n---\n\n\n## ✨ Core Features\n\n| Capability | Description |\n|------------|-------------|\n| 🤖 **24/7 Proactive Agent** | Always-on memory agent that works continuously in the background—never sleeps, never forgets |\n| 🎯 **User Intention Capture** | Understands and remembers user goals, preferences, and context across sessions automatically |\n| 💰 **Cost Efficient** | Reduces long-running token costs by caching insights and avoiding redundant LLM calls |\n---\n\n## 🔄 How Proactive Memory Works\n\n```bash\n\ncd examples/proactive\npython proactive.py\n\n```\n\n---\n\n### Proactive Memory Lifecycle\n```\n┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\n│                                         USER QUERY                                               │\n└──────────────────────────────────────────────────────────────────────────────────────────────────┘\n                 │                                                           │\n                 ▼                                                           ▼\n┌────────────────────────────────────────┐         ┌────────────────────────────────────────────────┐\n│         🤖 MAIN AGENT                  │         │              🧠 MEMU BOT                       │\n│                                        │         │                                                │\n│  Handle user queries & execute tasks   │  ◄───►  │  Monitor, memorize & proactive intelligence   │\n├────────────────────────────────────────┤         ├────────────────────────────────────────────────┤\n│                                        │         │                                                │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  1. RECEIVE USER INPUT           │  │         │  │  1. MONITOR INPUT/OUTPUT                 │  │\n│  │     Parse query, understand      │  │   ───►  │  │     Observe agent interactions           │  │\n│  │     context and intent           │  │         │  │     Track conversation flow              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  2. PLAN & EXECUTE               │  │         │  │  2. MEMORIZE & EXTRACT                   │  │\n│  │     Break down tasks             │  │   ◄───  │  │     Store insights, facts, preferences   │  │\n│  │     Call tools, retrieve data    │  │  inject │  │     Extract skills & knowledge           │  │\n│  │     Generate responses           │  │  memory │  │     Update user profile                  │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  3. RESPOND TO USER              │  │         │  │  3. PREDICT USER INTENT                  │  │\n│  │     Deliver answer/result        │  │   ───►  │  │     Anticipate next steps                │  │\n│  │     Continue conversation        │  │         │  │     Identify upcoming needs              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  4. LOOP                         │  │         │  │  4. RUN PROACTIVE TASKS                  │  │\n│  │     Wait for next user input     │  │   ◄───  │  │     Pre-fetch relevant context           │  │\n│  │     or proactive suggestions     │  │  suggest│  │     Prepare recommendations              │  │\n│  └──────────────────────────────────┘  │         │  │     Update todolist autonomously         │  │\n│                                        │         │  └──────────────────────────────────────────┘  │\n└────────────────────────────────────────┘         └────────────────────────────────────────────────┘\n                 │                                                           │\n                 └───────────────────────────┬───────────────────────────────┘\n                                             ▼\n                              ┌──────────────────────────────┐\n                              │     CONTINUOUS SYNC LOOP     │\n                              │  Agent ◄──► MemU Bot ◄──► DB │\n                              └──────────────────────────────┘\n```\n\n---\n\n## 🎯 Proactive Use Cases\n\n### 1. **Information Recommendation**\n*Agent monitors interests and proactively surfaces relevant content*\n```python\n# User has been researching AI topics\nMemU tracks: reading history, saved articles, search queries\n\n# When new content arrives:\nAgent: \"I found 3 new papers on RAG optimization that align with\n        your recent research on retrieval systems. One author\n        (Dr. Chen) you've cited before published yesterday.\"\n\n# Proactive behaviors:\n- Learns topic preferences from browsing patterns\n- Tracks author/source credibility preferences\n- Filters noise based on engagement history\n- Times recommendations for optimal attention\n```\n\n### 2. **Email Management**\n*Agent learns communication patterns and handles routine correspondence*\n```python\n# MemU observes email patterns over time:\n- Response templates for common scenarios\n- Priority contacts and urgent keywords\n- Scheduling preferences and availability\n- Writing style and tone variations\n\n# Proactive email assistance:\nAgent: \"You have 12 new emails. I've drafted responses for 3 routine\n        requests and flagged 2 urgent items from your priority contacts.\n        Should I also reschedule tomorrow's meeting based on the\n        conflict John mentioned?\"\n\n# Autonomous actions:\n✓ Draft context-aware replies\n✓ Categorize and prioritize inbox\n✓ Detect scheduling conflicts\n✓ Summarize long threads with key decisions\n```\n\n### 3. **Trading & Financial Monitoring**\n*Agent tracks market context and user investment behavior*\n```python\n# MemU learns trading preferences:\n- Risk tolerance from historical decisions\n- Preferred sectors and asset classes\n- Response patterns to market events\n- Portfolio rebalancing triggers\n\n# Proactive alerts:\nAgent: \"NVDA dropped 5% in after-hours trading. Based on your past\n        behavior, you typically buy tech dips above 3%. Your current\n        allocation allows for $2,000 additional exposure while\n        maintaining your 70/30 equity-bond target.\"\n\n# Continuous monitoring:\n- Track price alerts tied to user-defined thresholds\n- Correlate news events with portfolio impact\n- Learn from executed vs. ignored recommendations\n- Anticipate tax-loss harvesting opportunities\n```\n\n\n...\n\n---\n\n## 🗂️ Hierarchical Memory Architecture\n\nMemU's three-layer system enables both **reactive queries** and **proactive context loading**:\n\n<img width=\"100%\" alt=\"structure\" src=\"assets/structure.png\" />\n\n| Layer | Reactive Use | Proactive Use |\n|-------|--------------|---------------|\n| **Resource** | Direct access to original data | Background monitoring for new patterns |\n| **Item** | Targeted fact retrieval | Real-time extraction from ongoing interactions |\n| **Category** | Summary-level overview | Automatic context assembly for anticipation |\n\n**Proactive Benefits:**\n- **Auto-categorization**: New memories self-organize into topics\n- **Pattern Detection**: System identifies recurring themes\n- **Context Prediction**: Anticipates what information will be needed next\n\n---\n\n## 🚀 Quick Start\n\n### Option 1: Cloud Version\n\nExperience proactive memory instantly:\n\n👉 **[memu.so](https://memu.so)** - Hosted service with 7×24 continuous learning\n\nFor enterprise deployment with custom proactive workflows, contact **info@nevamind.ai**\n\n#### Cloud API (v3)\n\n| Base URL | `https://api.memu.so` |\n|----------|----------------------|\n| Auth | `Authorization: Bearer YOUR_API_KEY` |\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `/api/v3/memory/memorize` | Register continuous learning task |\n| `GET` | `/api/v3/memory/memorize/status/{task_id}` | Check real-time processing status |\n| `POST` | `/api/v3/memory/categories` | List auto-generated categories |\n| `POST` | `/api/v3/memory/retrieve` | Query memory (supports proactive context loading) |\n\n📚 **[Full API Documentation](https://memu.pro/docs#cloud-version)**\n\n---\n\n### Option 2: Self-Hosted\n\n#### Installation\n```bash\npip install -e .\n```\n\n#### Basic Example\n\n> **Requirements**: Python 3.13+ and an OpenAI API key\n\n**Test Continuous Learning** (in-memory):\n```bash\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_inmemory.py\n```\n\n**Test with Persistent Storage** (PostgreSQL):\n```bash\n# Start PostgreSQL with pgvector\ndocker run -d \\\n  --name memu-postgres \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=memu \\\n  -p 5432:5432 \\\n  pgvector/pgvector:pg16\n\n# Run continuous learning test\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_postgres.py\n```\n\nBoth examples demonstrate **proactive memory workflows**:\n1. **Continuous Ingestion**: Process multiple files sequentially\n2. **Auto-Extraction**: Immediate memory creation\n3. **Proactive Retrieval**: Context-aware memory surfacing\n\nSee [`tests/test_inmemory.py`](tests/test_inmemory.py) and [`tests/test_postgres.py`](tests/test_postgres.py) for implementation details.\n\n---\n\n### Custom LLM and Embedding Providers\n\nMemU supports custom LLM and embedding providers beyond OpenAI. Configure them via `llm_profiles`:\n```python\nfrom memu import MemUService\n\nservice = MemUService(\n    llm_profiles={\n        # Default profile for LLM operations\n        \"default\": {\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n            \"api_key\": \"your_api_key\",\n            \"chat_model\": \"qwen3-max\",\n            \"client_backend\": \"sdk\"  # \"sdk\" or \"http\"\n        },\n        # Separate profile for embeddings\n        \"embedding\": {\n            \"base_url\": \"https://api.voyageai.com/v1\",\n            \"api_key\": \"your_voyage_api_key\",\n            \"embed_model\": \"voyage-3.5-lite\"\n        }\n    },\n    # ... other configuration\n)\n```\n\n---\n\n### OpenRouter Integration\n\nMemU supports [OpenRouter](https://openrouter.ai) as a model provider, giving you access to multiple LLM providers through a single API.\n\n#### Configuration\n```python\nfrom memu import MemoryService\n\nservice = MemoryService(\n    llm_profiles={\n        \"default\": {\n            \"provider\": \"openrouter\",\n            \"client_backend\": \"httpx\",\n            \"base_url\": \"https://openrouter.ai\",\n            \"api_key\": \"your_openrouter_api_key\",\n            \"chat_model\": \"anthropic/claude-3.5-sonnet\",  # Any OpenRouter model\n            \"embed_model\": \"openai/text-embedding-3-small\",  # Embedding model\n        },\n    },\n    database_config={\n        \"metadata_store\": {\"provider\": \"inmemory\"},\n    },\n)\n```\n\n#### Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `OPENROUTER_API_KEY` | Your OpenRouter API key from [openrouter.ai/keys](https://openrouter.ai/keys) |\n\n#### Supported Features\n\n| Feature | Status | Notes |\n|---------|--------|-------|\n| Chat Completions | Supported | Works with any OpenRouter chat model |\n| Embeddings | Supported | Use OpenAI embedding models via OpenRouter |\n| Vision | Supported | Use vision-capable models (e.g., `openai/gpt-4o`) |\n\n#### Running OpenRouter Tests\n```bash\nexport OPENROUTER_API_KEY=your_api_key\n\n# Full workflow test (memorize + retrieve)\npython tests/test_openrouter.py\n\n# Embedding-specific tests\npython tests/test_openrouter_embedding.py\n\n# Vision-specific tests\npython tests/test_openrouter_vision.py\n```\n\nSee [`examples/example_4_openrouter_memory.py`](examples/example_4_openrouter_memory.py) for a complete working example.\n\n---\n\n## 📖 Core APIs\n\n### `memorize()` - Continuous Learning Pipeline\n\nProcesses inputs in real-time and immediately updates memory:\n\n<img width=\"100%\" alt=\"memorize\" src=\"assets/memorize.png\" />\n\n```python\nresult = await service.memorize(\n    resource_url=\"path/to/file.json\",  # File path or URL\n    modality=\"conversation\",            # conversation | document | image | video | audio\n    user={\"user_id\": \"123\"}             # Optional: scope to a user\n)\n\n# Returns immediately with extracted memory:\n{\n    \"resource\": {...},      # Stored resource metadata\n    \"items\": [...],         # Extracted memory items (available instantly)\n    \"categories\": [...]     # Auto-updated category structure\n}\n```\n\n**Proactive Features:**\n- Zero-delay processing—memories available immediately\n- Automatic categorization without manual tagging\n- Cross-reference with existing memories for pattern detection\n\n### `retrieve()` - Dual-Mode Intelligence\n\nMemU supports both **proactive context loading** and **reactive querying**:\n\n<img width=\"100%\" alt=\"retrieve\" src=\"assets/retrieve.png\" />\n\n#### RAG-based Retrieval (`method=\"rag\"`)\n\nFast **proactive context assembly** using embeddings:\n\n- ✅ **Instant context**: Sub-second memory surfacing\n- ✅ **Background monitoring**: Can run continuously without LLM costs\n- ✅ **Similarity scoring**: Identifies most relevant memories automatically\n\n#### LLM-based Retrieval (`method=\"llm\"`)\n\nDeep **anticipatory reasoning** for complex contexts:\n\n- ✅ **Intent prediction**: LLM infers what user needs before they ask\n- ✅ **Query evolution**: Automatically refines search as context develops\n- ✅ **Early termination**: Stops when sufficient context is gathered\n\n#### Comparison\n\n| Aspect | RAG (Fast Context) | LLM (Deep Reasoning) |\n|--------|-------------------|---------------------|\n| **Speed** | ⚡ Milliseconds | 🐢 Seconds |\n| **Cost** | 💰 Embedding only | 💰💰 LLM inference |\n| **Proactive use** | Continuous monitoring | Triggered context loading |\n| **Best for** | Real-time suggestions | Complex anticipation |\n\n#### Usage\n```python\n# Proactive retrieval with context history\nresult = await service.retrieve(\n    queries=[\n        {\"role\": \"user\", \"content\": {\"text\": \"What are their preferences?\"}},\n        {\"role\": \"user\", \"content\": {\"text\": \"Tell me about work habits\"}}\n    ],\n    where={\"user_id\": \"123\"},  # Optional: scope filter\n    method=\"rag\"  # or \"llm\" for deeper reasoning\n)\n\n# Returns context-aware results:\n{\n    \"categories\": [...],     # Relevant topic areas (auto-prioritized)\n    \"items\": [...],          # Specific memory facts\n    \"resources\": [...],      # Original sources for traceability\n    \"next_step_query\": \"...\" # Predicted follow-up context\n}\n```\n\n**Proactive Filtering**: Use `where` to scope continuous monitoring:\n- `where={\"user_id\": \"123\"}` - User-specific context\n- `where={\"agent_id__in\": [\"1\", \"2\"]}` - Multi-agent coordination\n- Omit `where` for global context awareness\n\n---\n\n## 💡 Proactive Scenarios\n\n### Example 1: Always-Learning Assistant\n\nContinuously learns from every interaction without explicit memory commands:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_1_conversation_memory.py\n```\n\n**Proactive Behavior:**\n- Automatically extracts preferences from casual mentions\n- Builds relationship models from interaction patterns\n- Surfaces relevant context in future conversations\n- Adapts communication style based on learned preferences\n\n**Best for:** Personal AI assistants, customer support that remembers, social chatbots\n\n---\n\n### Example 2: Self-Improving Agent\n\nLearns from execution logs and proactively suggests optimizations:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_2_skill_extraction.py\n```\n\n**Proactive Behavior:**\n- Monitors agent actions and outcomes continuously\n- Identifies patterns in successes and failures\n- Auto-generates skill guides from experience\n- Proactively suggests strategies for similar future tasks\n\n**Best for:** DevOps automation, agent self-improvement, knowledge capture\n\n---\n\n### Example 3: Multimodal Context Builder\n\nUnifies memory across different input types for comprehensive context:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_3_multimodal_memory.py\n```\n\n**Proactive Behavior:**\n- Cross-references text, images, and documents automatically\n- Builds unified understanding across modalities\n- Surfaces visual context when discussing related topics\n- Anticipates information needs by combining multiple sources\n\n**Best for:** Documentation systems, learning platforms, research assistants\n\n---\n\n## 📊 Performance\n\nMemU achieves **92.09% average accuracy** on the Locomo benchmark across all reasoning tasks, demonstrating reliable proactive memory operations.\n\n<img width=\"100%\" alt=\"benchmark\" src=\"https://github.com/user-attachments/assets/6fec4884-94e5-4058-ad5c-baac3d7e76d9\" />\n\nView detailed experimental data: [memU-experiment](https://github.com/NevaMind-AI/memU-experiment)\n\n---\n\n## 🧩 Ecosystem\n\n| Repository | Description | Proactive Features |\n|------------|-------------|-------------------|\n| **[memU](https://github.com/NevaMind-AI/memU)** | Core proactive memory engine | 7×24 learning pipeline, auto-categorization |\n| **[memU-server](https://github.com/NevaMind-AI/memU-server)** | Backend with continuous sync | Real-time memory updates, webhook triggers |\n| **[memU-ui](https://github.com/NevaMind-AI/memU-ui)** | Visual memory dashboard | Live memory evolution monitoring |\n\n**Quick Links:**\n- 🚀 [Try MemU Cloud](https://app.memu.so/quick-start)\n- 📚 [API Documentation](https://memu.pro/docs)\n- 💬 [Discord Community](https://discord.com/invite/hQZntfGsbJ)\n\n---\n\n## 🤝 Partners\n\n<div align=\"center\">\n\n<a href=\"https://github.com/TEN-framework/ten-framework\"><img src=\"https://avatars.githubusercontent.com/u/113095513?s=200&v=4\" alt=\"Ten\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://openagents.org\"><img src=\"assets/partners/openagents.png\" alt=\"OpenAgents\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/milvus-io/milvus\"><img src=\"https://miro.medium.com/v2/resize:fit:2400/1*-VEGyAgcIBD62XtZWavy8w.png\" alt=\"Milvus\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://xroute.ai/\"><img src=\"assets/partners/xroute.png\" alt=\"xRoute\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://jaaz.app/\"><img src=\"assets/partners/jazz.png\" alt=\"Jazz\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/Buddie-AI/Buddie\"><img src=\"assets/partners/buddie.png\" alt=\"Buddie\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/bytebase/bytebase\"><img src=\"assets/partners/bytebase.png\" alt=\"Bytebase\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/LazyAGI/LazyLLM\"><img src=\"assets/partners/LazyLLM.png\" alt=\"LazyLLM\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://clawdchat.ai/\"><img src=\"assets/partners/Clawdchat.png\" alt=\"Clawdchat\" height=\"40\" style=\"margin: 10px;\"></a>\n\n</div>\n\n---\n\n## 🤝 How to Contribute\n\nWe welcome contributions from the community! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated.\n\n### Getting Started\n\nTo start contributing to MemU, you'll need to set up your development environment:\n\n#### Prerequisites\n- Python 3.13+\n- [uv](https://github.com/astral-sh/uv) (Python package manager)\n- Git\n\n#### Setup Development Environment\n```bash\n# 1. Fork and clone the repository\ngit clone https://github.com/YOUR_USERNAME/memU.git\ncd memU\n\n# 2. Install development dependencies\nmake install\n```\n\nThe `make install` command will:\n- Create a virtual environment using `uv`\n- Install all project dependencies\n- Set up pre-commit hooks for code quality checks\n\n#### Running Quality Checks\n\nBefore submitting your contribution, ensure your code passes all quality checks:\n```bash\nmake check\n```\n\nThe `make check` command runs:\n- **Lock file verification**: Ensures `pyproject.toml` consistency\n- **Pre-commit hooks**: Lints code with Ruff, formats with Black\n- **Type checking**: Runs `mypy` for static type analysis\n- **Dependency analysis**: Uses `deptry` to find obsolete dependencies\n\n### Contributing Guidelines\n\nFor detailed contribution guidelines, code standards, and development practices, please see [CONTRIBUTING.md](CONTRIBUTING.md).\n\n**Quick tips:**\n- Create a new branch for each feature or bug fix\n- Write clear commit messages\n- Add tests for new functionality\n- Update documentation as needed\n- Run `make check` before pushing\n\n---\n\n## 📄 License\n\n[Apache License 2.0](LICENSE.txt)\n\n---\n\n## 🌍 Community\n\n- **GitHub Issues**: [Report bugs & request features](https://github.com/NevaMind-AI/memU/issues)\n- **Discord**: [Join the community](https://discord.com/invite/hQZntfGsbJ)\n- **X (Twitter)**: [Follow @memU_ai](https://x.com/memU_ai)\n- **Contact**: info@nevamind.ai\n\n---\n\n<div align=\"center\">\n\n⭐ **Star us on GitHub** to get notified about new releases!\n\n</div>\n"
  },
  {
    "path": "docs/HACKATHON_ISSUE_DRAFT.md",
    "content": "# GitHub Issue Draft: Memory Types + Tool Memory\n\n## Title\n`[2026NewYearChallenge] Specialized Memory Types with Tool Learning`\n\n---\n\n## Description\n\n### What will this task implement?\n\nThis PR enhances MemU's memory type system to support specialized memory structures with type-specific metadata and introduces Tool Memory for agent self-improvement.\n\n**Current State:** MemU has a `memory_type` field with 5 types (profile, event, knowledge, behavior, skill) and uses different LLM prompts to extract each type. However, after extraction, all memories share the same storage schema - just `summary` and `embedding`. There's no type-specific metadata, no type-aware retrieval, and no way for agents to learn from their tool usage.\n\n**Enhancement:** Extend the memory system to support:\n- Type-specific metadata fields (e.g., `when_to_use` for smarter retrieval)\n- Tool Memory type for tracking tool execution history\n- Tool usage statistics for agent self-improvement\n- Type-aware retrieval filtering\n\n**Key Benefits:**\n- Agents can learn from their own tool usage patterns\n- Smarter retrieval based on memory context\n- Foundation for agents that improve over time\n- Better alignment with agentic application needs\n\n---\n\n## Requirements\n\n- [x] Type-specific metadata schema with `when_to_use` field\n- [x] Tool Memory implementation with execution tracking\n- [x] Tool statistics (success_rate, avg_time_cost, avg_score)\n- [ ] Type-aware retrieval filtering\n- [x] Tests for Tool Memory CRUD and statistics\n- [ ] Documentation and usage examples\n\n---\n\n## Review Criteria\n\n- Correctness: All tests pass, no regressions\n- Quality: Clean code, follows existing patterns\n- DX: Clear documentation and examples\n- Impact: Enables agent self-improvement use cases\n\n---\n\n## Implementation Summary\n\n### Files Modified:\n1. `src/memu/database/models.py` - Added `ToolCallResult` model, extended `MemoryItem` with `when_to_use`, `metadata`, `tool_calls` fields\n2. `src/memu/database/repositories/memory_item.py` - Updated interface with new fields\n3. `src/memu/database/inmemory/repositories/memory_item_repo.py` - Updated implementation\n4. `src/memu/database/postgres/repositories/memory_item_repo.py` - Updated implementation\n5. `src/memu/database/postgres/models.py` - Added JSON columns for new fields\n6. `src/memu/prompts/memory_type/__init__.py` - Added tool type\n7. `src/memu/prompts/memory_type/tool.py` - New prompt for tool memory extraction\n\n### Files Added:\n1. `tests/test_tool_memory.py` - 14 unit tests for Tool Memory feature\n\n---\n\n## Notes\n\nThis builds on MemU's existing memory type foundation while adding the specialized structures needed for agentic applications. The Tool Memory feature is particularly valuable for agents that need to learn which tools work best in different situations.\n"
  },
  {
    "path": "docs/HACKATHON_MAD_COMBOS.md",
    "content": "# 🔥🔥🔥 MAD COMBO OPTIONS FOR MEMU HACKATHON 🔥🔥🔥\n\n> **Goal:** Implement high-impact features that MemU is missing, sourced from competitor analysis of 7 memory repos (memoripy, memlayer, ReMe, memX, memphora-sdk, MemOS, memor).\n\n---\n\n## 📋 COMPLETE FEATURE GAP ANALYSIS\n\n### FEATURES MEMU IS MISSING (Deep Scan Results)\n\n#### FROM MEMORIPY:\n- ❌ `access_counts[]` - Track how often each memory is accessed\n- ❌ `timestamps[]` - Track when memory was created/last accessed\n- ❌ `decay_factor` - Exponential time-based decay: `np.exp(-decay_rate * time_diff)`\n- ❌ `reinforcement_factor` - Log-scaled access boost: `np.log1p(access_count)`\n- ❌ `adjusted_similarity` - `similarity * decay_factor * reinforcement_factor`\n- ❌ Short-term → Long-term memory promotion (when `access_count > 10`)\n- ❌ `nx.Graph()` concept associations (NetworkX graph)\n- ❌ `spreading_activation()` - Spread activation through concept graph\n- ❌ `cluster_interactions()` - KMeans clustering for hierarchical memory\n- ❌ `semantic_memory` clusters - Retrieve from semantic memory clusters\n\n#### FROM MEMLAYER:\n- ❌ `SalienceGate` - Filter what's worth saving vs noise\n- ❌ `SalienceMode.LOCAL` - Local ML model for salience\n- ❌ `SalienceMode.ONLINE` - OpenAI API for salience\n- ❌ `SalienceMode.LIGHTWEIGHT` - Keyword-based salience (no embeddings)\n- ❌ `SALIENT_PROTOTYPES` / `NON_SALIENT_PROTOTYPES` - Prototype sentences\n- ❌ `is_worth_saving()` - Determine if text should be saved\n- ❌ `CurationService` - Background memory decay/expiration\n- ❌ `_calculate_relevance()` - Score based on age, recency, attention\n- ❌ Auto-archive low-relevance memories\n- ❌ Auto-delete expired memories (`expiration_timestamp`)\n- ❌ `SchedulerService` - Background task scheduler\n- ❌ `get_due_tasks_for_user()` - Check for pending scheduled tasks\n- ❌ `ConsolidationService` - Background knowledge extraction\n- ❌ `analyze_and_extract_knowledge()` - Extract facts, entities, relationships\n- ❌ `NetworkXStorage` - Graph storage for entities/relationships\n- ❌ `add_entity()` / `add_relationship()` - Knowledge graph operations\n- ❌ `get_subgraph_context()` - Graph traversal for context\n- ❌ `find_matching_nodes()` - Fuzzy entity matching\n- ❌ `_find_canonical_entity()` - Entity deduplication\n- ❌ `_merge_entity_nodes()` - Merge duplicate entities\n- ❌ `importance_score` / `expiration_timestamp` metadata\n- ❌ `track_memory_access()` - Track when memories are accessed\n- ❌ Task reminders system (`add_task`, `get_pending_tasks`, `update_task_status`)\n\n#### FROM REME:\n- ❌ `UpdateMemoryFreqOp` - Increment frequency counter on recall\n- ❌ `metadata[\"freq\"]` - Frequency counter in metadata\n- ❌ `UpdateMemoryUtilityOp` - Increment utility score when useful\n- ❌ `metadata[\"utility\"]` - Utility score in metadata\n- ❌ `DeleteMemoryOp` - Delete based on freq/utility thresholds\n- ❌ `utility/freq < threshold` pruning - Prune low-value memories\n- ❌ **MEMORY TYPES:**\n  - ❌ `TaskMemory` - Task-related information\n  - ❌ `PersonalMemory` - Personal info with `target` and `reflection_subject`\n  - ❌ `ToolMemory` - Tool call execution history\n  - ❌ `ToolCallResult` - Record tool execution results with hash deduplication\n- ❌ `MemoryDeduplicationOp` - Remove duplicate memories using embedding similarity\n- ❌ `WorkingMemory` operations:\n  - ❌ `MessageCompressOp` - LLM-based compression for long conversations\n  - ❌ `MessageCompactOp` - Compact verbose tool messages\n  - ❌ `MessageOffloadOp` - Orchestrate compaction + compression\n  - ❌ `WorkingSummaryMode.COMPACT/COMPRESS/AUTO`\n- ❌ `UpdateMemory` tool - Update/edit existing memories\n- ❌ `session_memory_id` tracking - Track memories per session\n- ❌ Tool memory statistics (`avg_token_cost`, `success_rate`, `avg_time_cost`, `avg_score`)\n\n#### FROM MEMX:\n- ❌ `pubsub.py` - Real-time pub/sub system\n- ❌ `subscribe(key, websocket)` - WebSocket subscriptions\n- ❌ `publish(key, payload)` - Broadcast updates to subscribers\n- ❌ `set_value()` with timestamps - Last-write-wins with timestamps\n- ❌ Redis-backed shared memory - Multi-agent shared state\n- ❌ `register_schema()` / `validate_schema()` - JSON schema validation\n\n#### FROM MEMOS (MemOS):\n- ❌ **Memory Scheduler System:**\n  - ❌ `BaseScheduler` - Full task scheduling infrastructure\n  - ❌ `SchedulerDispatcher` - Parallel task dispatch\n  - ❌ `ScheduleTaskQueue` - Priority task queue\n  - ❌ `TaskStatusTracker` - Track task status in Redis\n  - ❌ `TaskPriorityLevel` - Priority levels for tasks\n- ❌ `MemoryMonitorItem` - Monitor memory with importance scores\n- ❌ `replace_working_memory()` - Replace working memory after reranking\n- ❌ `update_activation_memory()` - Update activation memory periodically\n- ❌ `transform_working_memories_to_monitors()` - Convert memories to monitors\n- ❌ `visibility` field - Public/private memory visibility\n- ❌ `confidence` score - Confidence level for memories\n- ❌ `status` field (activated/archived) - Memory activation status\n- ❌ `tags` field - Memory tagging system\n- ❌ `entities` extraction - Extract entities from memories\n\n#### FROM MEMPHORA-SDK:\n- ❌ `store_shared()` - Store shared memory for groups\n- ❌ Multi-agent crew memory - Shared memory for agent crews\n- ❌ Per-agent namespaces - Isolated memory per agent\n- ❌ Framework integrations (AutoGen, CrewAI, LangChain, LlamaIndex)\n\n---\n\n---\n\n## 🏆 COMBO 1: \"INTELLIGENT MEMORY LIFECYCLE\"\n\n**Theme:** Memory that learns, ages, and self-curates like human memory\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| Decay & Reinforcement | memoripy | 3 pts | LOW |\n| Frequency & Utility Tracking | ReMe | 3 pts | LOW |\n| Auto-Pruning Low-Value Memories | ReMe | 3 pts | LOW |\n\n**Total: 9 pts | LOW-MEDIUM effort**\n\n### Why it's MAD:\n\n```\nMemory accessed often → gets STRONGER (reinforcement)\nMemory ignored → gets WEAKER (decay)\nMemory with low utility/freq ratio → gets DELETED automatically\n\nResult: Self-healing, self-optimizing memory that mimics human forgetting!\n```\n\n### The Pitch:\n> \"MemU now has HUMAN-LIKE memory - it remembers what matters and forgets what doesn't!\"\n\n### Technical Implementation:\n\n```python\n# Decay formula (from memoripy)\ndecay_factor = np.exp(-decay_rate * time_diff)\nreinforcement_factor = np.log1p(access_count)\nadjusted_similarity = similarity * decay_factor * reinforcement_factor\n\n# Pruning logic (from ReMe)\nif freq >= freq_threshold:\n    if utility / freq < utility_threshold:\n        delete_memory(memory_id)\n```\n\n---\n\n## 🏆 COMBO 2: \"SMART MEMORY GATE\"\n\n**Theme:** Don't save garbage, only save gold\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| Salience Filtering | memlayer | 3-5 pts | MEDIUM |\n| Decay & Reinforcement | memoripy | 3 pts | LOW |\n| Background Curation Service | memlayer | 3 pts | MEDIUM |\n\n**Total: 9-11 pts | MEDIUM effort**\n\n### Why it's MAD:\n\n```\nINPUT: \"Hello!\" → BLOCKED (not salient)\nINPUT: \"My name is John, I work at Google\" → SAVED (salient)\nBACKGROUND: Old unused memories → AUTO-ARCHIVED\nRETRIEVAL: Frequently accessed → BOOSTED\n\nResult: Clean, high-quality memory that doesn't bloat!\n```\n\n### The Pitch:\n> \"MemU now has a BOUNCER - only important memories get in, garbage stays out!\"\n\n### Technical Implementation:\n\n```python\n# Salience Gate (from memlayer)\nclass SalienceGate:\n    SALIENT_PROTOTYPES = [\"My name is...\", \"I work at...\", \"The deadline is...\"]\n    NON_SALIENT_PROTOTYPES = [\"Hello\", \"Thanks\", \"Okay\", \"Got it\"]\n\n    def is_worth_saving(self, text: str) -> bool:\n        # Compare similarity to salient vs non-salient prototypes\n        salient_score = max_similarity(text, SALIENT_PROTOTYPES)\n        non_salient_score = max_similarity(text, NON_SALIENT_PROTOTYPES)\n        return salient_score > (non_salient_score + threshold)\n```\n\n---\n\n## 🏆 COMBO 3: \"KNOWLEDGE BRAIN\"\n\n**Theme:** Memory that understands relationships\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| Knowledge Graph | memlayer | 5 pts | HIGH |\n| Entity Extraction | memlayer | 3 pts | MEDIUM |\n| Graph Traversal Retrieval | memlayer | 3 pts | MEDIUM |\n\n**Total: 11 pts | HIGH effort**\n\n### Why it's MAD:\n\n```\nINPUT: \"John works at Google. Sarah is John's wife.\"\n\nGRAPH:\n    John --[works_at]--> Google\n    John --[married_to]--> Sarah\n\nQUERY: \"Who is related to Google?\"\nRESULT: John (works there), Sarah (married to John who works there)\n\nResult: Memory that REASONS about relationships!\n```\n\n### The Pitch:\n> \"MemU now has a BRAIN - it understands how things connect!\"\n\n### Technical Implementation:\n\n```python\n# Knowledge Graph (from memlayer)\nimport networkx as nx\n\nclass KnowledgeGraph:\n    def __init__(self):\n        self.graph = nx.Graph()\n\n    def add_entity(self, name: str, node_type: str):\n        self.graph.add_node(name, type=node_type)\n\n    def add_relationship(self, subject: str, predicate: str, obj: str):\n        self.graph.add_edge(subject, obj, relation=predicate)\n\n    def get_subgraph_context(self, entity: str, depth: int = 2):\n        # Traverse graph for related entities\n        return nx.ego_graph(self.graph, entity, radius=depth)\n```\n\n---\n\n## 🏆 COMBO 4: \"MEMORY EVOLUTION\" ⭐ TOP PICK\n\n**Theme:** Memory that evolves and improves itself\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| Salience Gate (LIGHTWEIGHT mode) | memlayer | 3 pts | LOW |\n| Decay & Reinforcement | memoripy | 3 pts | LOW |\n| Frequency & Utility | ReMe | 3 pts | LOW |\n| Auto-Pruning | ReMe | 3 pts | LOW |\n\n**Total: 12 pts | LOW-MEDIUM effort**\n\n### Why it's MAD:\n\n```\nSTAGE 1: Salience Gate filters noise at INPUT\nSTAGE 2: Decay/Reinforcement adjusts scores at RETRIEVAL\nSTAGE 3: Frequency/Utility tracks VALUE over time\nSTAGE 4: Auto-Pruning DELETES low-value memories\n\nResult: FULL LIFECYCLE MANAGEMENT - from birth to death!\n```\n\n### The Pitch:\n> \"MemU memories now have a LIFECYCLE - they're born, they grow, they age, they die!\"\n\n### Memory Lifecycle Diagram:\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                    MEMORY LIFECYCLE                              │\n├─────────────────────────────────────────────────────────────────┤\n│                                                                  │\n│   INPUT ──► [SALIENCE GATE] ──► SAVE or REJECT                  │\n│                    │                                             │\n│                    ▼                                             │\n│              ┌─────────┐                                         │\n│              │ MEMORY  │ ◄── access_count, last_accessed        │\n│              │  ITEM   │ ◄── freq, utility, salience_score      │\n│              └────┬────┘                                         │\n│                   │                                              │\n│         ┌────────┴────────┐                                      │\n│         ▼                 ▼                                      │\n│   [RETRIEVAL]       [BACKGROUND]                                 │\n│         │                 │                                      │\n│   decay_factor      auto_prune()                                 │\n│   reinforcement     if utility/freq < threshold                  │\n│         │                 │                                      │\n│         ▼                 ▼                                      │\n│   BOOSTED SCORE     DELETE MEMORY                                │\n│                                                                  │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## 📊 COMPARISON TABLE\n\n| Combo | Points | Effort | WOW Factor | Complexity | Recommendation |\n|-------|--------|--------|------------|------------|----------------|\n| 1. Intelligent Lifecycle | 9 | LOW | ⭐⭐⭐⭐ | LOW | ✅ SAFE BET |\n| 2. Smart Gate | 9-11 | MEDIUM | ⭐⭐⭐⭐ | MEDIUM | ✅ GOOD |\n| 3. Knowledge Brain | 11 | HIGH | ⭐⭐⭐⭐⭐ | HIGH | ⚠️ RISKY |\n| 4. Memory Evolution | 12 | LOW-MED | ⭐⭐⭐⭐⭐ | MEDIUM | 🏆 **BEST COMBO** |\n\n---\n\n## 🎯 RECOMMENDATION: COMBO 4 \"MEMORY EVOLUTION\"\n\n### Why This Combo Wins:\n\n1. **12 points** - highest point potential\n2. **LOW-MEDIUM effort** - achievable in hackathon timeframe\n3. **4 features that SYNERGIZE** - each builds on the other\n4. **UNIQUE story** - \"memory lifecycle\" is a compelling narrative\n5. **Easy to demo** - show memory being filtered, decaying, getting pruned\n\n### Implementation Order:\n\n```\nStep 1: Add fields to MemoryItem model\n        - access_count: int = 0\n        - last_accessed: datetime\n        - freq: int = 0\n        - utility: int = 0\n        - salience_score: float = 0.0\n\nStep 2: Implement lightweight salience gate (keyword-based, no ML)\n        - SALIENT_KEYWORDS list\n        - NON_SALIENT_KEYWORDS list\n        - is_worth_saving() function\n\nStep 3: Implement decay-aware retrieval\n        - Modify cosine_topk() to apply decay formula\n        - Update access_count and last_accessed on retrieval\n\nStep 4: Implement frequency/utility tracking\n        - Increment freq on every retrieval\n        - Increment utility when memory contributes to response\n\nStep 5: Implement auto-pruning\n        - Background check for low utility/freq ratio\n        - Delete memories below threshold\n```\n\n### Files to Modify:\n\n```\nmemU/src/memu/database/models.py          # Add new fields\nmemU/src/memu/database/inmemory/vector.py # Decay-aware retrieval\nmemU/src/memu/app/memorize.py             # Salience gate\nmemU/src/memu/app/retrieve.py             # Frequency/utility tracking\nmemU/src/memu/app/service.py              # Auto-pruning service\n```\n\n---\n\n## 📚 Reference Implementations\n\n### From memoripy (Decay & Reinforcement):\n- File: `prospects/memoripy/memoripy/memory_store.py`\n- Key functions: `retrieve()`, `classify_memory()`\n\n### From memlayer (Salience Gate):\n- File: `prospects/memlayer/memlayer/ml_gate.py`\n- Key class: `SalienceGate`\n\n### From ReMe (Frequency & Utility):\n- Files:\n  - `prospects/ReMe/reme_ai/vector_store/update_memory_freq_op.py`\n  - `prospects/ReMe/reme_ai/vector_store/update_memory_utility_op.py`\n  - `prospects/ReMe/reme_ai/vector_store/delete_memory_op.py`\n\n---\n\n## 🚀 Ready to Implement?\n\nChoose your combo and let's build! 🔥\n\n\n## 🏆 UPDATED MAD COMBOS (After Deep Scan)\n\n---\n\n## 🏆 COMBO 1: \"INTELLIGENT MEMORY LIFECYCLE\"\n\n**Theme:** Memory that learns, ages, and self-curates like human memory\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| Decay & Reinforcement | memoripy | 3 pts | LOW |\n| Frequency & Utility Tracking | ReMe | 3 pts | LOW |\n| Auto-Pruning Low-Value Memories | ReMe | 3 pts | LOW |\n\n**Total: 9 pts | LOW-MEDIUM effort**\n\n### Why it's MAD:\n\n```\nMemory accessed often → gets STRONGER (reinforcement)\nMemory ignored → gets WEAKER (decay)\nMemory with low utility/freq ratio → gets DELETED automatically\n\nResult: Self-healing, self-optimizing memory that mimics human forgetting!\n```\n\n### The Pitch:\n> \"MemU now has HUMAN-LIKE memory - it remembers what matters and forgets what doesn't!\"\n\n---\n\n## 🏆 COMBO 2: \"SMART MEMORY GATE\"\n\n**Theme:** Don't save garbage, only save gold\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| Salience Filtering | memlayer | 3-5 pts | MEDIUM |\n| Decay & Reinforcement | memoripy | 3 pts | LOW |\n| Background Curation Service | memlayer | 3 pts | MEDIUM |\n\n**Total: 9-11 pts | MEDIUM effort**\n\n### Why it's MAD:\n\n```\nINPUT: \"Hello!\" → BLOCKED (not salient)\nINPUT: \"My name is John, I work at Google\" → SAVED (salient)\nBACKGROUND: Old unused memories → AUTO-ARCHIVED\nRETRIEVAL: Frequently accessed → BOOSTED\n\nResult: Clean, high-quality memory that doesn't bloat!\n```\n\n### The Pitch:\n> \"MemU now has a BOUNCER - only important memories get in, garbage stays out!\"\n\n---\n\n## 🏆 COMBO 3: \"KNOWLEDGE BRAIN\"\n\n**Theme:** Memory that understands relationships\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| Knowledge Graph (NetworkX) | memlayer | 5 pts | HIGH |\n| Entity Extraction & Deduplication | memlayer | 3 pts | MEDIUM |\n| Graph Traversal Retrieval | memlayer | 3 pts | MEDIUM |\n\n**Total: 11 pts | HIGH effort**\n\n### Why it's MAD:\n\n```\nINPUT: \"John works at Google. Sarah is John's wife.\"\n\nGRAPH:\n    John --[works_at]--> Google\n    John --[married_to]--> Sarah\n\nQUERY: \"Who is related to Google?\"\nRESULT: John (works there), Sarah (married to John who works there)\n\nResult: Memory that REASONS about relationships!\n```\n\n### The Pitch:\n> \"MemU now has a BRAIN - it understands how things connect!\"\n\n---\n\n## 🏆 COMBO 4: \"MEMORY EVOLUTION\" ⭐ TOP PICK\n\n**Theme:** Memory that evolves and improves itself\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| Salience Gate (LIGHTWEIGHT mode) | memlayer | 3 pts | LOW |\n| Decay & Reinforcement | memoripy | 3 pts | LOW |\n| Frequency & Utility | ReMe | 3 pts | LOW |\n| Auto-Pruning | ReMe | 3 pts | LOW |\n\n**Total: 12 pts | LOW-MEDIUM effort**\n\n### Why it's MAD:\n\n```\nSTAGE 1: Salience Gate filters noise at INPUT\nSTAGE 2: Decay/Reinforcement adjusts scores at RETRIEVAL\nSTAGE 3: Frequency/Utility tracks VALUE over time\nSTAGE 4: Auto-Pruning DELETES low-value memories\n\nResult: FULL LIFECYCLE MANAGEMENT - from birth to death!\n```\n\n### The Pitch:\n> \"MemU memories now have a LIFECYCLE - they're born, they grow, they age, they die!\"\n\n---\n\n## 🏆 COMBO 5: \"MEMORY TYPES\" (NEW!)\n\n**Theme:** Different memory types for different purposes\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| TaskMemory type | ReMe | 3 pts | MEDIUM |\n| PersonalMemory type | ReMe | 3 pts | MEDIUM |\n| ToolMemory type | ReMe | 5 pts | HIGH |\n| Memory Deduplication | ReMe | 3 pts | MEDIUM |\n\n**Total: 14 pts | MEDIUM-HIGH effort**\n\n### Why it's MAD:\n\n```\nTaskMemory: \"Complete the report by Friday\"\n  - when_to_use: \"When user asks about deadlines\"\n  - content: \"Report due Friday\"\n\nPersonalMemory: \"User prefers dark mode\"\n  - target: \"user_preferences\"\n  - reflection_subject: \"ui_settings\"\n\nToolMemory: \"file_reader tool usage history\"\n  - tool_call_results: [...]\n  - statistics: {avg_token_cost, success_rate, avg_score}\n\nResult: Specialized memory for specialized tasks!\n```\n\n### The Pitch:\n> \"MemU now has SPECIALIZED MEMORY - task memory, personal memory, tool memory!\"\n\n---\n\n## 🏆 COMBO 6: \"WORKING MEMORY COMPRESSION\" (NEW!)\n\n**Theme:** Handle long conversations without losing context\n\n| Component | Source | Points | Effort |\n|-----------|--------|--------|--------|\n| MessageCompressOp | ReMe | 3 pts | MEDIUM |\n| MessageCompactOp | ReMe | 3 pts | MEDIUM |\n| MessageOffloadOp | ReMe | 3 pts | MEDIUM |\n\n**Total: 9 pts | MEDIUM effort**\n\n### Why it's MAD:\n\n```\nLONG CONVERSATION (50k tokens) → COMPRESS → STATE SNAPSHOT (5k tokens)\n\nModes:\n- COMPACT: Store full content externally, keep short previews\n- COMPRESS: LLM-based compression to generate dense summaries\n- AUTO: Compact first, then compress if needed\n\nResult: Handle infinite conversations without context overflow!\n```\n\n### The Pitch:\n> \"MemU now handles INFINITE conversations - compress, compact, never forget!\"\n\n---\n\n## 📊 UPDATED COMPARISON TABLE\n\n| Combo | Points | Effort | WOW Factor | Complexity | Recommendation |\n|-------|--------|--------|------------|------------|----------------|\n| 1. Intelligent Lifecycle | 9 | LOW | ⭐⭐⭐⭐ | LOW | ✅ SAFE BET |\n| 2. Smart Gate | 9-11 | MEDIUM | ⭐⭐⭐⭐ | MEDIUM | ✅ GOOD |\n| 3. Knowledge Brain | 11 | HIGH | ⭐⭐⭐⭐⭐ | HIGH | ⚠️ RISKY |\n| 4. Memory Evolution | 12 | LOW-MED | ⭐⭐⭐⭐⭐ | MEDIUM | 🏆 **BEST COMBO** |\n| 5. Memory Types | 14 | MED-HIGH | ⭐⭐⭐⭐⭐ | HIGH | 🔥 HIGH POINTS |\n| 6. Working Memory | 9 | MEDIUM | ⭐⭐⭐⭐ | MEDIUM | ✅ GOOD |\n\n---\n\n## 🎯 FINAL RECOMMENDATION\n\n### For MAX POINTS with REASONABLE EFFORT: **COMBO 4 \"MEMORY EVOLUTION\"**\n\n**Why?**\n1. **12 points** - highest point potential for effort\n2. **LOW-MEDIUM effort** - achievable in hackathon timeframe\n3. **4 features that SYNERGIZE** - each builds on the other\n4. **UNIQUE story** - \"memory lifecycle\" is a compelling narrative\n5. **Easy to demo** - show memory being filtered, decaying, getting pruned\n\n### Implementation Order:\n\n```\nStep 1: Add fields to MemoryItem model\n        - access_count: int = 0\n        - last_accessed: datetime\n        - freq: int = 0\n        - utility: int = 0\n        - salience_score: float = 0.0\n\nStep 2: Implement lightweight salience gate (keyword-based, no ML)\n        - SALIENT_KEYWORDS list\n        - NON_SALIENT_KEYWORDS list\n        - is_worth_saving() function\n\nStep 3: Implement decay-aware retrieval\n        - Modify cosine_topk() to apply decay formula\n        - Update access_count and last_accessed on retrieval\n\nStep 4: Implement frequency/utility tracking\n        - Increment freq on every retrieval\n        - Increment utility when memory contributes to response\n\nStep 5: Implement auto-pruning\n        - Background check for low utility/freq ratio\n        - Delete memories below threshold\n```\n\n### Files to Modify:\n\n```\nmemU/src/memu/database/models.py          # Add new fields\nmemU/src/memu/database/inmemory/vector.py # Decay-aware retrieval\nmemU/src/memu/app/memorize.py             # Salience gate\nmemU/src/memu/app/retrieve.py             # Frequency/utility tracking\nmemU/src/memu/app/service.py              # Auto-pruning service\n```\n\n---\n\n## 📚 Reference Implementations\n\n### From memoripy (Decay & Reinforcement):\n- File: `prospects/memoripy/memoripy/memory_store.py`\n- Key functions: `retrieve()`, `classify_memory()`\n\n### From memlayer (Salience Gate + Knowledge Graph):\n- File: `prospects/memlayer/memlayer/ml_gate.py` - SalienceGate\n- File: `prospects/memlayer/memlayer/storage/networkx.py` - Knowledge Graph\n- File: `prospects/memlayer/memlayer/services.py` - CurationService\n\n### From ReMe (Frequency & Utility + Memory Types):\n- File: `prospects/ReMe/reme_ai/vector_store/update_memory_freq_op.py`\n- File: `prospects/ReMe/reme_ai/vector_store/update_memory_utility_op.py`\n- File: `prospects/ReMe/reme_ai/vector_store/delete_memory_op.py`\n- File: `prospects/ReMe/reme_ai/schema/memory.py` - Memory types\n- File: `prospects/ReMe/reme_ai/summary/task/memory_deduplication_op.py`\n- File: `prospects/ReMe/reme_ai/summary/working/` - Working memory ops\n\n### From MemOS (Scheduler):\n- File: `prospects/MemOS/src/memos/mem_scheduler/base_scheduler.py`\n\n---\n\n## 🚀 Ready to Implement?\n\nChoose your combo and let's build! 🔥\n"
  },
  {
    "path": "docs/adr/0001-workflow-pipeline-architecture.md",
    "content": "# ADR 0001: Use Workflow Pipelines for Core Operations\n\n- Status: Accepted\n- Date: 2026-02-24\n\n## Context\n\nmemU has multiple high-level operations (`memorize`, `retrieve`, and CRUD/patch operations) that each require multi-stage execution, LLM calls, storage writes, and optional short-circuit behavior.\n\nA single monolithic function per operation would make these flows hard to extend, observe, and customize.\n\n## Decision\n\nModel each core operation as a named workflow pipeline composed of ordered `WorkflowStep` units.\n\n- Register pipelines centrally in `MemoryService` via `PipelineManager`\n- Validate required/produced state keys at pipeline registration/mutation time\n- Execute through a `WorkflowRunner` abstraction (`local` by default)\n- Support runtime customization by step-level config and structural mutation (insert/replace/remove)\n- Provide before/after/on_error step interceptors for instrumentation and control\n\n## Consequences\n\nPositive:\n\n- uniform execution model across memorize/retrieve/CRUD\n- explicit, inspectable stage boundaries\n- extension points for custom runners and step customization\n- easier interception and observability around stage execution\n\nNegative:\n\n- dict-based workflow state relies on key naming discipline\n- pipeline mutation can increase behavioral variance between deployments\n- more framework code compared to direct function calls\n"
  },
  {
    "path": "docs/adr/0002-pluggable-storage-and-vector-strategy.md",
    "content": "# ADR 0002: Use Pluggable Storage with Backend-Specific Vector Search\n\n- Status: Accepted\n- Date: 2026-02-24\n\n## Context\n\nmemU must support:\n\n- zero-setup local development\n- lightweight persisted deployments\n- production deployments that need scalable vector similarity\n\nNo single storage engine fits all three cases.\n\n## Decision\n\nAdopt repository-based storage abstraction behind a `Database` protocol, with selectable providers:\n\n- `inmemory`: in-process state, brute-force similarity\n- `sqlite`: file-based persistence, embeddings stored as JSON text, brute-force similarity\n- `postgres`: SQL persistence, pgvector-enabled similarity when configured\n\nVector behavior is backend-aware:\n\n- brute-force cosine search remains available for portability\n- Postgres can use pgvector distance queries when vector support is enabled\n- salience ranking (reinforcement/recency-aware) uses local scoring logic\n\n## Consequences\n\nPositive:\n\n- one service API works across local and production footprints\n- clear backend contracts through repository interfaces\n- predictable fallback behavior when native vector index is unavailable\n\nNegative:\n\n- duplicate repository logic across backends\n- behavior/performance differences between providers\n- SQLite and in-memory vector search does not scale as well as indexed pgvector\n"
  },
  {
    "path": "docs/adr/0003-user-scope-in-data-model.md",
    "content": "# ADR 0003: Model User Scope as First-Class Fields on Memory Records\n\n- Status: Accepted\n- Date: 2026-02-24\n\n## Context\n\nmemU retrieval and writes need scoped operation (for example per `user_id`, `agent_id`, or session) for multi-user and multi-agent scenarios.\n\nKeeping scope outside stored records would force ad-hoc filtering logic and weaken data isolation.\n\n## Decision\n\nEmbed scope directly into all persisted entities by merging a configurable `UserConfig.model` with core record models.\n\n- Scope fields are part of resource/category/item/relation models\n- Repositories accept `user_data` on writes and `where` filters on reads\n- API-level `where` filters are validated against configured scope fields before execution\n\n## Consequences\n\nPositive:\n\n- consistent filtering model across memorize/retrieve/CRUD APIs\n- backend-independent scoping semantics\n- supports multi-tenant and multi-agent patterns without separate storage stacks\n\nNegative:\n\n- schema/model generation complexity increases\n- schema and index shape can vary by chosen scope model\n- callers must keep `where` and `user` payloads aligned with configured scope fields\n"
  },
  {
    "path": "docs/adr/README.md",
    "content": "# Architecture Decision Records\n\n- [0001: Use Workflow Pipelines for Core Operations](0001-workflow-pipeline-architecture.md)\n- [0002: Use Pluggable Storage with Backend-Specific Vector Search](0002-pluggable-storage-and-vector-strategy.md)\n- [0003: Model User Scope as First-Class Fields on Memory Records](0003-user-scope-in-data-model.md)\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "# memU Architecture\n\n## Purpose and scope\n\nThis document describes the self-hosted `memu` Python package architecture as implemented in this repository.\n\nThe repository also describes a hosted Cloud product in `README.md`, but this document focuses on the local `MemoryService` runtime and its code paths.\n\n## System overview\n\nmemU follows the \"memory as file system\" concept from the README and implements it with three persistent layers:\n\n- `Resource`: raw source artifacts (conversation/document/image/video/audio)\n- `MemoryItem`: extracted atomic memories with embeddings\n- `MemoryCategory`: grouped topic summaries\n- `CategoryItem`: item-category relation edges\n\nAt runtime, `MemoryService` orchestrates ingestion, retrieval, and manual CRUD over these layers.\n\n```mermaid\nflowchart TD\n  A[\"Input Resource or Query\"] --> B[\"MemoryService\"]\n  B --> C[\"Workflow Pipelines\"]\n  C --> D[\"LLM Clients\"]\n  C --> E[\"Database Repositories\"]\n  E --> F[\"Resources\"]\n  E --> G[\"Memory Items\"]\n  E --> H[\"Memory Categories\"]\n  E --> I[\"Category Relations\"]\n```\n\n## Core runtime components\n\n### `MemoryService` as composition root\n\n`src/memu/app/service.py` constructs and owns:\n\n- typed configs (`LLMProfilesConfig`, `DatabaseConfig`, `MemorizeConfig`, `RetrieveConfig`, `UserConfig`)\n- storage backend (`build_database(...)`)\n- resource filesystem fetcher (`LocalFS`)\n- LLM client cache and wrappers\n- workflow and LLM interceptor registries\n- workflow runner (`local` by default, pluggable)\n- named workflow pipelines via `PipelineManager`\n\nPublic APIs are assembled by mixins:\n\n- `MemorizeMixin`: `memorize(...)`\n- `RetrieveMixin`: `retrieve(...)`\n- `CRUDMixin`: list/clear/create/update/delete memory operations\n\n### Workflow engine\n\nAll major operations execute as workflows (`WorkflowStep`) with:\n\n- explicit required/produced state keys\n- declared capability tags (`llm`, `vector`, `db`, `io`, `vision`)\n- per-step config (for profile selection)\n\n`PipelineManager` validates step dependencies at registration/mutation time and supports runtime pipeline revisioning (`config_step`, `insert_before/after`, `replace_step`, `remove_step`).\n\n`WorkflowRunner` is a protocol; default `LocalWorkflowRunner` executes sequentially with `run_steps(...)`.\n\n### Interception and observability hooks\n\nTwo interceptor systems exist:\n\n- workflow step interceptors: before/after/on_error around each step\n- LLM call interceptors: before/after/on_error around `chat/summarize/vision/embed/transcribe`\n\nLLM wrappers also extract best-effort usage metadata from raw provider responses.\n\n## Ingestion architecture (`memorize`)\n\n`memorize(...)` executes the `memorize` pipeline:\n\n1. `ingest_resource`: fetch local/remote resource into `blob_config.resources_dir` via `LocalFS`\n2. `preprocess_multimodal`: modality-specific preprocessing for conversation/document/audio (text-oriented path) and image/video (vision-oriented path)\n3. `extract_items`: per-memory-type LLM extraction into structured entries\n4. `dedupe_merge`: placeholder stage (currently pass-through)\n5. `categorize_items`: persist resource + memory items + item-category relations and embeddings\n6. `persist_index`: update category summaries; optionally persist item references\n7. `build_response`: return resource(s), items, categories, relations\n\nCategory bootstrap is lazy and scoped: categories are initialized when needed with embeddings, and mapped by normalized category name.\n\n## Retrieval architecture (`retrieve`)\n\n`retrieve(...)` chooses one of two pipelines from config:\n\n- `retrieve_rag` (embedding-driven ranking)\n- `retrieve_llm` (LLM-driven ranking)\n\nBoth use the same staged pattern:\n\n1. route intention + optional query rewrite\n2. category recall\n3. sufficiency check (optional)\n4. item recall\n5. sufficiency check (optional)\n6. resource recall\n7. response build\n\nKey behavior:\n\n- `where` filters are validated against `user_model` fields before querying\n- RAG path uses vector similarity (and optional salience ranking for items)\n- LLM path ranks IDs from formatted category/item/resource context\n- each stage can stop early if sufficiency check decides context is enough\n\n## Data and storage architecture\n\n### Repository contracts\n\nStorage is abstracted through a `Database` protocol with four repositories:\n\n- `ResourceRepo`\n- `MemoryItemRepo`\n- `MemoryCategoryRepo`\n- `CategoryItemRepo`\n\n### Backends\n\n`build_database(...)` selects backend by `database_config.metadata_store.provider`:\n\n- `inmemory`: in-process dict/list state\n- `sqlite`: SQLModel persistence, embeddings stored as JSON text, brute-force cosine search\n- `postgres`: SQLModel persistence with pgvector support (when enabled), local fallback ranking when needed\n\nFor Postgres, startup runs migration bootstrap and attempts `CREATE EXTENSION IF NOT EXISTS vector` in `ddl_mode=\"create\"`.\n\n### Scope model propagation\n\n`UserConfig.model` is merged into record/table models so scope fields (for example `user_id`) become first-class columns/attributes across resources, items, categories, and relations.\n\nThis is why `where` filters and `user_data` writes are consistently available across APIs.\n\n## LLM/provider architecture\n\nLLM access is profile-based (`llm_profiles`):\n\n- `default` profile for chat-like tasks\n- `embedding` profile for embedding tasks (auto-derived from default if not set)\n\nPer-step profile routing happens through step config (`chat_llm_profile`, `embed_llm_profile`, or `llm_profile`).\n\nClient backends:\n\n- `sdk`: official OpenAI SDK wrapper\n- `httpx`: provider-adapted HTTP backend (OpenAI, Doubao, Grok, OpenRouter)\n- `lazyllm_backend`: LazyLLM adapter\n\n## Integration surfaces\n\n- `memu.client.openai_wrapper`: opt-in OpenAI client wrapper that auto-retrieves memories and injects them into system context\n- `memu.integrations.langgraph`: LangChain/LangGraph tool adapter (`save_memory`, `search_memory`)\n\n## Current constraints and tradeoffs\n\n- workflow state is dict-based, so step contracts are validated by key names rather than static types\n- SQLite/inmemory vector search is brute-force (portable but less scalable)\n- category update quality and extraction quality are prompt/LLM dependent\n- some extension hooks exist as placeholders (for example dedupe/merge stage)\n\n## Related ADRs\n\n- `docs/adr/0001-workflow-pipeline-architecture.md`\n- `docs/adr/0002-pluggable-storage-and-vector-strategy.md`\n- `docs/adr/0003-user-scope-in-data-model.md`\n"
  },
  {
    "path": "docs/integrations/grok.md",
    "content": "# Grok (xAI) Integration\n\nMemU supports **Grok**, the AI model from xAI, as a first-class LLM provider.\n\n## Prerequisites\n\n1.  **xAI Account:** You need an active account with [xAI](https://x.ai/).\n2.  **API Key:** Obtain an API key from the [xAI Console](https://console.x.ai/).\n\n## Configuration\n\nTo enable Grok, you need to set the `XAI_API_KEY` environment variable.\n\n### Environment Variable\n\n```bash\nexport XAI_API_KEY=\"your-xai-api-key-here\"\n```\n\nPowerShell:\n\n```powershell\n$env:XAI_API_KEY=\"your-xai-api-key-here\"\n```\n\n## Usage\n\nTo use Grok as your LLM provider, switch the `provider` setting to `grok`. This can be done in your configuration file or when initializing the application.\n\n### Python Example\n\n```python\nfrom memu.app.settings import LLMConfig\n\n# Configure MemU to use Grok\nconfig = LLMConfig(\n    provider=\"grok\",\n    # The default API key env var is XAI_API_KEY\n    # The default model is grok-2-latest\n)\n\nprint(f\"Using provider: {config.provider}\")\nprint(f\"Base URL: {config.base_url}\")\nprint(f\"Chat Model: {config.chat_model}\")\n```\n\n## Models Supported\n\nWe currently support the following Grok models:\n\n*   **grok-2-latest** (Default)\n\nThe integration automatically sets the base URL to `https://api.x.ai/v1`.\n"
  },
  {
    "path": "docs/langgraph_integration.md",
    "content": "# MemU LangGraph Integration\n\nThe MemU LangGraph Integration provides a seamless adapter to expose MemU's powerful memory capabilities (`memorize` and `retrieve`) as standard [LangChain](https://python.langchain.com/) / [LangGraph](https://langchain-ai.github.io/langgraph/) tools. This allows your agents to persist information and recall it across sessions using MemU as the long-term memory backend.\n\n## Overview\n\nThis integration wraps the `MemoryService` and exposes two key tools:\n- **`save_memory`**: Persists text, conversation snippets, or facts associated with a user.\n- **`search_memory`**: Retrieves relevant memories based on semantic search queries.\n\nThese tools are fully typed and compatible with LangGraph's `prebuilt.ToolNode` and LangChain's agents.\n\n## Installation\n\nTo use this integration, you need to install the optional dependencies:\n\n```bash\nuv add langgraph langchain-core\n```\n\n## Quick Start\n\nHere is a complete example of how to initialize the MemU memory service and bind it to a LangGraph agent.\n\n```python\nimport asyncio\nimport os\nfrom memu.app.service import MemoryService\nfrom memu.integrations.langgraph import MemULangGraphTools\n\n# Ensure you have your configuration set (e.g., env vars for DB connection)\n# os.environ[\"MEMU_DATABASE_URL\"] = \"...\"\n\nasync def main():\n    # 1. Initialize MemoryService\n    memory_service = MemoryService()\n    # If your service requires async init (check your specific implementation):\n    # await memory_service.initialize()\n\n    # 2. Instantiate MemULangGraphTools\n    memu_tools = MemULangGraphTools(memory_service)\n\n    # Get the list of tools (BaseTool compatible)\n    tools = memu_tools.tools()\n\n    # 3. Example Usage: Manually invoking a tool\n    # In a real app, you would pass 'tools' to your LangGraph agent or StateGraph.\n\n    # Save a memory\n    save_tool = memu_tools.save_memory_tool()\n    print(\"Saving memory...\")\n    result = await save_tool.ainvoke({\n        \"content\": \"The user prefers dark mode.\",\n        \"user_id\": \"user_123\",\n        \"metadata\": {\"category\": \"preferences\"}\n    })\n    print(f\"Save Result: {result}\")\n\n    # Search for a memory\n    search_tool = memu_tools.search_memory_tool()\n    print(\"\\nSearching memory...\")\n    search_result = await search_tool.ainvoke({\n        \"query\": \"What are the user's preferences?\",\n        \"user_id\": \"user_123\"\n    })\n    print(f\"Search Result:\\n{search_result}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## API Reference\n\n### `MemULangGraphTools`\n\nThe main adapter class.\n\n```python\nclass MemULangGraphTools(memory_service: MemoryService)\n```\n\n#### `save_memory_tool() -> StructuredTool`\nReturns a tool named `save_memory`.\n- **Inputs**: `content` (str), `user_id` (str), `metadata` (dict, optional).\n- **Description**: Save a piece of information, conversation snippet, or memory for a user.\n\n#### `search_memory_tool() -> StructuredTool`\nReturns a tool named `search_memory`.\n- **Inputs**: `query` (str), `user_id` (str), `limit` (int, default=5), `metadata_filter` (dict, optional), `min_relevance_score` (float, default=0.0).\n- **Description**: Search for relevant memories or information for a user based on a query.\n\n## Troubleshooting\n\n### Import Errors\nIf you see an `ImportError` regarding `langchain_core` or `langgraph`:\n1. Ensure you have installed the extras: `uv add langgraph langchain-core` (or `pip install langgraph langchain-core`).\n2. Verify your virtual environment is active.\n"
  },
  {
    "path": "docs/providers/grok.md",
    "content": "# Grok (xAI) Provider\n\nmemU includes first-class support for [Grok](https://grok.x.ai/), allowing you to leverage xAI's powerful language models directly within your application.\n\n## Prerequisites\n\nTo use this provider, you must have an active xAI account.\n\n1.  Navigate to the [xAI Console](https://console.x.ai/).\n2.  Sign up or log in.\n3.  Create a new **API Key** in the API Keys section.\n\n## Configuration\n\nThe integration is designed to work out-of-the-box with minimal configuration.\n\n### Environment Variables\n\nSet the following environment variable in your `.env` file or system environment:\n\n```bash\nGROK_API_KEY=xai-YOUR_API_KEY_HERE\n```\n\n### Defaults\n\nWhen you select the `grok` provider, memU automatically configures the following defaults:\n\n*   **Base URL**: `https://api.x.ai/v1`\n*   **Model**: `grok-2-latest`\n\n## Usage Example\n\nYou can enable the Grok provider by setting the `provider` field to `\"grok\"` in your application configuration.\n\n### Using Python Configuration\n\n```python\nfrom memu.app.settings import LLMConfig\nfrom memu.app.service import MemoryService\n\n# Configure the LLM provider to use Grok\nllm_config = LLMConfig(provider=\"grok\")\n\n# Initialize the service\nservice = MemoryService(llm_config=llm_config)\nprint(f\"Service initialized with model: {llm_config.chat_model}\")\n# Output: Service initialized with model: grok-2-latest\n```\n\n## Troubleshooting\n\n### Connection Issues\nIf you are unable to connect to the xAI API:\n1.  Verify that your `GROK_API_KEY` is set correctly and has not expired.\n2.  Ensure that the `base_url` is resolving to `https://api.x.ai/v1`. If you have manual overrides in your settings, they might be conflicting with the default.\n\n### Model Availability\nIf you receive a `404` or \"Model not found\" error, xAI may have updated their model names. You can override the model manually in the config if needed:\n\n```python\nconfig = LLMConfig(\n    provider=\"grok\",\n    chat_model=\"grok-beta\" # Example override\n)\n```\n"
  },
  {
    "path": "docs/sealos-devbox-guide.md",
    "content": "# Deploying MemU on Sealos DevBox\n\nThis guide demonstrates how to build and deploy a **Personal AI Assistant with Long-Term Memory** using MemU on [Sealos DevBox](https://sealos.io/products/devbox).\n\n## Overview\n\nMemU enables AI agents to maintain persistent, structured memory across conversations. Combined with Sealos DevBox's 1-click cloud development environment, you can quickly build and deploy memory-enabled AI applications.\n\n**What we'll build:**\n- A FastAPI-based AI assistant that remembers user preferences and past conversations\n- Persistent memory storage using MemU's in-memory or PostgreSQL backend\n- Simple REST API for chat interactions\n- One-click deployment to production\n\n**Time to complete:** ~15 minutes\n\n## Prerequisites\n\n- [Sealos account](https://sealos.io) (free tier available)\n- OpenAI API key (or compatible provider like Nebius, Groq)\n\n## Step 1: Create a DevBox Environment\n\n1. Log in to [Sealos Dashboard](https://cloud.sealos.io)\n2. Navigate to **DevBox** module\n3. Click **Create New Project**\n4. Select **Python 3.11+** template\n5. Configure resources (recommended: 2 vCPU, 4GB RAM)\n6. Click **Create** - your environment will be ready in ~60 seconds\n\n## Step 2: Connect Your IDE\n\n1. In the DevBox project list, click the **VS Code** or **Cursor** button\n2. Your local IDE will open with a secure SSH connection to the cloud environment\n3. All code runs in the cloud, keeping your local machine free\n\n## Step 3: Set Up the Project\n\nOpen the terminal in your connected IDE and run:\n\n```bash\n# Clone or create project directory\nmkdir memu-assistant && cd memu-assistant\n\n# Create virtual environment\npython -m venv venv\nsource venv/bin/activate  # On Windows: venv\\Scripts\\activate\n\n# Install dependencies\npip install memu fastapi uvicorn python-dotenv\n```\n\n## Step 4: Create the Application\n\nCreate the following files in your project:\n\n### `.env`\n\n```env\n# LLM Provider Configuration\nOPENAI_API_KEY=your_api_key_here\nOPENAI_BASE_URL=https://api.openai.com/v1\n\n# Or use Nebius (OpenAI-compatible)\n# OPENAI_API_KEY=your_nebius_key\n# OPENAI_BASE_URL=https://api.tokenfactory.nebius.com/v1/\n\n# Model Configuration\nCHAT_MODEL=gpt-4o-mini\nEMBED_MODEL=text-embedding-3-small\n\n# Server Configuration\nHOST=0.0.0.0\nPORT=8000\n```\n\n### `main.py`\n\n```python\n\"\"\"\nPersonal AI Assistant with Long-Term Memory\nPowered by MemU + FastAPI on Sealos DevBox\n\"\"\"\n\nimport os\nfrom contextlib import asynccontextmanager\nfrom dotenv import load_dotenv\nfrom fastapi import FastAPI, HTTPException\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom pydantic import BaseModel\n\nload_dotenv()\n\n# MemU imports\nfrom memu.app import MemoryService\n\n# Global memory service\nmemory_service: MemoryService | None = None\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    \"\"\"Initialize MemU on startup.\"\"\"\n    global memory_service\n\n    llm_profiles = {\n        \"default\": {\n            \"provider\": \"openai\",\n            \"base_url\": os.getenv(\"OPENAI_BASE_URL\", \"https://api.openai.com/v1\"),\n            \"api_key\": os.getenv(\"OPENAI_API_KEY\"),\n            \"chat_model\": os.getenv(\"CHAT_MODEL\", \"gpt-4o-mini\"),\n            \"client_backend\": \"sdk\",\n        },\n        \"embedding\": {\n            \"provider\": \"openai\",\n            \"base_url\": os.getenv(\"OPENAI_BASE_URL\", \"https://api.openai.com/v1\"),\n            \"api_key\": os.getenv(\"OPENAI_API_KEY\"),\n            \"embed_model\": os.getenv(\"EMBED_MODEL\", \"text-embedding-3-small\"),\n            \"client_backend\": \"sdk\",\n        },\n    }\n\n    memory_service = MemoryService(llm_profiles=llm_profiles)\n    print(\"✓ MemU Memory Service initialized\")\n    yield\n    print(\"Shutting down...\")\n\n\napp = FastAPI(\n    title=\"MemU Assistant\",\n    description=\"AI Assistant with Long-Term Memory\",\n    lifespan=lifespan,\n)\n\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"*\"],\n    allow_methods=[\"*\"],\n    allow_headers=[\"*\"],\n)\n\n\nclass ChatRequest(BaseModel):\n    message: str\n    user_id: str = \"default\"\n\n\nclass ChatResponse(BaseModel):\n    response: str\n    memories_used: int\n    memories_stored: int\n\n\nclass MemorizeRequest(BaseModel):\n    content: str\n    user_id: str = \"default\"\n\n\n@app.get(\"/\")\nasync def root():\n    return {\n        \"service\": \"MemU Assistant\",\n        \"status\": \"running\",\n        \"endpoints\": [\"/chat\", \"/memorize\", \"/recall\", \"/health\"],\n    }\n\n\n@app.get(\"/health\")\nasync def health():\n    return {\"status\": \"healthy\", \"memory_service\": memory_service is not None}\n\n\n@app.post(\"/chat\", response_model=ChatResponse)\nasync def chat(request: ChatRequest):\n    \"\"\"\n    Chat with the AI assistant. The assistant will:\n    1. Retrieve relevant memories from past conversations\n    2. Generate a response using those memories as context\n    3. Store new information from the conversation\n    \"\"\"\n    if not memory_service:\n        raise HTTPException(status_code=503, detail=\"Memory service not initialized\")\n\n    # Step 1: Retrieve relevant memories\n    retrieve_result = await memory_service.retrieve(\n        queries=[{\"role\": \"user\", \"content\": request.message}]\n    )\n\n    memories = retrieve_result.get(\"items\", [])\n    memories_context = \"\"\n    if memories:\n        memories_context = \"\\n\\nRelevant memories from past conversations:\\n\"\n        for mem in memories[:5]:  # Limit to top 5 memories\n            if isinstance(mem, dict):\n                memories_context += f\"- {mem.get('summary', str(mem))}\\n\"\n\n    # Step 2: Generate response (simplified - in production, use full LLM call)\n    # For demo, we'll create a simple response acknowledging the memories\n    response_text = f\"I received your message: '{request.message}'\"\n    if memories:\n        response_text += f\"\\n\\nI found {len(memories)} relevant memories that might help.\"\n\n    # Step 3: Store the conversation as a new memory\n    import tempfile\n    with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:\n        f.write(f\"User ({request.user_id}): {request.message}\")\n        temp_file = f.name\n\n    try:\n        memorize_result = await memory_service.memorize(\n            resource_url=temp_file,\n            modality=\"text\",\n        )\n        memories_stored = len(memorize_result.get(\"items\", []))\n    finally:\n        os.unlink(temp_file)\n\n    return ChatResponse(\n        response=response_text,\n        memories_used=len(memories),\n        memories_stored=memories_stored,\n    )\n\n\n@app.post(\"/memorize\")\nasync def memorize(request: MemorizeRequest):\n    \"\"\"Store information in long-term memory.\"\"\"\n    if not memory_service:\n        raise HTTPException(status_code=503, detail=\"Memory service not initialized\")\n\n    import tempfile\n    with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:\n        f.write(request.content)\n        temp_file = f.name\n\n    try:\n        result = await memory_service.memorize(\n            resource_url=temp_file,\n            modality=\"text\",\n        )\n        return {\n            \"status\": \"stored\",\n            \"items_created\": len(result.get(\"items\", [])),\n            \"categories\": len(result.get(\"categories\", [])),\n        }\n    finally:\n        os.unlink(temp_file)\n\n\n@app.get(\"/recall\")\nasync def recall(query: str, limit: int = 5):\n    \"\"\"Recall memories related to a query.\"\"\"\n    if not memory_service:\n        raise HTTPException(status_code=503, detail=\"Memory service not initialized\")\n\n    result = await memory_service.retrieve(\n        queries=[{\"role\": \"user\", \"content\": query}]\n    )\n\n    items = result.get(\"items\", [])[:limit]\n    return {\n        \"query\": query,\n        \"memories_found\": len(items),\n        \"memories\": [\n            {\"summary\": item.get(\"summary\", str(item)) if isinstance(item, dict) else str(item)}\n            for item in items\n        ],\n    }\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n    uvicorn.run(\n        \"main:app\",\n        host=os.getenv(\"HOST\", \"0.0.0.0\"),\n        port=int(os.getenv(\"PORT\", 8000)),\n        reload=True,\n    )\n```\n\n### `requirements.txt`\n\n```\nmemu>=0.1.0\nfastapi>=0.100.0\nuvicorn[standard]>=0.23.0\npython-dotenv>=1.0.0\n```\n\n### `entrypoint.sh`\n\n```bash\n#!/bin/bash\nsource venv/bin/activate\nuvicorn main:app --host 0.0.0.0 --port 8000\n```\n\n## Step 5: Test Locally in DevBox\n\n```bash\n# Run the application\npython main.py\n```\n\nUse the DevBox preview feature to access your running application, or test with curl:\n\n```bash\n# Health check\ncurl http://localhost:8000/health\n\n# Store a memory\ncurl -X POST http://localhost:8000/memorize \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"content\": \"User prefers dark mode and uses Python for AI development\"}'\n\n# Chat with memory\ncurl -X POST http://localhost:8000/chat \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"message\": \"What programming language do I use?\"}'\n\n# Recall memories\ncurl \"http://localhost:8000/recall?query=programming%20preferences\"\n```\n\n## Step 6: Deploy to Production\n\n1. In the Sealos Dashboard, go to your DevBox project\n2. Click **Create Release** to package your application\n3. Click **Deploy** next to your release\n4. Configure environment variables (OPENAI_API_KEY, etc.)\n5. Click **Deploy** - your app will be live in minutes!\n\nYour application will receive a public URL like: `https://your-app.cloud.sealos.io`\n\n## Using with PostgreSQL (Optional)\n\nFor production deployments with persistent storage:\n\n1. In Sealos Dashboard, go to **Database** module\n2. Create a PostgreSQL instance\n3. Update your `.env` with the connection string:\n\n```env\nDATABASE_URL=postgresql://user:password@host:5432/memu\n```\n\n4. Update `main.py` to use PostgreSQL backend (see MemU documentation)\n\n## API Reference\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/` | GET | Service info |\n| `/health` | GET | Health check |\n| `/chat` | POST | Chat with memory-aware AI |\n| `/memorize` | POST | Store information in memory |\n| `/recall` | GET | Query stored memories |\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────────┐\n│                    Sealos DevBox                        │\n│  ┌─────────────────────────────────────────────────┐   │\n│  │              FastAPI Application                 │   │\n│  │  ┌─────────┐  ┌─────────┐  ┌─────────────────┐  │   │\n│  │  │  /chat  │  │/memorize│  │    /recall      │  │   │\n│  │  └────┬────┘  └────┬────┘  └────────┬────────┘  │   │\n│  │       │            │                │           │   │\n│  │       └────────────┼────────────────┘           │   │\n│  │                    │                            │   │\n│  │            ┌───────▼───────┐                    │   │\n│  │            │  MemU Service │                    │   │\n│  │            │  (Memory Mgmt)│                    │   │\n│  │            └───────┬───────┘                    │   │\n│  │                    │                            │   │\n│  │       ┌────────────┼────────────┐               │   │\n│  │       │            │            │               │   │\n│  │  ┌────▼────┐  ┌────▼────┐  ┌───▼────┐          │   │\n│  │  │ Vector  │  │   LLM   │  │Postgres│          │   │\n│  │  │ Store   │  │   API   │  │(opt.)  │          │   │\n│  │  └─────────┘  └─────────┘  └────────┘          │   │\n│  └─────────────────────────────────────────────────┘   │\n└─────────────────────────────────────────────────────────┘\n```\n\n## Benefits of This Setup\n\n- **Zero Infrastructure Management**: Sealos handles Kubernetes complexity\n- **Instant Environment**: Ready-to-code in 60 seconds\n- **Persistent Memory**: MemU maintains context across sessions\n- **Scalable**: Easily scale resources as needed\n- **Cost-Effective**: Pay only for what you use\n\n## Next Steps\n\n- Add authentication for multi-user support\n- Integrate with Slack, Discord, or other platforms\n- Use PostgreSQL for production-grade persistence\n- Add conversation history UI\n\n## Resources\n\n- [MemU Documentation](https://github.com/NevaMind-AI/memU)\n- [Sealos DevBox Guide](https://sealos.io/blog/how-to-setup-devbox)\n- [FastAPI Documentation](https://fastapi.tiangolo.com)\n\n---\n\n*This guide was created for the MemU PR Hackathon - 2026 New Year Challenge (Issue #228)*\n"
  },
  {
    "path": "docs/sealos_use_case.md",
    "content": "# 🛡️ Context-Aware Support Agent (Sealos Edition)\n\n## Overview\nThis use case demonstrates how **MemU** enables a support agent to remember user history across sessions, deployed on a **Sealos Devbox** environment.\n\nUnlike a standard web app, this demo focuses on the **backend memory orchestration**. It runs as a **CLI (Command Line Interface)** tool to transparently show the internal memory logs, retrieval process, and state persistence without the abstraction layer of a UI.\n\n## 🚀 Quick Start\n\n### Prerequisites\n- Sealos Devbox Environment\n- Python 3.13+\n- MemU Library (installed via `make install`)\n\n### How to Run the Demo\nSince this is a backend demonstration, you will run the agent directly in the terminal to observe the memory cycle.\n\n```bash\nuv run python examples/sealos_support_agent.py\n```\n\n## 📸 Live Demo Output (Proof of Concept)\n\nBelow is the actual output captured from the Sealos terminal. This serves as verification of the \"Demonstration Quality\" requirement.\n\n```plaintext\n🚀 Starting Sealos Support Agent Demo (Offline Mode)\n\n📝 --- Phase 1: Ingesting Conversation History ---\n👤 Captain: \"I'm getting a 502 Bad Gateway error on port 3000.\"\n🤖 Agent: (Memorizing this interaction...)\n✅ Memory stored! extracted 2 items.\n   - [profile] Captain reported a 502 Bad Gateway error on port 3000.\n\n🔍 --- Phase 2: Retrieval on New Interaction ---\n👤 Captain: \"Hello\"\n🤖 Agent: (Searching memory for context...)\n\n💡 Retrieved Context:\n   Found Memory: Captain reported a 502 Bad Gateway error on port 3000.\n\n💬 --- Phase 3: Agent Response ---\n🤖 Agent: \"Welcome back, Captain. I see you had a 502 error on port 3000 recently. Is that resolved?\"\n\n✨ Demo Completed Successfully\n```\n\n## 💡 Code Highlights & Justification\n\n- **CLI vs Web**: We chose a CLI implementation to provide clear visibility into the memory ingestion and retrieval logs, which are often hidden in web implementations.\n\n- **MockLLM**: Includes a MockLLM class to ensure the demo is 100% reproducible by reviewers without needing external API keys.\n\n- **Sealos Native**: Optimized to run within the ephemeral Sealos Devbox container lifecycle.\n"
  },
  {
    "path": "docs/sqlite.md",
    "content": "# SQLite Database Integration\n\nMemU supports SQLite as a lightweight, file-based database backend for memory storage. This is ideal for:\n\n- **Local development** and testing\n- **Single-user applications** with persistent storage\n- **Portable deployments** where you need a simple database solution\n- **Offline-capable applications** that can't rely on external databases\n\n## Quick Start\n\n### Basic Configuration\n\n```python\nfrom memu.app import MemoryService\n\n# Using default SQLite file (memu.db in current directory)\nservice = MemoryService(\n    llm_profiles={\"default\": {\"api_key\": \"your-api-key\"}},\n    database_config={\n        \"metadata_store\": {\n            \"provider\": \"sqlite\",\n        },\n    },\n)\n\n# Or specify a custom database path\nservice = MemoryService(\n    llm_profiles={\"default\": {\"api_key\": \"your-api-key\"}},\n    database_config={\n        \"metadata_store\": {\n            \"provider\": \"sqlite\",\n            \"dsn\": \"sqlite:///path/to/your/memory.db\",\n        },\n    },\n)\n```\n\n### In-Memory SQLite (No Persistence)\n\nFor testing or temporary storage, you can use an in-memory SQLite database:\n\n```python\nservice = MemoryService(\n    llm_profiles={\"default\": {\"api_key\": \"your-api-key\"}},\n    database_config={\n        \"metadata_store\": {\n            \"provider\": \"sqlite\",\n            \"dsn\": \"sqlite:///:memory:\",\n        },\n    },\n)\n```\n\n## Configuration Options\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `provider` | `str` | `\"inmemory\"` | Set to `\"sqlite\"` to use SQLite backend |\n| `dsn` | `str` | `\"sqlite:///memu.db\"` | SQLite connection string |\n\n### DSN Format\n\nSQLite DSN follows this format:\n- **File-based**: `sqlite:///path/to/database.db`\n- **In-memory**: `sqlite:///:memory:`\n- **Relative path**: `sqlite:///./data/memu.db`\n- **Absolute path**: `sqlite:////home/user/data/memu.db` (note the 4 slashes)\n\n## Vector Search\n\nSQLite doesn't have native vector support like PostgreSQL's pgvector. MemU uses **brute-force cosine similarity** for vector search when using SQLite:\n\n```python\nservice = MemoryService(\n    llm_profiles={\"default\": {\"api_key\": \"your-api-key\"}},\n    database_config={\n        \"metadata_store\": {\n            \"provider\": \"sqlite\",\n            \"dsn\": \"sqlite:///memu.db\",\n        },\n        \"vector_index\": {\n            \"provider\": \"bruteforce\",  # This is the default for SQLite\n        },\n    },\n)\n```\n\n**Note**: Brute-force search loads all embeddings into memory and computes similarity for each. This works well for moderate dataset sizes (up to ~100k items) but may be slow for larger datasets.\n\n## Database Schema\n\nSQLite creates the following tables automatically:\n\n- `sqlite_resources` - Multimodal resource records (images, documents, etc.)\n- `sqlite_memory_items` - Extracted memory items with embeddings\n- `sqlite_memory_categories` - Memory categories with summaries\n- `sqlite_category_items` - Relationships between items and categories\n\nEmbeddings are stored as JSON-serialized text in SQLite since there's no native vector type.\n\n## Data Import/Export\n\n### Export Data\n\nYou can export your SQLite database for backup or migration:\n\n```python\nimport shutil\n\n# Simply copy the database file\nshutil.copy(\"memu.db\", \"memu_backup.db\")\n```\n\n### Import from SQLite to PostgreSQL\n\nTo migrate data from SQLite to PostgreSQL:\n\n```python\nimport json\nfrom memu.database.sqlite import build_sqlite_database\nfrom memu.database.postgres import build_postgres_database\nfrom memu.app.settings import DatabaseConfig\nfrom pydantic import BaseModel\n\nclass UserScope(BaseModel):\n    user_id: str\n\n# Load from SQLite\nsqlite_config = DatabaseConfig(\n    metadata_store={\"provider\": \"sqlite\", \"dsn\": \"sqlite:///memu.db\"}\n)\nsqlite_db = build_sqlite_database(config=sqlite_config, user_model=UserScope)\nsqlite_db.load_existing()\n\n# Connect to PostgreSQL\npostgres_config = DatabaseConfig(\n    metadata_store={\"provider\": \"postgres\", \"dsn\": \"postgresql://...\"}\n)\npostgres_db = build_postgres_database(config=postgres_config, user_model=UserScope)\n\n# Migrate resources\nfor res_id, resource in sqlite_db.resources.items():\n    postgres_db.resource_repo.create_resource(\n        url=resource.url,\n        modality=resource.modality,\n        local_path=resource.local_path,\n        caption=resource.caption,\n        embedding=resource.embedding,\n        user_data={\"user_id\": getattr(resource, \"user_id\", None)},\n    )\n\n# Similar for categories, items, and relations...\n```\n\n## Performance Considerations\n\n| Aspect | SQLite | PostgreSQL |\n|--------|--------|------------|\n| Setup | Zero configuration | Requires server setup |\n| Concurrency | Single writer, multiple readers | Full concurrent access |\n| Vector Search | Brute-force (in-memory) | Native pgvector (indexed) |\n| Scale | Up to ~100k items | Millions of items |\n| Deployment | Single file, portable | External service |\n\n## Example: Full Workflow\n\n```python\nimport asyncio\nfrom memu.app import MemoryService\n\nasync def main():\n    # Initialize with SQLite\n    service = MemoryService(\n        llm_profiles={\"default\": {\"api_key\": \"your-api-key\"}},\n        database_config={\n            \"metadata_store\": {\n                \"provider\": \"sqlite\",\n                \"dsn\": \"sqlite:///my_memories.db\",\n            },\n        },\n    )\n\n    # Memorize a conversation\n    result = await service.memorize(\n        resource_url=\"conversation.json\",\n        modality=\"conversation\",\n        user={\"user_id\": \"alice\"},\n    )\n    print(f\"Created {len(result['categories'])} categories\")\n\n    # Retrieve relevant memories\n    memories = await service.retrieve(\n        queries=[\n            {\"role\": \"user\", \"content\": {\"text\": \"What are my preferences?\"}}\n        ],\n        where={\"user_id\": \"alice\"},\n    )\n\n    for item in memories.get(\"items\", []):\n        print(f\"- {item['summary']}\")\n\nasyncio.run(main())\n```\n\n## Troubleshooting\n\n### Database Locked Error\n\nSQLite only allows one writer at a time. If you see \"database is locked\" errors:\n\n1. Ensure you're not running multiple processes writing to the same database\n2. Consider using PostgreSQL for concurrent access needs\n3. Use connection pooling with appropriate timeouts\n\n### Permission Denied\n\nMake sure the directory containing the SQLite file is writable:\n\n```bash\nchmod 755 /path/to/data/directory\n```\n\n### Slow Vector Search\n\nIf vector search is slow with large datasets:\n\n1. Consider migrating to PostgreSQL with pgvector\n2. Use more selective `where` filters to reduce the search space\n3. Reduce `top_k` parameters in your retrieve configuration\n"
  },
  {
    "path": "docs/tutorials/getting_started.md",
    "content": "# Quickstart: Adding Long-Term Memory to Python Agents\n\nWelcome to MemU! This guide will help you add robust long-term memory capabilities to your Python agents in just a few minutes. Without MemU, LLMs are limited by their context window. MemU solves this by providing an intelligent, persistent memory layer.\n\n## Prerequisites\n\nBefore we begin, ensure you have the following:\n\n-   **Python 3.13+**: MemU takes advantage of modern Python features.\n-   **OpenAI API Key**: This quickstart uses OpenAI's models (`gpt-4o-mini`). You will need a valid API key.\n\n## Step-by-Step Guide\n\n### 1. Installation\n\nInstall MemU using `pip` or `uv`:\n\n```bash\npip install memu\n# OR\nuv add memu\n```\n\n### 2. Configuration\n\nMemU requires an LLM backend to function. By default, it looks for the `OPENAI_API_KEY` environment variable.\n\n**Linux / macOS / Git Bash:**\n```bash\nexport OPENAI_API_KEY=sk-proj-your-api-key\n```\n\n**Windows (PowerShell):**\n```powershell\n$env:OPENAI_API_KEY=\"sk-proj-your-api-key\"\n```\n\n### 3. The Robust Starter Script\n\nBelow is a complete, production-ready script that demonstrates the full lifecycle of a memory-enabled agent: **Initialization**, **Injection** (adding memory), and **Retrieval** (searching memory).\n\nCreate a file named `getting_started.py` and paste the following code:\n\n```python\n\"\"\"\nGetting Started with MemU: A Robust Example.\n\nThis script demonstrates the core lifecycle of MemU:\n1.  **Initialization**: Setting up the client with secure API key handling.\n2.  **Memory Injection**: Adding a specific memory with metadata.\n3.  **Retrieval**: Searching for that memory using natural language.\n4.  **Error Handling**: Catching common configuration issues.\n\nUsage:\n    export OPENAI_API_KEY=your_api_key_here\n    python getting_started.py\n\"\"\"\n\nimport asyncio\nimport logging\nimport os\nimport sys\n\nfrom memu.app import MemoryService\n\n# Configure logging to show info but suppress noisy libraries\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s - %(levelname)s - %(message)s\")\nlogging.getLogger(\"httpx\").setLevel(logging.WARNING)\n\n\nasync def main() -> None:\n    \"\"\"Run the MemU lifecycle demonstration.\"\"\"\n    print(\">>> MemU Getting Started Example\")\n    print(\"-\" * 30)\n\n    # 1. API Key Handling\n    # MemU relies on an LLM backend (defaulting to OpenAI).\n    # We ensure the API key is present before proceeding.\n    api_key = os.getenv(\"OPENAI_API_KEY\")\n    if not api_key:\n        print(\"[!] Error: OPENAI_API_KEY environment variable is not set.\")\n        print(\"Please export it: export OPENAI_API_KEY=sk-...\")\n        return\n\n    try:\n        # 2. Initialization\n        # We initialize the MemoryService with:\n        # - llm_profiles: Configuration for the LLM (model, api_key).\n        # - memorize_config: Pre-defining a memory category ensures we can organize memories efficiently.\n        print(f\"[*] Initializing MemoryService with model: gpt-4o-mini...\")\n        service = MemoryService(\n            llm_profiles={\n                \"default\": {\n                    \"api_key\": api_key,\n                    \"chat_model\": \"gpt-4o-mini\",\n                },\n            },\n            memorize_config={\n                \"memory_categories\": [\n                    {\n                        \"name\": \"User Facts\",\n                        \"description\": \"General and specific facts known about the user preference and identity.\",\n                    }\n                ]\n            },\n        )\n        print(\"[OK] Service initialized successfully.\\n\")\n\n        # 3. Memory Injection\n        # We manually inject a memory into the system.\n        # This is useful for bootstrapping a user profile or adding explicit knowledge.\n        print(\"[*] Injecting memory...\")\n        memory_content = \"The user is a senior Python architect who loves clean code and type hints.\"\n\n        # We use 'create_memory_item' to insert a single memory record.\n        # memory_type='profile' indicates this is an attribute of the user.\n        result = await service.create_memory_item(\n            memory_type=\"profile\",\n            memory_content=memory_content,\n            memory_categories=[\"User Facts\"],\n        )\n        print(f\"[OK] Memory created! ID: {result.get('memory_item', {}).get('id')}\\n\")\n\n        # 4. Retrieval\n        # Now we query the system naturally to see if it recalls the information.\n        query_text = \"What kind of code does the user like?\"\n        print(f\"[*] Querying: '{query_text}'\")\n\n        search_results = await service.retrieve(\n            queries=[{\"role\": \"user\", \"content\": query_text}]\n        )\n\n        # 5. Display Results\n        items = search_results.get(\"items\", [])\n        if items:\n            print(f\"[OK] Found {len(items)} relevant memory item(s):\")\n            for idx, item in enumerate(items, 1):\n                print(f\"   {idx}. {item.get('summary')} (Type: {item.get('memory_type')})\")\n        else:\n            print(\"[!] No relevant memories found.\")\n\n    except Exception as e:\n        print(f\"\\n[!] An error occurred during execution: {e}\")\n        logging.exception(\"Detailed traceback:\")\n    finally:\n        print(\"\\n[=] Example execution finished.\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Understanding the Code\n\n1.  **Initialization**: We configure `MemoryService` with specific `llm_profiles`. This tells MemU which model to use. We also define a `memorize_config` with a \"User Facts\" category. Categories help the LLM organize and retrieve information more effectively.\n2.  **Memory Injection**: `create_memory_item` is used to explicitly add a piece of knowledge. We tag it with `memory_type=\"profile\"` to semantically indicate this is a user attribute.\n3.  **Retrieval**: We use `retrieve` with a natural language query. MemU's internal workflow (\"RAG\" or \"LLM\" based) will determine the best way to find relevant memories.\n\n## Troubleshooting\n\n### `[!] Error: OPENAI_API_KEY environment variable is not set.`\n\nThis is the most common issue. It means the script cannot find your API key which is required to communicate with OpenAI.\n\n**Solution:**\nEnsure you have exported the key in your **current terminal session**.\n-   **Windows PowerShell**: `$env:OPENAI_API_KEY=\"sk-...\"`\n-   **Linux/Mac**: `export OPENAI_API_KEY=sk-...`\n\nAlso, verify that you didn't accidentally include spaces around the `=` sign in bash.\n\n## Next Steps\n\nNow that you have the basics running, consider exploring:\n-   **Core Concepts**: Learn about `MemoryService`, `MemoryItem`, and `MemoryCategory`.\n-   **Advanced Configuration**: Switch to local LLMs or use different vector stores.\n-   **Integrations**: Connect MemU to your existing agent framework.\n\n## Community Resources\n\nThis tutorial was created as part of the MemU 2026 Challenge. For a summary of the architectural analysis, see the author's [LinkedIn Post](https://www.linkedin.com/posts/david-a-mamani-c_github-nevamind-aimemu-memory-infrastructure-activity-7418493617482207232-_MtG?utm_source=share&utm_medium=member_desktop&rcm=ACoAAFdc0CIB__DJovR2t1BOxxJ6tgEeOqVEgx4).\n"
  },
  {
    "path": "examples/example_1_conversation_memory.py",
    "content": "\"\"\"\nExample 1: Multiple Conversations -> Memory Category File\n\nThis example demonstrates how to process multiple conversation files\nand generate a memory category JSON file.\n\nUsage:\n    export OPENAI_API_KEY=your_api_key\n    python examples/example_1_conversation_memory.py\n\"\"\"\n\nimport asyncio\nimport os\nimport sys\n\nfrom memu.app import MemoryService\n\n# Add src to sys.path\nsrc_path = os.path.abspath(\"src\")\nsys.path.insert(0, src_path)\n\n\nasync def generate_memory_md(categories, output_dir):\n    \"\"\"Generate concise markdown files for each memory category.\"\"\"\n\n    os.makedirs(output_dir, exist_ok=True)\n\n    generated_files = []\n\n    for cat in categories:\n        name = cat.get(\"name\", \"unknown\")\n        summary = cat.get(\"summary\", \"\")\n\n        filename = f\"{name}.md\"\n        filepath = os.path.join(output_dir, filename)\n\n        with open(filepath, \"w\", encoding=\"utf-8\") as f:\n            # Title\n            # Content - concise version\n            if summary:\n                cleaned_summary = summary.replace(\"<content>\", \"\").replace(\"</content>\", \"\").strip()\n                f.write(f\"{cleaned_summary}\\n\")\n            else:\n                f.write(\"*No content available*\\n\")\n\n        generated_files.append(filename)\n\n    return generated_files\n\n\nasync def main():\n    \"\"\"\n    Process multiple conversation files and generate memory categories.\n\n    This example:\n    1. Initializes MemoryService with OpenAI API\n    2. Processes conversation JSON files\n    3. Extracts memory categories from conversations\n    4. Outputs the categories to files\n    \"\"\"\n    print(\"Example 1: Conversation Memory Processing\")\n    print(\"-\" * 50)\n\n    # Get OpenAI API key from environment\n    api_key = os.getenv(\"OPENAI_API_KEY\")\n    if not api_key:\n        msg = \"Please set OPENAI_API_KEY environment variable\"\n        raise ValueError(msg)\n\n    # Initialize service with OpenAI using llm_profiles\n    # The \"default\" profile is required and used as the primary LLM configuration\n    service = MemoryService(\n        llm_profiles={\n            \"default\": {\n                \"api_key\": api_key,\n                \"chat_model\": \"gpt-4o-mini\",\n            },\n        },\n    )\n\n    # Conversation files to process\n    conversation_files = [\n        \"examples/resources/conversations/conv1.json\",\n        \"examples/resources/conversations/conv2.json\",\n        \"examples/resources/conversations/conv3.json\",\n    ]\n\n    # Process each conversation\n    print(\"\\nProcessing conversations...\")\n    total_items = 0\n    categories = []\n    for conv_file in conversation_files:\n        if not os.path.exists(conv_file):\n            continue\n\n        try:\n            result = await service.memorize(resource_url=conv_file, modality=\"conversation\")\n            total_items += len(result.get(\"items\", []))\n            # Categories are returned in the result and updated after each memorize call\n            categories = result.get(\"categories\", [])\n        except Exception as e:\n            print(f\"Error: {e}\")\n\n    # Write to output files\n    output_dir = \"examples/output/conversation_example\"\n    os.makedirs(output_dir, exist_ok=True)\n\n    # 1. Generate individual Markdown files for each category\n    await generate_memory_md(categories, output_dir)\n\n    print(f\"\\n✓ Processed {len(conversation_files)} files, extracted {total_items} items\")\n    print(f\"✓ Generated {len(categories)} categories\")\n    print(f\"✓ Output: {output_dir}/\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/example_2_skill_extraction.py",
    "content": "\"\"\"\nExample 2: Workflow & Agent Logs -> Skill Extraction\n\nThis example demonstrates how to extract skills from workflow descriptions\nand agent runtime logs, then output them to a Markdown file.\n\nUsage:\n    export OPENAI_API_KEY=your_api_key\n    python examples/example_2_skill_extraction.py\n\"\"\"\n\nimport asyncio\nimport os\nimport sys\n\nfrom openai import AsyncOpenAI\n\nfrom memu.app import MemoryService\n\n# Add src to sys.path\nsrc_path = os.path.abspath(\"src\")\nsys.path.insert(0, src_path)\n\n\nasync def generate_skill_md(\n    all_skills, service, output_file, attempt_number, total_attempts, categories=None, is_final=False\n):\n    \"\"\"\n    Use LLM to generate a concise task execution guide (skill.md).\n\n    This creates a production-ready guide incorporating lessons learned from deployment attempts.\n    \"\"\"\n\n    os.makedirs(os.path.dirname(output_file), exist_ok=True)\n\n    # Prepare context for LLM\n    skills_text = \"\\n\\n\".join([f\"### From {skill_data['source']}\\n{skill_data['skill']}\" for skill_data in all_skills])\n\n    # Get category summaries if available\n    categories_text = \"\"\n    if categories:\n        categories_with_content = [cat for cat in categories if cat.get(\"summary\") and cat.get(\"summary\").strip()]\n        if categories_with_content:\n            categories_text = \"\\n\\n\".join([\n                f\"**{cat.get('name', 'unknown')}**:\\n{cat.get('summary', '')}\" for cat in categories_with_content\n            ])\n\n    # Construct prompt for LLM\n    prompt = f\"\"\"Generate a concise production-ready task execution guide.\n\n**Context**:\n- Task: Production Microservice Deployment with Blue-Green Strategy\n- Progress: {attempt_number}/{total_attempts} attempts\n- Status: {\"Complete\" if is_final else f\"v0.{attempt_number}\"}\n\n**Skills Learned**:\n{skills_text}\n\n{f\"**Categories**:\\n{categories_text}\" if categories_text else \"\"}\n\n**Required Structure**:\n\n1. **Frontmatter** (YAML):\n   - name: production-microservice-deployment\n   - description: Brief description\n   - version: {\"1.0.0\" if is_final else f\"0.{attempt_number}.0\"}\n   - status: {\"Production-Ready\" if is_final else \"Evolving\"}\n\n2. **Introduction**: What this guide does and when to use it\n\n3. **Deployment Context**: Strategy, environment, goals\n\n4. **Pre-Deployment Checklist**:\n   - Actionable checks from lessons learned\n   - Group by category (Database, Monitoring, etc.)\n   - Mark critical items\n\n5. **Deployment Procedure**:\n   - Step-by-step instructions with commands\n   - Include monitoring points\n\n6. **Rollback Procedure**:\n   - When to rollback (thresholds)\n   - Exact commands\n   - Expected recovery time\n\n7. **Common Pitfalls & Solutions**:\n   - Failures/issues encountered\n   - Root cause, symptoms, solution\n\n8. **Best Practices**:\n   - What works well\n   - Expected timelines\n\n9. **Key Takeaways**: 3-5 most important lessons\n\n**Style**:\n- Use markdown with clear hierarchy\n- Be specific and concise\n- Technical and production-grade tone\n- Focus on PRACTICAL steps\n\n**CRITICAL**:\n- ONLY use information from provided skills/lessons\n- DO NOT make assumptions or add generic advice\n- Extract ACTUAL experiences from the logs\n\nGenerate the complete markdown document now:\"\"\"\n\n    client = AsyncOpenAI(api_key=service.llm_config.api_key)\n\n    response = await client.chat.completions.create(\n        model=service.llm_config.chat_model,\n        messages=[\n            {\n                \"role\": \"system\",\n                \"content\": \"You are an expert technical writer creating concise, production-grade deployment guides from real experiences.\",\n            },\n            {\"role\": \"user\", \"content\": prompt},\n        ],\n        temperature=0.7,\n        max_tokens=3000,\n    )\n\n    generated_content = response.choices[0].message.content\n\n    # Write to file\n    with open(output_file, \"w\", encoding=\"utf-8\") as f:\n        f.write(generated_content)\n\n    return True\n\n\nasync def main():\n    \"\"\"\n    Extract skills from agent logs using incremental memory updates.\n\n    This example demonstrates INCREMENTAL LEARNING:\n    1. Process files ONE BY ONE\n    2. Each file UPDATES existing memory\n    3. Category summaries EVOLVE with each new file\n    4. Final output shows accumulated knowledge\n    \"\"\"\n    print(\"Example 2: Incremental Skill Extraction\")\n    print(\"-\" * 50)\n\n    # Get OpenAI API key from environment\n    api_key = os.getenv(\"OPENAI_API_KEY\")\n    if not api_key:\n        msg = \"Please set OPENAI_API_KEY environment variable\"\n        raise ValueError(msg)\n\n    # Custom config for skill extraction\n    skill_prompt = \"\"\"\n    You are analyzing an agent execution log. Extract the key actions taken, their outcomes, and lessons learned.\n\n    For each significant action or phase:\n\n    1. **Action/Phase**: What was being attempted?\n    2. **Status**: SUCCESS ✅ or FAILURE ❌\n    3. **What Happened**: What was executed\n    4. **Outcome**: What worked/failed, metrics\n    5. **Root Cause** (for failures): Why did it fail?\n    6. **Lesson**: What did we learn?\n    7. **Action Items**: Concrete steps for next time\n\n    **IMPORTANT**:\n    - Focus on ACTIONS and outcomes\n    - Be specific: include actual metrics, errors, timing\n    - ONLY extract information explicitly stated\n    - DO NOT infer or assume information\n\n    Extract ALL significant actions from the text:\n\n    Text: {resource}\n    \"\"\"\n\n    # Define custom categories\n    skill_categories = [\n        {\"name\": \"deployment_execution\", \"description\": \"Deployment actions, traffic shifting, environment management\"},\n        {\n            \"name\": \"pre_deployment_validation\",\n            \"description\": \"Capacity validation, configuration checks, readiness verification\",\n        },\n        {\n            \"name\": \"incident_response_rollback\",\n            \"description\": \"Incident response, error detection, rollback procedures\",\n        },\n        {\n            \"name\": \"performance_monitoring\",\n            \"description\": \"Metrics monitoring, performance analysis, bottleneck detection\",\n        },\n        {\"name\": \"database_management\", \"description\": \"Database capacity planning, optimization, schema changes\"},\n        {\"name\": \"testing_verification\", \"description\": \"Testing, smoke tests, load tests, verification\"},\n        {\"name\": \"infrastructure_setup\", \"description\": \"Kubernetes, containers, networking configuration\"},\n        {\"name\": \"lessons_learned\", \"description\": \"Key reflections, root cause analyses, action items\"},\n    ]\n\n    memorize_config = {\n        \"memory_types\": [\"skill\"],\n        \"memory_type_prompts\": {\"skill\": skill_prompt},\n        \"memory_categories\": skill_categories,\n    }\n\n    # Initialize service with OpenAI using llm_profiles\n    # The \"default\" profile is required and used as the primary LLM configuration\n    service = MemoryService(\n        llm_profiles={\n            \"default\": {\n                \"api_key\": api_key,\n                \"chat_model\": \"gpt-4o-mini\",\n            },\n        },\n        memorize_config=memorize_config,\n    )\n\n    # Resources to process\n    resources = [\n        (\"examples/resources/logs/log1.txt\", \"document\"),\n        (\"examples/resources/logs/log2.txt\", \"document\"),\n        (\"examples/resources/logs/log3.txt\", \"document\"),\n    ]\n\n    # Process each resource sequentially\n    print(\"\\nProcessing files...\")\n    all_skills = []\n    categories = []\n\n    for idx, (resource_file, modality) in enumerate(resources, 1):\n        if not os.path.exists(resource_file):\n            continue\n\n        try:\n            result = await service.memorize(resource_url=resource_file, modality=modality)\n\n            # Extract skill items\n            for item in result.get(\"items\", []):\n                if item.get(\"memory_type\") == \"skill\":\n                    all_skills.append({\"skill\": item.get(\"summary\", \"\"), \"source\": os.path.basename(resource_file)})\n\n            # Categories are returned in the result and updated after each memorize call\n            categories = result.get(\"categories\", [])\n\n            # Generate intermediate skill.md\n            await generate_skill_md(\n                all_skills=all_skills,\n                service=service,\n                output_file=f\"examples/output/skill_example/log_{idx}.md\",\n                attempt_number=idx,\n                total_attempts=len(resources),\n                categories=categories,\n            )\n\n        except Exception as e:\n            print(f\"Error: {e}\")\n\n    # Generate final comprehensive skill.md\n    await generate_skill_md(\n        all_skills=all_skills,\n        service=service,\n        output_file=\"examples/output/skill_example/skill.md\",\n        attempt_number=len(resources),\n        total_attempts=len(resources),\n        categories=categories,\n        is_final=True,\n    )\n\n    print(f\"\\n✓ Processed {len(resources)} files, extracted {len(all_skills)} skills\")\n    print(f\"✓ Generated {len(categories)} categories\")\n    print(\"✓ Output: examples/output/skill_example/\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/example_3_multimodal_memory.py",
    "content": "\"\"\"\nExample 3: Multimodal Processing -> Memory Category File\n\nThis example demonstrates how to process multiple modalities (images, documents)\nand generate a unified memory category JSON file.\n\nUsage:\n    export OPENAI_API_KEY=your_api_key\n    python examples/example_3_multimodal_memory.py\n\"\"\"\n\nimport asyncio\nimport os\nimport sys\n\nfrom memu.app import MemoryService\n\n# Add src to sys.path\nsrc_path = os.path.abspath(\"src\")\nsys.path.insert(0, src_path)\n\n\nasync def generate_memory_md(categories, output_dir):\n    \"\"\"Generate concise markdown files for each memory category.\"\"\"\n\n    os.makedirs(output_dir, exist_ok=True)\n\n    generated_files = []\n\n    for cat in categories:\n        name = cat.get(\"name\", \"unknown\")\n        description = cat.get(\"description\", \"\")\n        summary = cat.get(\"summary\", \"\")\n\n        filename = f\"{name}.md\"\n        filepath = os.path.join(output_dir, filename)\n\n        with open(filepath, \"w\", encoding=\"utf-8\") as f:\n            # Title\n            formatted_name = name.replace(\"_\", \" \").title()\n            f.write(f\"# {formatted_name}\\n\\n\")\n\n            if description:\n                f.write(f\"*{description}*\\n\\n\")\n\n            # Content - full version\n            if summary:\n                cleaned_summary = summary.replace(\"<content>\", \"\").replace(\"</content>\", \"\").strip()\n                f.write(f\"{cleaned_summary}\\n\")\n            else:\n                f.write(\"*No content available*\\n\")\n\n        generated_files.append(filename)\n\n    return generated_files\n\n\nasync def main():\n    \"\"\"\n    Process multiple modalities (images and documents) to generate memory categories.\n\n    This example:\n    1. Initializes MemoryService with OpenAI API\n    2. Processes documents and images\n    3. Extracts unified memory categories across modalities\n    4. Outputs the categories to files\n    \"\"\"\n    print(\"Example 3: Multimodal Memory Processing\")\n    print(\"-\" * 50)\n\n    # Get OpenAI API key from environment\n    api_key = os.getenv(\"OPENAI_API_KEY\")\n    if not api_key:\n        msg = \"Please set OPENAI_API_KEY environment variable\"\n        raise ValueError(msg)\n\n    # Define custom categories for multimodal content\n    multimodal_categories = [\n        {\"name\": \"technical_documentation\", \"description\": \"Technical documentation, guides, and tutorials\"},\n        {\n            \"name\": \"architecture_concepts\",\n            \"description\": \"System architecture, design patterns, and structural concepts\",\n        },\n        {\"name\": \"best_practices\", \"description\": \"Best practices, recommendations, and guidelines\"},\n        {\"name\": \"code_examples\", \"description\": \"Code snippets, examples, and implementation details\"},\n        {\"name\": \"visual_diagrams\", \"description\": \"Visual concepts, diagrams, charts, and illustrations from images\"},\n    ]\n\n    # Initialize service with OpenAI using llm_profiles\n    # The \"default\" profile is required and used as the primary LLM configuration\n    service = MemoryService(\n        llm_profiles={\n            \"default\": {\n                \"api_key\": api_key,\n                \"chat_model\": \"gpt-4o-mini\",\n            },\n        },\n        memorize_config={\"memory_categories\": multimodal_categories},\n    )\n\n    # Resources to process (file_path, modality)\n    resources = [\n        (\"examples/resources/docs/doc1.txt\", \"document\"),\n        (\"examples/resources/docs/doc2.txt\", \"document\"),\n        (\"examples/resources/images/image1.png\", \"image\"),\n    ]\n\n    # Process each resource\n    print(\"\\nProcessing resources...\")\n    total_items = 0\n    categories = []\n    for resource_file, modality in resources:\n        if not os.path.exists(resource_file):\n            continue\n\n        try:\n            result = await service.memorize(resource_url=resource_file, modality=modality)\n            total_items += len(result.get(\"items\", []))\n            # Categories are returned in the result and updated after each memorize call\n            categories = result.get(\"categories\", [])\n        except Exception as e:\n            print(f\"Error: {e}\")\n\n    # Write to output files\n    output_dir = \"examples/output/multimodal_example\"\n    os.makedirs(output_dir, exist_ok=True)\n\n    # 1. Generate individual Markdown files for each category\n    await generate_memory_md(categories, output_dir)\n\n    print(f\"\\n✓ Processed {len(resources)} files, extracted {total_items} items\")\n    print(f\"✓ Generated {len(categories)} categories\")\n    print(f\"✓ Output: {output_dir}/\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/example_4_openrouter_memory.py",
    "content": "\"\"\"\nExample 4: Multiple Conversations -> Memory Category File (Using OpenRouter)\n\nThis example demonstrates how to process multiple conversation files\nand generate memory categories using OpenRouter as the LLM backend.\n\nUsage:\n    export OPENROUTER_API_KEY=your_api_key\n    python examples/example_4_openrouter_memory.py\n\"\"\"\n\nimport asyncio\nimport os\nimport sys\n\nfrom memu.app import MemoryService\n\nsrc_path = os.path.abspath(\"src\")\nsys.path.insert(0, src_path)\n\n\nasync def generate_memory_md(categories, output_dir):\n    \"\"\"Generate concise markdown files for each memory category.\"\"\"\n    os.makedirs(output_dir, exist_ok=True)\n    generated_files = []\n\n    for cat in categories:\n        name = cat.get(\"name\", \"unknown\")\n        summary = cat.get(\"summary\", \"\")\n\n        filename = f\"{name}.md\"\n        filepath = os.path.join(output_dir, filename)\n\n        with open(filepath, \"w\", encoding=\"utf-8\") as f:\n            if summary:\n                cleaned_summary = summary.replace(\"<content>\", \"\").replace(\"</content>\", \"\").strip()\n                f.write(f\"{cleaned_summary}\\n\")\n            else:\n                f.write(\"*No content available*\\n\")\n\n        generated_files.append(filename)\n\n    return generated_files\n\n\nasync def main():\n    \"\"\"\n    Process multiple conversation files and generate memory categories using OpenRouter.\n\n    This example:\n    1. Initializes MemoryService with OpenRouter API\n    2. Processes conversation JSON files\n    3. Extracts memory categories from conversations\n    4. Outputs the categories to files\n    \"\"\"\n    print(\"Example 4: Conversation Memory Processing (OpenRouter)\")\n    print(\"-\" * 50)\n\n    api_key = os.getenv(\"OPENROUTER_API_KEY\")\n    if not api_key:\n        msg = \"Please set OPENROUTER_API_KEY environment variable\"\n        raise ValueError(msg)\n\n    # Initialize service with OpenRouter\n    service = MemoryService(\n        llm_profiles={\n            \"default\": {\n                \"provider\": \"openrouter\",\n                \"client_backend\": \"httpx\",\n                \"base_url\": \"https://openrouter.ai\",\n                \"api_key\": api_key,\n                \"chat_model\": \"anthropic/claude-3.5-sonnet\",  # you can use any model from openrouter.ai\n                \"embed_model\": \"openai/text-embedding-3-small\",  # you can use any model from openrouter.ai\n            },\n        },\n    )\n\n    conversation_files = [\n        \"examples/resources/conversations/conv1.json\",\n        \"examples/resources/conversations/conv2.json\",\n        \"examples/resources/conversations/conv3.json\",\n    ]\n\n    print(\"\\nProcessing conversations...\")\n    total_items = 0\n    categories = []\n\n    for conv_file in conversation_files:\n        if not os.path.exists(conv_file):\n            print(f\"Skipped: {conv_file} not found\")\n            continue\n\n        try:\n            print(f\"Processing: {conv_file}\")\n            result = await service.memorize(resource_url=conv_file, modality=\"conversation\")\n            total_items += len(result.get(\"items\", []))\n            categories = result.get(\"categories\", [])\n        except Exception as e:\n            print(f\"Error processing {conv_file}: {e}\")\n\n    output_dir = \"examples/output/openrouter_example\"\n    os.makedirs(output_dir, exist_ok=True)\n\n    await generate_memory_md(categories, output_dir)\n\n    print(f\"\\nProcessed {len(conversation_files)} files, extracted {total_items} items\")\n    print(f\"Generated {len(categories)} categories\")\n    print(f\"Output: {output_dir}/\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/example_5_with_lazyllm_client.py",
    "content": "\"\"\"\nUnified Example: LazyLLM Integration Demo\n=========================================\n\nThis example merges functionalities from:\n1. Example 1: Conversation Memory Processing\n2. Example 2: Skill Extraction\n3. Example 3: Multimodal Processing\n\nIt demonstrates how to use the LazyLLM backend for:\n- Processing conversation history\n- Extracting technical skills from logs\n- Handling multimodal content (images + text)\n- defaut source and model are from qwen\n\nUsage:\n    export MEMU_QWEN_API_KEY=your_api_key\n    python examples/example_5_with_lazyllm_client.py\n\"\"\"\n\nimport asyncio\nimport os\nimport sys\nfrom pathlib import Path\n\n# Add src to sys.path FIRST before importing memu\nproject_root = Path(__file__).parent.parent\nsrc_path = str(project_root / \"src\")\nif src_path not in sys.path:\n    sys.path.insert(0, src_path)\n\nfrom memu.app import MemoryService\n\n# ==========================================\n# PART 1: Conversation Memory Processing\n# ==========================================\n\n\nasync def run_conversation_memory_demo(service):\n    print(\"\\n\" + \"=\" * 60)\n    print(\"PART 1: Conversation Memory Processing\")\n    print(\"=\" * 60)\n\n    conversation_files = [\n        \"examples/resources/conversations/conv1.json\",\n        \"examples/resources/conversations/conv2.json\",\n        \"examples/resources/conversations/conv3.json\",\n    ]\n\n    total_items = 0\n    categories = []\n\n    for conv_file in conversation_files:\n        if not os.path.exists(conv_file):\n            print(f\"⚠ File not found: {conv_file}\")\n            continue\n\n        try:\n            print(f\"  Processing: {conv_file}\")\n            result = await service.memorize(resource_url=conv_file, modality=\"conversation\")\n            total_items += len(result.get(\"items\", []))\n            categories = result.get(\"categories\", [])\n            print(f\"    ✓ Extracted {len(result.get('items', []))} items\")\n        except Exception as e:\n            print(f\"  ✗ Error processing {conv_file}: {e}\")\n\n    # Output generation\n    output_dir = \"examples/output/lazyllm_example/conversation\"\n    os.makedirs(output_dir, exist_ok=True)\n    await generate_markdown_output(categories, output_dir)\n    print(f\"✓ Conversation processing complete. Output: {output_dir}\")\n\n\n# ==========================================\n# PART 2: Skill Extraction\n# ==========================================\n\n\nasync def run_skill_extraction_demo(service):\n    print(\"\\n\" + \"=\" * 60)\n    print(\"PART 2: Skill Extraction from Logs\")\n    print(\"=\" * 60)\n\n    # Configure prompt for skill extraction\n    skill_prompt = \"\"\"\n    You are analyzing an agent execution log. Extract the key actions taken, their outcomes, and lessons learned.\n\n    Output MUST be valid XML wrapped in <skills> tags.\n    Format:\n    <skills>\n        <memory>\n            <content>\n                [Action] Description...\n                [Lesson] Key lesson...\n            </content>\n            <categories>\n                <category>Category Name</category>\n            </categories>\n        </memory>\n    </skills>\n\n    Text: {resource}\n    \"\"\"\n\n    # Update service config for skill extraction\n    service.memorize_config.memory_types = [\"skill\"]\n    service.memorize_config.memory_type_prompts = {\"skill\": skill_prompt}\n\n    logs = [\"examples/resources/logs/log1.txt\", \"examples/resources/logs/log2.txt\", \"examples/resources/logs/log3.txt\"]\n\n    all_skills = []\n    for log_file in logs:\n        if not os.path.exists(log_file):\n            continue\n\n        print(f\"  Processing log: {log_file}\")\n        try:\n            result = await service.memorize(resource_url=log_file, modality=\"document\")\n            for item in result.get(\"items\", []):\n                if item.get(\"memory_type\") == \"skill\":\n                    all_skills.append(item.get(\"summary\", \"\"))\n            print(f\"    ✓ Extracted {len(result.get('items', []))} skills\")\n        except Exception as e:\n            print(f\"  ✗ Error: {e}\")\n\n    # Generate summary guide\n    if all_skills:\n        output_file = \"examples/output/lazyllm_example/skills/skill_guide.md\"\n        await generate_skill_guide(all_skills, service, output_file)\n        print(f\"✓ Skill guide generated: {output_file}\")\n\n\n# ==========================================\n# PART 3: Multimodal Memory\n# ==========================================\n\n\nasync def run_multimodal_demo(service):\n    print(\"\\n\" + \"=\" * 60)\n    print(\"PART 3: Multimodal Memory Processing\")\n    print(\"=\" * 60)\n\n    # Configure for knowledge extraction\n    xml_prompt = \"\"\"\n    Analyze content and extract key information.\n    Output MUST be valid XML wrapped in <knowledge> tags.\n    Format:\n    <knowledge>\n        <memory>\n            <content>Extracted content...</content>\n            <categories><category>category_name</category></categories>\n        </memory>\n    </knowledge>\n\n    Content: {resource}\n    \"\"\"\n\n    service.memorize_config.memory_types = [\"knowledge\"]\n    service.memorize_config.memory_type_prompts = {\"knowledge\": xml_prompt}\n\n    resources = [\n        (\"examples/resources/docs/doc1.txt\", \"document\"),\n        (\"examples/resources/images/image1.png\", \"image\"),\n    ]\n\n    categories = []\n    for res_file, modality in resources:\n        if not os.path.exists(res_file):\n            continue\n\n        print(f\"  Processing {modality}: {res_file}\")\n        try:\n            result = await service.memorize(resource_url=res_file, modality=modality)\n            categories = result.get(\"categories\", [])\n            print(f\"    ✓ Extracted {len(result.get('items', []))} items\")\n        except Exception as e:\n            print(f\"  ✗ Error: {e}\")\n\n    output_dir = \"examples/output/lazyllm_example/multimodal\"\n    os.makedirs(output_dir, exist_ok=True)\n    await generate_markdown_output(categories, output_dir)\n    print(f\"✓ Multimodal processing complete. Output: {output_dir}\")\n\n\n# ==========================================\n# Helpers\n# ==========================================\n\n\nasync def generate_markdown_output(categories, output_dir):\n    for cat in categories:\n        name = cat.get(\"name\", \"unknown\")\n        summary = cat.get(\"summary\", \"\")\n        if not summary:\n            continue\n\n        with open(os.path.join(output_dir, f\"{name}.md\"), \"w\", encoding=\"utf-8\") as f:\n            f.write(f\"# {name.replace('_', ' ').title()}\\n\\n\")\n            cleaned = summary.replace(\"<content>\", \"\").replace(\"</content>\", \"\").strip()\n            f.write(cleaned)\n\n\nasync def generate_skill_guide(skills, service, output_file):\n    os.makedirs(os.path.dirname(output_file), exist_ok=True)\n    skills_text = \"\\n\\n\".join(skills)\n    prompt = f\"Summarize these skills into a guide:\\n\\n{skills_text}\"\n\n    # Use LazyLLM via service\n    summary = await service.llm_client.chat(text=prompt)\n\n    with open(output_file, \"w\", encoding=\"utf-8\") as f:\n        f.write(summary)\n\n\n# ==========================================\n# Main Entry\n# ==========================================\n\n\nasync def main():\n    print(\"Unified LazyLLM Example\")\n    print(\"=\" * 60)\n    # 1. Initialize Shared Service\n    service = MemoryService(\n        llm_profiles={\n            \"default\": {\n                \"client_backend\": \"lazyllm_backend\",\n                \"chat_model\": \"qwen3-max\",\n                \"embed_model\": \"text-embedding-v3\",\n                \"lazyllm_source\": {\n                    \"source\": \"qwen\",\n                    \"llm_source\": \"qwen\",\n                    \"vlm_source\": \"qwen\",\n                    \"embed_source\": \"qwen\",\n                    \"stt_source\": \"qwen\",\n                    \"vlm_model\": \"qwen-vl-plus\",\n                    \"stt_model\": \"qwen-audio-turbo\",\n                },\n            },\n        }\n    )\n\n    # 2. Run Demos\n    await run_conversation_memory_demo(service)\n    # await run_skill_extraction_demo(service)\n    # await run_multimodal_demo(service)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/getting_started_robust.py",
    "content": "\"\"\"\nGetting Started with MemU: A Robust Example.\n\nThis script demonstrates the core lifecycle of MemU:\n1.  **Initialization**: Setting up the client with secure API key handling.\n2.  **Memory Injection**: Adding a specific memory with metadata.\n3.  **Retrieval**: Searching for that memory using natural language.\n4.  **Error Handling**: Catching common configuration issues.\n\nUsage:\n    export OPENAI_API_KEY=your_api_key_here\n    python examples/getting_started_robust.py\n\"\"\"\n\nimport asyncio\nimport logging\nimport os\nimport sys\n\n# Ensure src is in the path for local usage if custom installing\nsys.path.insert(0, os.path.abspath(\"src\"))\n\nfrom memu.app import MemoryService\n\n# Configure logging to show info but suppress noisy libraries\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s - %(levelname)s - %(message)s\")\nlogging.getLogger(\"httpx\").setLevel(logging.WARNING)\n\n\nasync def main() -> None:\n    \"\"\"Run the MemU lifecycle demonstration.\"\"\"\n    print(\">>> MemU Getting Started Example\")\n    print(\"-\" * 30)\n\n    # 1. API Key Handling\n    # MemU relies on an LLM backend (defaulting to OpenAI).\n    # We ensure the API key is present before proceeding.\n    api_key = os.getenv(\"OPENAI_API_KEY\")\n    if not api_key:\n        print(\"[!] Error: OPENAI_API_KEY environment variable is not set.\")\n        print(\"Please export it: export OPENAI_API_KEY=sk-...\")\n        return\n\n    try:\n        # 2. Initialization\n        # We initialize the MemoryService with:\n        # - llm_profiles: Configuration for the LLM (model, api_key).\n        # - memorize_config: Pre-defining a memory category ensures we can organize memories efficiently.\n        print(\"[*] Initializing MemoryService with model: gpt-4o-mini...\")\n        service = MemoryService(\n            llm_profiles={\n                \"default\": {\n                    \"api_key\": api_key,\n                    \"chat_model\": \"gpt-4o-mini\",\n                },\n            },\n            memorize_config={\n                \"memory_categories\": [\n                    {\n                        \"name\": \"User Facts\",\n                        \"description\": \"General and specific facts known about the user preference and identity.\",\n                    }\n                ]\n            },\n        )\n        print(\"[OK] Service initialized successfully.\\n\")\n\n        # 3. Memory Injection\n        # We manually inject a memory into the system.\n        # This is useful for bootstrapping a user profile or adding explicit knowledge.\n        print(\"[*] Injecting memory...\")\n        memory_content = \"The user is a senior Python architect who loves clean code and type hints.\"\n\n        # We use 'create_memory_item' to insert a single memory record.\n        # memory_type='profile' indicates this is an attribute of the user.\n        result = await service.create_memory_item(\n            memory_type=\"profile\",\n            memory_content=memory_content,\n            memory_categories=[\"User Facts\"],\n        )\n        print(f\"[OK] Memory created! ID: {result.get('memory_item', {}).get('id')}\\n\")\n\n        # 4. Retrieval\n        # Now we query the system naturally to see if it recalls the information.\n        query_text = \"What kind of code does the user like?\"\n        print(f\"[*] Querying: '{query_text}'\")\n\n        search_results = await service.retrieve(queries=[{\"role\": \"user\", \"content\": query_text}])\n\n        # 5. Display Results\n        items = search_results.get(\"items\", [])\n        if items:\n            print(f\"[OK] Found {len(items)} relevant memory item(s):\")\n            for idx, item in enumerate(items, 1):\n                print(f\"   {idx}. {item.get('summary')} (Type: {item.get('memory_type')})\")\n        else:\n            print(\"[!] No relevant memories found.\")\n\n    except Exception as e:\n        print(f\"\\n[!] An error occurred during execution: {e}\")\n        logging.exception(\"Detailed traceback:\")\n    finally:\n        print(\"\\n[=] Example execution finished.\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/langgraph_demo.py",
    "content": "\"\"\"Demo script for MemU LangGraph Integration.\"\"\"\n\nimport asyncio\nimport logging\nimport os\nimport sys\n\n# Try imports and fail proactively if missing\ntry:\n    import langgraph  # noqa: F401\n    from langchain_core.tools import BaseTool\n\n    from memu.app.service import MemoryService\n    from memu.integrations.langgraph import MemULangGraphTools\nexcept ImportError:\n    print(\"Missing dependencies. Please run: uv sync --extra langgraph\")\n    sys.exit(1)\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(\"langgraph_demo\")\n\n\nasync def initialize_infrastructure() -> MemULangGraphTools:\n    \"\"\"Initialize the MemoryService and the LangGraph adapter.\"\"\"\n    # Ensure OPENAI_API_KEY is present\n    if not os.environ.get(\"OPENAI_API_KEY\"):\n        logger.warning(\"OPENAI_API_KEY not found in environment variables.\")\n\n    # In a real scenario, you might load config from file or env\n    service = MemoryService()\n    return MemULangGraphTools(service)\n\n\nasync def process_conversation(tools: list[BaseTool], user_id: str) -> None:\n    \"\"\"Simulate a conversation where memory is saved.\"\"\"\n    save_tool = next(t for t in tools if t.name == \"save_memory\")\n\n    logger.info(\"--- Simulating Save Memory ---\")\n    inputs = {\n        \"content\": \"The user prefers dark mode and likes Python programming.\",\n        \"user_id\": user_id,\n        \"metadata\": {\"source\": \"demo_script\"},\n    }\n    # Invoke the tool (async execution)\n    result = await save_tool.ainvoke(inputs)\n    logger.info(\"Save Result: %s\", result)\n\n\nasync def process_retrieval(tools: list[BaseTool], user_id: str) -> None:\n    \"\"\"Simulate retrieving memory.\"\"\"\n    search_tool = next(t for t in tools if t.name == \"search_memory\")\n\n    logger.info(\"--- Simulating Search Memory ---\")\n    inputs = {\"query\": \"What are the user's preferences?\", \"user_id\": user_id, \"limit\": 3}\n    result = await search_tool.ainvoke(inputs)\n    logger.info(\"Search Result:\\n%s\", result)\n\n\nasync def main() -> None:\n    \"\"\"Main entry point.\"\"\"\n    logger.info(\"Starting LangGraph Demo...\")\n\n    adapter = await initialize_infrastructure()\n    tools = adapter.tools()\n\n    user_id = \"demo_user_123\"\n\n    await process_conversation(tools, user_id)\n    await process_retrieval(tools, user_id)\n\n    logger.info(\"Demo completed.\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/output/conversation_example/activities.md",
    "content": "# activities\n## Open Source Contributions\n- The user enjoys contributing to open source projects, specifically a Python CLI tool used for automating deployment tasks.\n## Running\n- The user usually goes for a run every morning and is interested in running routes near downtown San Francisco.\n## Dining Plans\n- The user plans to try several vegetarian restaurants in San Francisco for their partner, who is vegetarian.\n## Gym\n- The user goes to the gym 3-4 times a week, usually after work around 7 PM.\n"
  },
  {
    "path": "examples/output/conversation_example/experiences.md",
    "content": "# experiences\n## User Experiences\n- The user has been programming for about 5 years.\n- The user is leading a big product launch next month, which is causing work-related stress that is affecting their sleep schedule.\n- The user usually goes to bed around 11 PM but finds themselves awake thinking about work projects.\n"
  },
  {
    "path": "examples/output/conversation_example/goals.md",
    "content": "# goals\n## Learning Objectives\n- The user is interested in learning more about system design and scalability patterns.\n- The user is learning about event-driven architecture and message queues, specifically using Apache Kafka for event streaming.\n\n## Product Development Goals\n- The user wants to build their own SaaS product related to developer tools or automation.\n- The user is currently in the research phase for their product idea, reading about OpenAPI specifications and exploring tools like Swagger and Postman.\n- The user plans to start prototyping their SaaS product idea in the next few months.\n\n## Tool Development Ideas\n- The user is considering building a tool for API testing and documentation that automatically generates test cases based on API specifications.\n- The user is researching tools for developing the API tool.\n\n## Work-Life Balance Goals\n- The user plans to talk to their team about response time expectations for emails, as they believe they are putting pressure on themselves.\n- The user expresses a desire to establish better work-life boundaries and has been considering ways to limit work-related activities during the evening.\n"
  },
  {
    "path": "examples/output/conversation_example/habits.md",
    "content": "# habits\n## Eating Habits\n- The user is trying to eat less meat.\n## Exercise Routine\n- The user usually goes for a run every morning.\n- The user exercises regularly, going to the gym 3-4 times a week.\n## Sleep Habits\n- The user has been having trouble sleeping due to work stress.\n- The user usually tries to go to bed around 11 PM but struggles to fall asleep due to stress from work projects.\n- The user checks their phone before bed, which may impact their sleep quality.\n- The user used to read before bed but stopped because they were always checking work emails.\n## Caffeine Consumption\n- The user usually drinks coffee throughout the day to stay alert, typically having their last coffee around 3-4 PM.\n"
  },
  {
    "path": "examples/output/conversation_example/knowledge.md",
    "content": "# knowledge\n## Reading Interests\n- The user has been reading about OpenAPI specifications and exploring tools like Swagger and Postman.\n## Technology Stack\n- Alex's technology stack includes Django and FastAPI for Python services, Kubernetes for orchestration, Redis for caching, and Apache Kafka for event streaming.\n## Interests\n- Alex has an interest in system design and scalability patterns and is learning about event-driven architecture and message queues.\n- For food in San Francisco, check out the Ferry Building Marketplace for gourmet options and local produce.\n- Golden Gate Park, Lands End, and Muir Woods are recommended for nature activities in San Francisco.\n- A self-guided tour of tech headquarters in Silicon Valley and the Computer History Museum are good tech-related activities in San Francisco.\n- The de Young Museum and SFMOMA are suitable for photography and museum interests in San Francisco.\n## Monitoring and Observability\n- For monitoring and observability, Alex works with Prometheus and Grafana.\n## Sleep and Wellness\n- Work stress can negatively affect sleep quality, especially when one is in a leadership role.\n- Using a phone before bed can disrupt sleep due to blue light, which suppresses melatonin production.\n- Regular exercise can improve sleep, but working out close to bedtime may be stimulating.\n- Caffeine consumption, particularly in the afternoon, can adversely affect sleep quality.\n- Establishing a wind-down routine, such as reading or gentle stretching before bed, can help improve sleep.\n- Keeping a phone out of the bedroom can create a boundary between work and sleep, improving sleep quality.\n- Setting specific 'no work' hours can help transition from work mode to rest mode, reducing stress.\n## Activities and Recommendations\n- The best time to visit Muir Woods is early in the morning around 8 AM to avoid crowds and enjoy peacefulness.\n- Parking reservations at Muir Woods are required and should be booked in advance.\n- Recommended vegetarian restaurants in San Francisco include Greens Restaurant, Nourish Cafe, Shizen, Gracias Madre, and Ike's Love & Sandwiches.\n- The Embarcadero is a great flat waterfront path for morning runs in San Francisco.\n- Crissy Field offers stunning views of the Golden Gate Bridge and is another good running spot.\n- Running to Coit Tower from North Beach provides a more challenging route with hills.\n- San Francisco weather can be changeable, particularly across different neighborhoods, so packing layers is advisable.\n"
  },
  {
    "path": "examples/output/conversation_example/opinions.md",
    "content": "*No content available*\n"
  },
  {
    "path": "examples/output/conversation_example/personal_info.md",
    "content": "# personal_info\n## Basic Information\n- The user is named Alex and works as a software engineer at TechCorp.\n"
  },
  {
    "path": "examples/output/conversation_example/preferences.md",
    "content": "# preferences\n## Interests\n- The user loves food and nature.\n- The user enjoys exploring tech companies due to their background in software development.\n- The user is interested in vegetarian options as their partner is vegetarian and the user is trying to eat less meat.\n- The user likes reading and used to read before bed.\n"
  },
  {
    "path": "examples/output/conversation_example/relationships.md",
    "content": "# relationships\n## partner\n- The user's partner enjoys photography and museums.\n"
  },
  {
    "path": "examples/output/conversation_example/work_life.md",
    "content": "# work_life\n## Professional Background\n- The user is a software engineer at TechCorp.\n- Alex is a software engineer with approximately 5 years of programming experience.\n- The user works in software development.\n## Current Responsibilities\n- The user primarily works on backend systems using Python and Go for an e-commerce platform.\n- At TechCorp, Alex's team is building a distributed microservices architecture for their e-commerce platform.\n- The user uses Django and FastAPI for Python services and is migrating to Go for better performance.\n- The user works with Kubernetes for orchestration and Redis for caching.\n- The user is responsible for monitoring and observability, using Prometheus and Grafana.\n- The user is leading a big product launch next month.\n"
  },
  {
    "path": "examples/proactive/memory/config.py",
    "content": "memorize_config = {\n    \"memory_types\": [\n        \"record\",\n    ],\n    \"memory_type_prompts\": {\n        \"record\": {\n            \"objective\": {\n                \"ordinal\": 10,\n                \"prompt\": \"# Task Objective\\nYou will be given a conversation between a user and an coding agent. Your goal is to extract detailed records for what are planed to do, and what have been done.\",\n            },\n            \"workflow\": {\n                \"ordinal\": 20,\n                \"prompt\": \"# Workflow\\nRead through the conversation and extract records. You should expecially focus on:\\n- What the user ask the agent to do\\n- What plan does the agent suggest\\n- What the agent has done\",\n            },\n            \"rules\": {\n                \"ordinal\": -1,\n                \"prompt\": None,\n            },\n            \"examples\": {\n                \"ordinal\": 60,\n                \"prompt\": \"# Example\\n## Output\\n<item>\\n    <memory>\\n        <content>The user ask the agent to generate a code example for fastapi</content>\\n        <categories>\\n            <category>todo</category>\\n        </categories>\\n    </memory>\\n    <memory>\\n        <content>The agent suggest to use the code example from the document</content>\\n        <categories>\\n            <category>todo</category>\\n        </categories>\\n    </memory>\\n    <memory>\\n        <content>The agent ask the user to specify the response type</content>\\n        <categories>\\n            <category>todo</category>\\n        </categories>\\n    </memory>\\n</item>\",\n            },\n        }\n    },\n    \"memory_categories\": [\n        {\n            \"name\": \"todo\",\n            \"description\": \"This file traces the latest status of the task. All records should be included in this file.\",\n            \"target_length\": None,\n            \"custom_prompt\": {\n                \"objective\": {\n                    \"ordinal\": 10,\n                    \"prompt\": \"# Task Objective\\nYou are a specialist in task management. You should update the markdown file to reflect the latest status of the task.\",\n                },\n                \"workflow\": {\n                    \"ordinal\": 20,\n                    \"prompt\": \"# Workflow\\nRead through the existing markdown file and the new records. Then update the markdown file to reflect:\\n- What existing tasks are completed\\n- What new tasks are added\\n- What tasks are still in progress\",\n                },\n                \"rules\": {\n                    \"ordinal\": 30,\n                    \"prompt\": \"# Rules\\nFor each action-like record, explictly mark it as [Done] or [Todo].\",\n                },\n                \"examples\": {\n                    \"ordinal\": 50,\n                    \"prompt\": \"# Example\\n## Output\\n```markdown\\n# Task\\n## Task Objective\\nThe user ask the agent to generate a code example for fastapi\\n## Breakdown\\n- [Done] The agent suggest to use the code example from the document\\n- [Todo] The agent ask the user to specify the response type\\n```\",\n                },\n            },\n        }\n    ],\n}\n\nretrieve_config = {\n    \"method\": \"rag\",\n    \"route_intention\": False,\n    \"sufficiency_check\": False,\n    \"category\": {\n        \"enabled\": False,\n    },\n    \"item\": {\n        \"enabled\": True,\n        \"top_k\": 10,\n    },\n    \"resource\": {\n        \"enabled\": False,\n    },\n}\n"
  },
  {
    "path": "examples/proactive/memory/local/__init__.py",
    "content": ""
  },
  {
    "path": "examples/proactive/memory/local/common.py",
    "content": "import os\n\nfrom memu.app import MemoryService\n\nfrom ..config import memorize_config, retrieve_config\n\nUSER_ID = \"claude_user\"\nSHARED_MEMORY_SERVICE = None\n\n\ndef get_memory_service() -> MemoryService:\n    global SHARED_MEMORY_SERVICE\n    if SHARED_MEMORY_SERVICE is not None:\n        return SHARED_MEMORY_SERVICE\n\n    api_key = os.getenv(\"OPENAI_API_KEY\")\n    if not api_key:\n        msg = \"Please set OPENAI_API_KEY environment variable\"\n        raise ValueError(msg)\n\n    SHARED_MEMORY_SERVICE = MemoryService(\n        llm_profiles={\n            \"default\": {\n                \"api_key\": api_key,\n                \"chat_model\": \"gpt-4o-mini\",\n            },\n        },\n        memorize_config=memorize_config,\n        retrieve_config=retrieve_config,\n    )\n    return SHARED_MEMORY_SERVICE\n"
  },
  {
    "path": "examples/proactive/memory/local/memorize.py",
    "content": "import json\nfrom collections.abc import Awaitable\nfrom pathlib import Path\nfrom typing import Any\n\nimport pendulum\n\nfrom .common import get_memory_service\n\nUSER_ID = \"claude_user\"\n\n\ndef dump_conversation_resource(\n    conversation_messages: list[dict[str, Any]],\n) -> str:\n    resource_data = {\n        \"content\": [\n            {\n                \"role\": message.get(\"role\", \"system\"),\n                \"content\": {\"text\": message.get(\"content\", \"\")},\n                \"created_at\": message.get(\"timestamp\", pendulum.now().isoformat()),\n            }\n            for message in conversation_messages\n        ]\n    }\n    time_string = pendulum.now().format(\"YYYYMMDD_HHmmss\")\n    resource_url = Path(__file__).parent / \"data\" / f\"conv_{time_string}.json\"\n    resource_url.parent.mkdir(parents=True, exist_ok=True)\n    with open(resource_url, \"w\") as f:\n        json.dump(resource_data, f, indent=4, ensure_ascii=False)\n    return resource_url.as_posix()\n\n\ndef memorize(conversation_messages: list[dict[str, Any]]) -> Awaitable[dict[str, Any]]:\n    memory_service = get_memory_service()\n\n    resource_url = dump_conversation_resource(conversation_messages)\n    return memory_service.memorize(resource_url=resource_url, modality=\"conversation\", user={\"user_id\": USER_ID})\n"
  },
  {
    "path": "examples/proactive/memory/local/tools.py",
    "content": "from typing import Any\n\nfrom claude_agent_sdk import create_sdk_mcp_server, tool\n\nfrom .common import get_memory_service\n\nUSER_ID = \"claude_user\"\n\n\n@tool(\"memu_memory\", \"Retrieve memory based on a query\", {\"query\": str})\nasync def get_memory(args: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Retrieve memory from the memory API based on the provided query.\"\"\"\n    query = {\"role\": \"user\", \"content\": args[\"query\"]}\n\n    memory_service = get_memory_service()\n\n    result = await memory_service.retrieve(query, where={\"user_id\": USER_ID})\n\n    return {\"content\": [{\"type\": \"text\", \"text\": str(result)}]}\n\n\nasync def _get_todos() -> str:\n    memory_service = get_memory_service()\n\n    result = await memory_service.list_memory_categories(where={\"user_id\": USER_ID})\n\n    categories = result[\"categories\"]\n    todos = \"\"\n    for category in categories:\n        if category[\"name\"] == \"todo\":\n            todos = category[\"summary\"]\n    return todos\n\n\n@tool(\"memu_todos\", \"Retrieve todos for the user\", {})\nasync def get_todos() -> dict[str, Any]:\n    \"\"\"Retrieve todos from the memory API.\"\"\"\n    todos = await _get_todos()\n    return {\"content\": [{\"type\": \"text\", \"text\": str(todos)}]}\n\n\nmemu_server = create_sdk_mcp_server(name=\"memu\", version=\"1.0.0\", tools=[get_memory, get_todos])\n"
  },
  {
    "path": "examples/proactive/memory/platform/__init__.py",
    "content": ""
  },
  {
    "path": "examples/proactive/memory/platform/memorize.py",
    "content": "from typing import Any\n\nimport aiohttp\n\nfrom ..config import memorize_config\n\nBASE_URL = \"https://api.memu.so\"\nAPI_KEY = \"your memu api key\"\nUSER_ID = \"claude_user\"\nAGENT_ID = \"claude_agent\"\n\n\nasync def memorize(conversation_messages: list[dict[str, Any]]) -> str | None:\n    payload = {\n        \"conversation\": conversation_messages,\n        \"user_id\": USER_ID,\n        \"agent_id\": AGENT_ID,\n        \"override_config\": memorize_config,\n    }\n\n    async with (\n        aiohttp.ClientSession() as session,\n        session.post(\n            f\"{BASE_URL}/api/v3/memory/memorize\",\n            headers={\"Authorization\": f\"Bearer {API_KEY}\"},\n            json=payload,\n        ) as response,\n    ):\n        result = await response.json()\n        task_id = result[\"task_id\"]\n        return task_id\n"
  },
  {
    "path": "examples/proactive/memory/platform/tools.py",
    "content": "from typing import Any\n\nimport aiohttp\nfrom claude_agent_sdk import create_sdk_mcp_server, tool\n\nBASE_URL = \"https://api.memu.so\"\nAPI_KEY = \"your memu api key\"\nUSER_ID = \"claude_user\"\nAGENT_ID = \"claude_agent\"\n\n\n@tool(\"memu_memory\", \"Retrieve memory based on a query\", {\"query\": str})\nasync def get_memory(args: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Retrieve memory from the memory API based on the provided query.\"\"\"\n    query = args[\"query\"]\n    url = f\"{BASE_URL}/api/v3/memory/retrieve\"\n    headers = {\"Authorization\": f\"Bearer {API_KEY}\"}\n    data = {\"user_id\": USER_ID, \"agent_id\": AGENT_ID, \"query\": query}\n\n    async with aiohttp.ClientSession() as session, session.post(url, headers=headers, json=data) as response:\n        result = await response.json()\n\n    return {\"content\": [{\"type\": \"text\", \"text\": str(result)}]}\n\n\nasync def _get_todos() -> str:\n    url = f\"{BASE_URL}/api/v3/memory/categories\"\n    headers = {\"Authorization\": f\"Bearer {API_KEY}\"}\n    data = {\n        \"user_id\": USER_ID,\n        \"agent_id\": AGENT_ID,\n    }\n    async with aiohttp.ClientSession() as session, session.post(url, headers=headers, json=data) as response:\n        result = await response.json()\n\n    categories = result[\"categories\"]\n    todos = \"\"\n    for category in categories:\n        if category[\"name\"] == \"todo\":\n            todos = category[\"summary\"]\n    return todos\n\n\n@tool(\"memu_todos\", \"Retrieve todos for the user\", {})\nasync def get_todos() -> dict[str, Any]:\n    \"\"\"Retrieve todos from the memory API.\"\"\"\n    todos = await _get_todos()\n    return {\"content\": [{\"type\": \"text\", \"text\": str(todos)}]}\n\n\n# Create the MCP server with the tool\nmemu_server = create_sdk_mcp_server(name=\"memu\", version=\"1.0.0\", tools=[get_memory, get_todos])\n"
  },
  {
    "path": "examples/proactive/proactive.py",
    "content": "import asyncio\n\nfrom claude_agent_sdk import (\n    AssistantMessage,\n    ClaudeAgentOptions,\n    ClaudeSDKClient,\n    ResultMessage,\n    TextBlock,\n)\nfrom memory.local.memorize import memorize\nfrom memory.local.tools import _get_todos, memu_server\n\n# Set your Anthropic API key here if it's not set in the environment variables\n# os.environ[\"ANTHROPIC_API_KEY\"] = \"\"\n\nN_MESSAGES_MEMORIZE = 2\nRUNNING_MEMORIZATION: asyncio.Task | None = None\n\n\nasync def trigger_memorize(messages: list[dict[str, any]]) -> bool:\n    \"\"\"Create a background task to memorize conversation messages.\n\n    Returns True if the task was successfully created and registered.\n    \"\"\"\n    global RUNNING_MEMORIZATION\n    try:\n        memorize_awaitable = memorize(messages)\n        RUNNING_MEMORIZATION = asyncio.create_task(memorize_awaitable)\n    except Exception as e:\n        print(f\"\\n[Memory] Memorization initialization failed: {e!r}\")\n        return False\n    else:\n        print(\"\\n[Memory] Memorization task submitted.\")\n        return True\n\n\nasync def get_next_input(iteration: int) -> tuple[str | None, bool]:\n    \"\"\"\n    Get the next input for the conversation.\n\n    Returns:\n        tuple of (input_text, should_break)\n        - input_text: The user input or todo-based input, None if should continue\n        - should_break: True if the loop should break\n    \"\"\"\n    if iteration == 0:\n        return await get_user_input()\n\n    todos = await _get_todos()\n\n    print(f\">>> Todos:\\n{todos}\\n\")\n    print(\"-\" * 40)\n\n    if todos and \"[todo]\" in todos.lower():\n        return f\"Please continue with the following todos:\\n{todos}\", False\n\n    return await get_user_input()\n\n\nasync def get_user_input() -> tuple[str | None, bool]:\n    \"\"\"\n    Get input from the user.\n\n    Returns:\n        tuple of (input_text, should_break)\n    \"\"\"\n    try:\n        user_input = input(\"\\nYou: \").strip()\n    except EOFError:\n        return None, True\n\n    if not user_input:\n        return None, False\n\n    if user_input.lower() in (\"quit\", \"exit\"):\n        return None, True\n\n    return user_input, False\n\n\nasync def process_response(client: ClaudeSDKClient) -> list[str]:\n    \"\"\"Process the assistant response and return collected text parts.\"\"\"\n    assistant_text_parts: list[str] = []\n\n    async for message in client.receive_response():\n        if isinstance(message, AssistantMessage):\n            for block in message.content:\n                if isinstance(block, TextBlock):\n                    print(f\"Claude: {block.text}\")\n                    assistant_text_parts.append(block.text)\n        elif isinstance(message, ResultMessage):\n            print(f\"Result: {message.result}\")\n\n    return assistant_text_parts\n\n\nasync def check_and_memorize(conversation_messages: list[dict[str, any]]) -> None:\n    \"\"\"Check if memorization threshold is reached and trigger if needed.\n\n    Skips triggering if a previous memorization task is still running.\n    \"\"\"\n    global RUNNING_MEMORIZATION\n\n    if len(conversation_messages) < N_MESSAGES_MEMORIZE:\n        return\n\n    # Check if there's a running memorization task\n    if RUNNING_MEMORIZATION is not None:\n        if not RUNNING_MEMORIZATION.done():\n            print(\"\\n[Info] Have running memorization, skipping...\")\n            return\n        # Previous task completed, check for exceptions\n        try:\n            RUNNING_MEMORIZATION.result()\n        except Exception as e:\n            print(f\"\\n[Memory] Memorization failed: {e!r}\")\n        RUNNING_MEMORIZATION = None\n\n    print(f\"\\n[Info] Reached {N_MESSAGES_MEMORIZE} messages, triggering memorization...\")\n    success = await trigger_memorize(conversation_messages.copy())\n    if success:\n        conversation_messages.clear()\n\n\nasync def run_conversation_loop(client: ClaudeSDKClient) -> list[dict[str, any]]:\n    \"\"\"Run the main conversation loop.\"\"\"\n    conversation_messages: list[dict[str, any]] = []\n    iteration = 0\n\n    while True:\n        user_input, should_break = await get_next_input(iteration)\n\n        if should_break:\n            break\n        if user_input is None:\n            continue\n\n        conversation_messages.append({\"role\": \"user\", \"content\": user_input})\n        await client.query(user_input)\n\n        assistant_text_parts = await process_response(client)\n\n        if assistant_text_parts:\n            conversation_messages.append({\n                \"role\": \"assistant\",\n                \"content\": \"\\n\".join(assistant_text_parts),\n            })\n\n        await check_and_memorize(conversation_messages)\n        iteration += 1\n\n    return conversation_messages\n\n\nasync def main():\n    options = ClaudeAgentOptions(\n        mcp_servers={\"memu\": memu_server},\n        allowed_tools=[\n            # \"mcp__memu__memu_memory\",\n            \"mcp__memu__memu_todos\",\n        ],\n    )\n\n    print(\"Claude Autorun\")\n    print(\"Type 'quit' or 'exit' to end the session.\")\n    print(\"-\" * 40)\n\n    async with ClaudeSDKClient(options=options) as client:\n        remaining_messages = await run_conversation_loop(client)\n\n    # Wait for any running memorization task to complete\n    global RUNNING_MEMORIZATION\n    if RUNNING_MEMORIZATION is not None and not RUNNING_MEMORIZATION.done():\n        print(\"\\n[Info] Waiting for running memorization task to complete...\")\n        try:\n            await RUNNING_MEMORIZATION\n            print(\"\\n[Memory] Running memorization completed successfully.\")\n        except Exception as e:\n            print(f\"\\n[Memory] Running memorization failed: {e!r}\")\n        RUNNING_MEMORIZATION = None\n\n    # Memorize remaining messages and wait for completion\n    if remaining_messages:\n        print(\"\\n[Info] Session ended, memorizing remaining messages...\")\n        success = await trigger_memorize(remaining_messages.copy())\n        if success and RUNNING_MEMORIZATION is not None:\n            print(\"\\n[Info] Waiting for final memorization to complete...\")\n            try:\n                await RUNNING_MEMORIZATION\n                print(\"\\n[Memory] Final memorization completed successfully.\")\n            except Exception as e:\n                print(f\"\\n[Memory] Final memorization failed: {e!r}\")\n\n    print(\"\\nDone\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/resources/conversations/conv1.json",
    "content": "[\n  {\n    \"role\": \"user\",\n    \"content\": \"Hi! I'm Alex. I'm a software engineer working at TechCorp. I've been programming for about 5 years now.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"content\": \"Nice to meet you, Alex! It's great to have you here. What kind of software development do you primarily work on at TechCorp?\"\n  },\n  {\n    \"role\": \"user\",\n    \"content\": \"I mainly work on backend systems using Python and Go. We're building a distributed microservices architecture for our e-commerce platform. I'm particularly interested in learning more about system design and scalability patterns.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"content\": \"That sounds like an exciting project! Distributed systems and microservices certainly present interesting challenges. Are you working with any specific frameworks or technologies for the microservices?\"\n  },\n  {\n    \"role\": \"user\",\n    \"content\": \"Yes, we use Django and FastAPI for Python services, and we're migrating some critical services to Go for better performance. We use Kubernetes for orchestration and Redis for caching. I've been learning about event-driven architecture and message queues lately.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"content\": \"Excellent technology stack! Event-driven architecture is a great pattern for microservices. Are you using any specific message broker like RabbitMQ or Kafka?\"\n  },\n  {\n    \"role\": \"user\",\n    \"content\": \"We're using Apache Kafka for our event streaming. I'm also responsible for monitoring and observability, so I work with Prometheus and Grafana. Outside of work, I like to contribute to open source projects. I recently started contributing to a Python CLI tool.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"content\": \"That's wonderful! Open source contributions are a great way to learn and give back to the community. What made you interested in contributing to that particular CLI tool?\"\n  },\n  {\n    \"role\": \"user\",\n    \"content\": \"I actually use it in my daily workflow for automating deployment tasks. I found a few bugs and thought I could help improve it. I also want to eventually build my own SaaS product, maybe something related to developer tools or automation.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"content\": \"That's a great goal! Having a SaaS product idea is exciting. Do you have any specific problem you're looking to solve with your developer tools idea?\"\n  },\n  {\n    \"role\": \"user\",\n    \"content\": \"I'm thinking about building a tool for API testing and documentation. Something that can automatically generate test cases based on API specifications and maintain up-to-date documentation. I notice this is a pain point in my current team.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"content\": \"That's a valuable problem to solve! Many teams struggle with keeping API documentation synchronized with their codebase. Have you done any market research or prototyping yet?\"\n  },\n  {\n    \"role\": \"user\",\n    \"content\": \"Not yet, I'm still in the research phase. I've been reading about OpenAPI specifications and exploring tools like Swagger and Postman. I think there's room for a more automated and AI-powered solution. I'm planning to start prototyping in the next few months.\"\n  }\n]\n"
  },
  {
    "path": "examples/resources/conversations/conv2.json",
    "content": "[\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-02-01T09:00:00Z\",\n    \"content\": \"I need help planning a weekend trip to San Francisco. Can you suggest some activities?\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-02-01T09:00:08Z\",\n    \"content\": \"I'd be happy to help you plan your San Francisco trip! What are your main interests? Are you into tech, food, nature, art, or something else?\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-02-01T09:00:25Z\",\n    \"content\": \"I love food and nature! I'm also interested in visiting some tech companies since I work in software development. My partner enjoys photography and museums.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-02-01T09:00:40Z\",\n    \"content\": \"Perfect! For food, you should definitely check out the Ferry Building Marketplace for gourmet options and local produce. For nature, I'd recommend Golden Gate Park, hiking at Lands End, or visiting Muir Woods for the redwoods. For tech, you could do a self-guided tour of tech headquarters in Silicon Valley or visit the Computer History Museum. For your partner, there's the de Young Museum in Golden Gate Park and SFMOMA for modern art.\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-02-01T09:01:20Z\",\n    \"content\": \"Those sound great! I'm particularly excited about Muir Woods. I've never seen redwoods before. What's the best time to visit?\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-02-01T09:01:35Z\",\n    \"content\": \"Muir Woods is magical! I'd recommend going early in the morning, ideally right when it opens around 8 AM. This way you'll avoid the crowds and experience the peaceful atmosphere of the forest. The morning light filtering through the redwoods is also spectacular for photography. Make sure to book parking reservations in advance as they're required.\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-02-01T09:02:10Z\",\n    \"content\": \"Good to know about the parking! Are there any good vegetarian restaurants you'd recommend? My partner is vegetarian and I'm trying to eat less meat.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-02-01T09:02:25Z\",\n    \"content\": \"San Francisco has amazing vegetarian options! I'd recommend Greens Restaurant for upscale vegetarian with a view, Nourish Cafe for healthy bowls, and Shizen for vegan sushi. If you're in the Mission District, try Gracias Madre for organic Mexican food. For something casual, Ike's Love & Sandwiches has great veggie options.\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-02-01T09:03:05Z\",\n    \"content\": \"Perfect! I'll add these to my list. One more thing - I usually go for a run every morning. Are there good running routes near downtown?\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-02-01T09:03:20Z\",\n    \"content\": \"Absolutely! The Embarcadero is perfect for morning runs - it's a flat waterfront path with beautiful bay views. You can run from the Ferry Building towards Fisherman's Wharf, which is about 3 miles one way. Alternatively, Crissy Field offers stunning views of the Golden Gate Bridge. If you want something more challenging with hills, try running up to Coit Tower from North Beach.\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-02-01T09:03:55Z\",\n    \"content\": \"The Embarcadero sounds perfect for my morning routine. Thanks for all the suggestions! I'm getting really excited about this trip now.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-02-01T09:04:05Z\",\n    \"content\": \"You're welcome! It sounds like you have a great mix of activities planned. Don't forget to bring layers - San Francisco weather can be quite changeable, especially if you're visiting different neighborhoods. Have a wonderful trip!\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-02-01T09:04:30Z\",\n    \"content\": \"Will do! I always forget how different the weather can be in different parts of the city. I'll pack accordingly.\"\n  }\n]\n"
  },
  {
    "path": "examples/resources/conversations/conv3.json",
    "content": "[\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-03-10T22:00:00Z\",\n    \"content\": \"I've been having trouble sleeping lately. I think work stress might be affecting my sleep schedule.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-03-10T22:00:12Z\",\n    \"content\": \"I'm sorry to hear you're having trouble sleeping. Work stress can definitely impact sleep quality. Can you tell me more about your current sleep routine and what's been going on at work?\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-03-10T22:00:45Z\",\n    \"content\": \"I usually try to go to bed around 11 PM, but I find myself lying awake thinking about work projects. I'm leading a big product launch next month and there's a lot of pressure. I also tend to check my phone before bed.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-03-10T22:01:05Z\",\n    \"content\": \"It's common for work pressure to interfere with sleep, especially when you're in a leadership role. The phone usage before bed could also be contributing to the problem - the blue light can suppress melatonin production. Have you tried any relaxation techniques or sleep hygiene practices?\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-03-10T22:01:40Z\",\n    \"content\": \"I tried meditation once but didn't stick with it. I do exercise regularly - I go to the gym 3-4 times a week, usually after work around 7 PM. I also drink coffee throughout the day to stay alert.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-03-10T22:01:55Z\",\n    \"content\": \"Exercise is great for sleep, but working out too close to bedtime can actually be stimulating. The caffeine consumption throughout the day could also be affecting your sleep, especially if you're having coffee in the afternoon. Could you try moving your workouts to the morning and cutting off caffeine after 2 PM?\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-03-10T22:02:25Z\",\n    \"content\": \"That makes sense. I usually have my last coffee around 3-4 PM. Maybe I should switch to decaf in the afternoon. Morning workouts might be tough though - I'm not really a morning person.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-03-10T22:02:40Z\",\n    \"content\": \"Starting with the caffeine cutoff is a good first step! As for morning workouts, you don't have to make a drastic change immediately. Even a 15-minute walk in the morning can help regulate your circadian rhythm. For the evening, you might want to establish a wind-down routine - maybe some light reading, gentle stretching, or breathing exercises about an hour before bed.\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-03-10T22:03:20Z\",\n    \"content\": \"I like reading, actually. I used to read before bed but stopped because I was always checking work emails. Maybe I should keep my phone out of the bedroom entirely.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-03-10T22:03:35Z\",\n    \"content\": \"That's an excellent idea! Keeping your phone out of the bedroom can make a big difference. You could get a regular alarm clock if you use your phone for that. Creating a clear boundary between work and sleep time is important, especially with the product launch stress.\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-03-10T22:04:10Z\",\n    \"content\": \"You're right. I think I need to be better about work-life boundaries in general. I often answer emails late at night, which probably doesn't help my stress levels.\"\n  },\n  {\n    \"role\": \"assistant\",\n    \"created_at\": \"2025-03-10T22:04:25Z\",\n    \"content\": \"Those late-night emails can definitely keep your mind in 'work mode' when it should be winding down. Have you considered setting specific 'no work' hours, maybe from 8 PM onwards? That way you give your brain time to transition from work mode to rest mode.\"\n  },\n  {\n    \"role\": \"user\",\n    \"created_at\": \"2025-03-10T22:04:55Z\",\n    \"content\": \"That sounds reasonable. I'll talk to my team about response time expectations. I don't think anyone expects instant replies at night anyway - I'm probably putting that pressure on myself.\"\n  }\n]\n"
  },
  {
    "path": "examples/resources/docs/doc1.txt",
    "content": "MemU: Agentic Memory Framework - Technical Documentation\n\nIntroduction\n============\nMemU is a sophisticated agentic memory framework designed to provide AI agents and applications with human-like memory capabilities. Unlike traditional RAG (Retrieval-Augmented Generation) systems that simply store and retrieve information, MemU organizes, categorizes, and maintains memories in a structured, semantically meaningful way.\n\nCore Concepts\n=============\n\n1. Memory Organization\n   MemU organizes information into several layers:\n   - Memory Items: Individual pieces of information extracted from inputs\n   - Memory Categories: Semantic groupings of related memories\n   - Memory Types: Classifications of memory content (profile, event, knowledge, behavior)\n\n2. Multi-Modal Support\n   The framework supports various input modalities:\n   - Text documents (PDF, TXT, DOC)\n   - Conversations (JSON chat logs)\n   - Images (PNG, JPG, with vision model integration)\n   - Audio (transcription and processing)\n   - Video (frame extraction and analysis)\n\n3. Intelligent Processing Pipeline\n   Each input goes through several processing stages:\n   a. Preprocessing: Content extraction and normalization\n   b. Summarization: Key information extraction\n   c. Embedding: Vector representation generation\n   d. Classification: Memory type identification\n   e. Categorization: Semantic category assignment\n   f. Storage: Persistent storage with metadata\n\nArchitecture Components\n=======================\n\n1. MemoryService (Core Service Layer)\n   The main entry point for all memory operations:\n   - memorize(): Process and store new information\n   - retrieve(): Query and fetch relevant memories\n   - update(): Modify existing memories\n   - delete(): Remove memories\n\n   Configuration options:\n   - LLM provider settings (OpenAI, Azure, custom)\n   - Embedding model selection\n   - Memory type definitions\n   - Category templates\n   - Retrieval methods (RAG, LLM-based)\n\n2. Storage Layer\n   Multiple storage backends supported:\n   - SQLite (default, local development)\n   - PostgreSQL (production deployments)\n   - In-memory (testing and temporary storage)\n\n   Data persistence:\n   - Memory items with metadata\n   - Category definitions and summaries\n   - Vector embeddings for similarity search\n   - Resource references and URLs\n\n3. Vector Search Engine\n   Semantic search capabilities powered by:\n   - Dense embeddings (OpenAI text-embedding-3-small/large)\n   - Similarity metrics (cosine similarity, dot product)\n   - Efficient indexing for fast retrieval\n   - Hybrid search combining semantic and keyword matching\n\n4. LLM Integration Layer\n   Flexible LLM backend support:\n   - OpenAI SDK client (primary)\n   - HTTP-based client (custom endpoints)\n   - Configurable model selection\n   - Prompt template system\n   - Streaming response support\n\nMemory Types\n============\n\n1. Profile Memory\n   Stores persistent information about entities:\n   - Personal attributes (name, age, occupation)\n   - Preferences and interests\n   - Relationships and connections\n   - Identity and characteristics\n\n   Example: \"Alex is a software engineer at TechCorp, specializing in backend development\"\n\n2. Event Memory\n   Records discrete occurrences and activities:\n   - Temporal events with timestamps\n   - Actions and experiences\n   - Milestones and achievements\n   - Incidents and interactions\n\n   Example: \"Completed the deployment pipeline implementation on November 15, 2024\"\n\n3. Knowledge Memory\n   Captures factual information and learnings:\n   - Facts and concepts\n   - Skills and capabilities\n   - Domain expertise\n   - Technical knowledge\n\n   Example: \"Proficient in Python, Go, Kubernetes, and microservices architecture\"\n\n4. Behavior Memory\n   Tracks patterns and tendencies:\n   - Habits and routines\n   - Decision patterns\n   - Behavioral preferences\n   - Interaction styles\n\n   Example: \"Prefers morning workouts, typically exercises 3-4 times per week\"\n\nCategory Management\n===================\n\nDynamic Categorization:\nMemU automatically assigns memories to semantic categories based on content similarity. Categories are created and maintained dynamically as new memories are added.\n\nDefault Categories:\n- personal_info: Personal details and identity\n- preferences: Likes, dislikes, and choices\n- relationships: Connections with others\n- activities: Hobbies and interests\n- goals: Aspirations and objectives\n- experiences: Past events and learnings\n- knowledge: Facts and information\n- opinions: Views and perspectives\n- habits: Routines and patterns\n- work_life: Professional information\n\nCustom Categories:\nUsers can define custom categories with:\n- Name and description\n- Embedding vector for semantic matching\n- Assignment threshold for automatic categorization\n- Summary generation for category overview\n\nCategory Summaries:\nMemU maintains auto-generated summaries for each category that:\n- Provide overview of category contents\n- Get updated as new memories are added\n- Help with high-level information retrieval\n- Support category-level search\n\nRetrieval Strategies\n====================\n\n1. RAG-Based Retrieval (Default)\n   Vector similarity search approach:\n   - Query embedding generation\n   - Similarity calculation with stored memories\n   - Top-K selection per category\n   - Ranking by relevance score\n   - Context window assembly\n\n   Advantages:\n   - Fast and efficient\n   - Deterministic results\n   - Lower LLM costs\n   - Good for factual recall\n\n2. LLM-Based Retrieval\n   AI-powered search and ranking:\n   - Query understanding and expansion\n   - Semantic relevance judgment\n   - Context-aware ranking\n   - Multi-hop reasoning support\n   - Natural language result explanation\n\n   Advantages:\n   - Better semantic understanding\n   - Handles complex queries\n   - Context-aware results\n   - Flexible interpretation\n\nRetrieval Pipeline:\n1. Pre-retrieval decision (should we retrieve?)\n2. Query rewriting (optimize for search)\n3. Category ranking (which categories are relevant?)\n4. Item retrieval (fetch top-K items)\n5. Item ranking (rerank by relevance)\n6. Resource retrieval (fetch original sources)\n7. Result assembly (format for output)\n\nBest Practices\n==============\n\n1. Memory Quality\n   - Provide detailed, contextual inputs\n   - Include timestamps for events\n   - Maintain consistent terminology\n   - Regular memory consolidation\n   - Remove outdated information\n\n2. Configuration Optimization\n   - Tune embedding models for your domain\n   - Adjust category assignment thresholds\n   - Customize memory type prompts\n   - Set appropriate top-K values\n   - Configure LLM parameters\n\n3. Performance Optimization\n   - Batch memory operations when possible\n   - Use appropriate storage backend\n   - Index frequently queried fields\n   - Cache embeddings when reusing\n   - Monitor memory growth\n\n4. Privacy and Security\n   - Implement access controls\n   - Encrypt sensitive memories\n   - Regular data audits\n   - Compliance with data regulations\n   - User consent for memory storage\n\nUse Cases\n=========\n\n1. Personal AI Assistants\n   - Remember user preferences and context\n   - Maintain conversation history\n   - Learn from interactions\n   - Personalize responses\n\n2. Customer Support Systems\n   - Track customer history and issues\n   - Remember preferences and complaints\n   - Build customer profiles\n   - Improve service quality\n\n3. Educational Applications\n   - Track learning progress\n   - Remember concepts learned\n   - Adapt to learning style\n   - Provide personalized content\n\n4. Knowledge Management\n   - Organize organizational knowledge\n   - Track project information\n   - Build expertise databases\n   - Enable knowledge discovery\n\n5. Agent Workflows\n   - Maintain task context\n   - Remember tool usage patterns\n   - Learn from execution history\n   - Optimize decision making\n\nAPI Reference\n=============\n\nBasic Usage Example:\n\n```python\nfrom memu.app import MemoryService\n\n# Initialize service\nservice = MemoryService(\n    llm_config={\n        \"api_key\": \"your-api-key\",\n        \"chat_model\": \"gpt-4o-mini\"\n    }\n)\n\n# Store a memory\nresult = await service.memorize(\n    resource_url=\"conversation.json\",\n    modality=\"conversation\"\n)\n\n# Retrieve memories\nmemories = await service.retrieve(\n    query=\"What programming languages does Alex know?\",\n    top_k=5\n)\n\n# Access categories\ncategories = service.store.categories\n```\n\nAdvanced Configuration:\n\n```python\n# Custom memory types\nmemorize_config = {\n    \"memory_types\": [\"profile\", \"knowledge\", \"custom\"],\n    \"memory_type_prompts\": {\n        \"custom\": \"Extract specific information: {resource}\"\n    },\n    \"memory_categories\": [\n        {\"name\": \"technical_skills\", \"description\": \"Programming and technical abilities\"},\n        {\"name\": \"soft_skills\", \"description\": \"Communication and interpersonal skills\"}\n    ]\n}\n\nservice = MemoryService(\n    llm_config=llm_config,\n    memorize_config=memorize_config\n)\n```\n\nRoadmap\n=======\n\nUpcoming Features:\n- Long-term memory consolidation\n- Federated memory systems\n- Memory importance scoring\n- Automatic memory pruning\n- Cross-user memory sharing\n- Memory versioning and history\n- Enhanced temporal reasoning\n- Graph-based memory relationships\n- Memory export and import\n- Advanced privacy controls\n\nContributing\n============\n\nMemU is open source and welcomes contributions:\n- Bug reports and feature requests\n- Documentation improvements\n- Code contributions\n- Example applications\n- Performance optimizations\n\nFor more information, visit: https://github.com/mem-labs/memU\n"
  },
  {
    "path": "examples/resources/docs/doc2.txt",
    "content": "Building Production-Ready AI Agents: A Comprehensive Guide\n\nExecutive Summary\n=================\n\nThis document provides a comprehensive guide to building, deploying, and maintaining production-ready AI agents. It covers architecture patterns, best practices, common pitfalls, and real-world implementation strategies based on experiences from deploying agents at scale.\n\nWhat is an AI Agent?\n====================\n\nAn AI agent is an autonomous system that can:\n- Perceive its environment through sensors or data inputs\n- Make decisions based on goals and constraints\n- Take actions using available tools and APIs\n- Learn and adapt from experience\n- Operate with minimal human intervention\n\nKey characteristics that differentiate agents from simple LLM applications:\n1. Goal-directed behavior (not just reactive)\n2. Multi-step reasoning and planning\n3. Tool use and external API integration\n4. Memory and state management\n5. Error handling and recovery\n6. Continuous operation\n\nAgent Architecture Patterns\n============================\n\n1. ReAct (Reasoning + Acting) Pattern\n   The most common agent architecture:\n   - Thought: Reason about the current situation\n   - Action: Choose and execute a tool/action\n   - Observation: Observe the results\n   - Repeat until goal is achieved\n\n   Advantages:\n   - Interpretable decision-making process\n   - Easy to debug with visible reasoning\n   - Works well with modern LLMs\n\n   Challenges:\n   - Can be verbose and slow\n   - May get stuck in reasoning loops\n   - Token costs accumulate quickly\n\n2. Plan-and-Execute Pattern\n   Separates planning from execution:\n   - Generate high-level plan upfront\n   - Execute steps sequentially\n   - Replan if execution fails\n\n   Advantages:\n   - More efficient for complex tasks\n   - Better resource allocation\n   - Clearer progress tracking\n\n   Challenges:\n   - Less adaptable to changing conditions\n   - Planning failures cascade\n   - Requires good task decomposition\n\n3. Hierarchical Agent Systems\n   Multiple agents working together:\n   - Coordinator agent manages workflow\n   - Specialist agents handle specific domains\n   - Memory shared across agents\n\n   Advantages:\n   - Scalable to complex domains\n   - Parallel execution possible\n   - Clear separation of concerns\n\n   Challenges:\n   - Coordination overhead\n   - Complex error propagation\n   - Harder to debug\n\n4. Autonomous Agent Pattern\n   Continuous operation without explicit tasks:\n   - Monitor environment for triggers\n   - Self-generate tasks and goals\n   - Learn from outcomes\n   - Adjust behavior over time\n\n   Advantages:\n   - Truly autonomous operation\n   - Proactive rather than reactive\n   - Continuous improvement\n\n   Challenges:\n   - Harder to control and bound\n   - Safety and alignment concerns\n   - Resource management critical\n\nEssential Components\n====================\n\n1. Language Model Integration\n   Choosing the right LLM:\n   - GPT-4 family: Best reasoning, highest cost\n   - Claude: Strong safety, good reasoning\n   - Open source models: Lower cost, self-hosted\n\n   Optimization strategies:\n   - Use smaller models for simple tasks\n   - Implement prompt caching\n   - Batch similar requests\n   - Fine-tune for specific domains\n\n2. Tool Registry and Execution\n   Managing agent capabilities:\n   - Dynamic tool discovery\n   - Type-safe tool schemas\n   - Sandboxed execution environment\n   - Rate limiting and quotas\n   - Error handling and retries\n\n   Tool design principles:\n   - Single responsibility per tool\n   - Clear input/output contracts\n   - Idempotent when possible\n   - Detailed error messages\n   - Comprehensive documentation\n\n3. Memory Systems\n   Types of memory agents need:\n   - Working memory: Current task context\n   - Short-term memory: Recent interactions\n   - Long-term memory: Persistent knowledge\n   - Episodic memory: Past experiences\n   - Semantic memory: General knowledge\n\n   Implementation approaches:\n   - Vector databases for semantic search\n   - Graph databases for relationships\n   - Key-value stores for fast lookup\n   - SQL databases for structured data\n\n4. Planning and Decision Making\n   Strategies for complex tasks:\n   - Breadth-first vs depth-first search\n   - Heuristic-guided planning\n   - Monte Carlo tree search\n   - Reinforcement learning\n   - Symbolic reasoning integration\n\n5. Monitoring and Observability\n   Critical metrics to track:\n   - Task completion rate\n   - Average execution time\n   - Token usage and costs\n   - Error rates by type\n   - Tool usage patterns\n   - User satisfaction scores\n\n   Logging and tracing:\n   - Structured logging (JSON format)\n   - Distributed tracing (trace IDs)\n   - Action replay capabilities\n   - Performance profiling\n   - Anomaly detection\n\nProduction Deployment Considerations\n====================================\n\n1. Safety and Control\n   Implementing guardrails:\n   - Input validation and sanitization\n   - Output filtering and moderation\n   - Action confirmation for critical operations\n   - Rollback mechanisms\n   - Circuit breakers for cascading failures\n   - Human-in-the-loop for high-risk decisions\n\n2. Cost Management\n   Strategies to control expenses:\n   - Budget limits per agent/user\n   - Automatic degradation to cheaper models\n   - Request batching and caching\n   - Usage analytics and alerts\n   - Prompt optimization for token efficiency\n\n3. Latency Optimization\n   Reducing response time:\n   - Parallel tool execution when possible\n   - Streaming responses to users\n   - Pre-warming model connections\n   - Edge deployment for low latency\n   - Async processing for non-critical tasks\n\n4. Scalability\n   Handling growth:\n   - Stateless agent design\n   - Horizontal scaling with load balancers\n   - Queue-based task distribution\n   - Database sharding strategies\n   - Caching layers (Redis, Memcached)\n\n5. Reliability\n   Building fault-tolerant systems:\n   - Graceful degradation\n   - Automatic retry with exponential backoff\n   - Dead letter queues for failed tasks\n   - Health checks and auto-recovery\n   - Multi-region deployment\n\nCommon Pitfalls and Solutions\n=============================\n\n1. Infinite Loops\n   Problem: Agent gets stuck repeating same actions\n   Solutions:\n   - Implement maximum iteration limits\n   - Track state to detect loops\n   - Add randomization to break patterns\n   - Use reflection to detect stuck states\n\n2. Context Window Overflow\n   Problem: Conversation history exceeds model limits\n   Solutions:\n   - Implement context summarization\n   - Use sliding window approach\n   - Store full history, send summaries\n   - Priority-based context selection\n\n3. Tool Hallucination\n   Problem: Agent tries to use non-existent tools\n   Solutions:\n   - Provide clear tool documentation in prompt\n   - Validate tool names before execution\n   - Use structured output formats (JSON)\n   - Fine-tune on correct tool usage\n\n4. Inconsistent Behavior\n   Problem: Agent gives different results for same input\n   Solutions:\n   - Set temperature to 0 for determinism\n   - Implement result caching\n   - Add explicit reasoning chain requirements\n   - Use voting/consensus from multiple runs\n\n5. Poor Error Recovery\n   Problem: Single failures cause complete task abandonment\n   Solutions:\n   - Implement retry logic with backoff\n   - Fallback to alternative approaches\n   - Graceful degradation to partial results\n   - Clear error messages and recovery suggestions\n\nTesting Strategies\n==================\n\n1. Unit Testing\n   - Test individual tool functions\n   - Mock LLM responses\n   - Validate prompt templates\n   - Test parsing and formatting logic\n\n2. Integration Testing\n   - End-to-end task completion\n   - Tool chain execution\n   - Memory persistence\n   - API integration points\n\n3. Evaluation Benchmarks\n   - Task success rate\n   - Response quality (human eval)\n   - Reasoning coherence\n   - Tool usage efficiency\n   - Cost per task\n\n4. Adversarial Testing\n   - Malicious input handling\n   - Edge case scenarios\n   - Resource exhaustion attacks\n   - Prompt injection attempts\n\n5. A/B Testing\n   - Compare prompt variations\n   - Test different model versions\n   - Evaluate architecture changes\n   - Measure user satisfaction\n\nReal-World Use Cases\n====================\n\n1. Customer Support Agent\n   Capabilities:\n   - Answer common questions using knowledge base\n   - Create support tickets for complex issues\n   - Schedule appointments and callbacks\n   - Escalate to human agents when needed\n\n   Key challenges:\n   - Maintaining empathy in responses\n   - Handling frustrated customers\n   - Accurate issue classification\n   - Privacy and data security\n\n2. Research Assistant Agent\n   Capabilities:\n   - Search academic databases\n   - Summarize research papers\n   - Identify trends and gaps\n   - Generate literature reviews\n\n   Key challenges:\n   - Source credibility verification\n   - Citation accuracy\n   - Handling conflicting information\n   - Domain expertise requirements\n\n3. DevOps Automation Agent\n   Capabilities:\n   - Monitor system metrics\n   - Diagnose performance issues\n   - Execute remediation actions\n   - Generate incident reports\n\n   Key challenges:\n   - High stakes decision-making\n   - Complex system dependencies\n   - Security and access control\n   - Audit trail requirements\n\n4. Sales Prospecting Agent\n   Capabilities:\n   - Research potential customers\n   - Personalize outreach messages\n   - Schedule meetings\n   - Track engagement and follow-ups\n\n   Key challenges:\n   - Avoiding spam-like behavior\n   - Personalization at scale\n   - CRM integration complexity\n   - Compliance with regulations\n\nPerformance Optimization\n========================\n\n1. Prompt Engineering\n   - Use clear, structured instructions\n   - Provide relevant examples (few-shot)\n   - Include constraints and boundaries\n   - Optimize token usage\n   - Version control prompts\n\n2. Model Selection\n   - Match model capability to task complexity\n   - Use smaller models for simple tasks\n   - Consider latency requirements\n   - Balance cost vs performance\n   - Evaluate fine-tuning benefits\n\n3. Caching Strategies\n   - Cache LLM responses for common queries\n   - Cache tool results when appropriate\n   - Implement embeddings cache\n   - Use CDN for static resources\n   - Cache database queries\n\n4. Parallel Execution\n   - Identify independent tool calls\n   - Execute in parallel where possible\n   - Use async/await patterns\n   - Implement concurrent request limits\n   - Handle partial failures gracefully\n\nFuture Trends\n=============\n\n1. Multi-Modal Agents\n   - Vision, audio, and text integration\n   - Video understanding capabilities\n   - Embodied AI for robotics\n   - Mixed reality interactions\n\n2. Improved Planning\n   - Better long-term reasoning\n   - Hierarchical task decomposition\n   - Probabilistic planning under uncertainty\n   - Resource-constrained optimization\n\n3. Enhanced Memory\n   - Better long-term retention\n   - Efficient memory consolidation\n   - Personalization and adaptation\n   - Cross-agent knowledge sharing\n\n4. Tool Learning\n   - Automatic tool discovery\n   - Tool composition and chaining\n   - Learning from tool usage patterns\n   - Generating new tools dynamically\n\n5. Human-Agent Collaboration\n   - Natural delegation interfaces\n   - Explainable decision-making\n   - Interactive planning and refinement\n   - Shared mental models\n\nConclusion\n==========\n\nBuilding production-ready AI agents requires careful attention to architecture, safety, performance, and user experience. Success comes from:\n- Starting with clear, well-defined use cases\n- Iterating based on real-world feedback\n- Investing in monitoring and observability\n- Prioritizing safety and controllability\n- Continuously optimizing costs and performance\n\nThe agent paradigm represents a significant leap from traditional applications, but with thoughtful design and implementation, agents can deliver tremendous value while operating reliably at scale.\n\nReferences and Resources\n========================\n\nKey papers:\n- \"ReAct: Synergizing Reasoning and Acting in Language Models\" (Yao et al., 2022)\n- \"Toolformer: Language Models Can Teach Themselves to Use Tools\" (Schick et al., 2023)\n- \"Reflexion: Language Agents with Verbal Reinforcement Learning\" (Shinn et al., 2023)\n\nFrameworks and tools:\n- LangChain: Popular agent framework\n- AutoGPT: Autonomous agent implementation\n- BabyAGI: Task-driven autonomous agent\n- AgentGPT: Web-based agent platform\n\nCommunities:\n- LangChain Discord\n- AI Agent subreddit\n- AutoGPT GitHub discussions\n- Agent research papers on ArXiv\n"
  },
  {
    "path": "examples/resources/logs/log1.txt",
    "content": "Agent Runtime Log - Production Microservice Deployment (ATTEMPT 1 - FAILURE)\n================================================================================\nSession ID: deploy-agent-20241120-1430-attempt1\nTask: \"Deploy recommendation microservice to production with zero downtime\"\nAgent: DeploymentAgent v3.2\nStart Time: 2024-11-20 14:30:00 UTC\nAttempt: 1/3\n\n[14:30:00] [INIT] Agent initialization started\n[14:30:00] [INFO] Task: Deploy recommendation-service v2.5.0 to production\n[14:30:00] [INFO] Target environment: production-cluster-us-east\n[14:30:01] [INFO] Deployment strategy: Blue-green with gradual traffic shift\n[14:30:01] [SUCCESS] Agent initialized\n\n[14:30:01] [PLANNING] Creating deployment plan\n[14:30:02] [THOUGHT] Deployment steps should include:\n  1. Validate service configuration and dependencies\n  2. Build and push Docker image\n  3. Deploy green environment alongside blue\n  4. Run health checks and smoke tests\n  5. Gradually shift traffic from blue to green\n  6. Monitor metrics and rollback if needed\n\n[14:30:02] [ACTION] Step 1: Validating service configuration\n[14:30:02] [TOOL] Executing: validate_config(service=\"recommendation-service\", version=\"v2.5.0\")\n[14:30:05] [OBSERVATION] Configuration validation results:\n  - Service manifest: Valid\n  - Environment variables: 12 variables defined\n  - Resource limits: CPU 2 cores, Memory 4Gi\n  - Dependencies: PostgreSQL, Redis, Kafka\n  - Health check endpoint: /health\n\n[14:30:05] [THOUGHT] Configuration looks good. Proceeding with Docker build.\n\n[14:30:05] [ACTION] Step 2: Building Docker image\n[14:30:05] [TOOL] Executing: build_docker_image(\n  dockerfile=\"services/recommendation/Dockerfile\",\n  tag=\"recommendation-service:v2.5.0\"\n)\n[14:30:55] [OBSERVATION] Docker build completed:\n  - Image: recommendation-service:v2.5.0\n  - Size: 856 MB\n  - Build time: 50 seconds\n  - Base image: python:3.11-slim\n  - Layers: 12\n\n[14:30:55] [ACTION] Pushing image to registry\n[14:30:55] [TOOL] Executing: push_image(image=\"recommendation-service:v2.5.0\")\n[14:31:20] [OBSERVATION] Image pushed successfully to gcr.io/company/recommendation-service:v2.5.0\n\n[14:31:20] [ACTION] Step 3: Deploying green environment\n[14:31:20] [TOOL] Executing: deploy_kubernetes(\n  service=\"recommendation-service\",\n  version=\"v2.5.0\",\n  environment=\"green\",\n  replicas=3\n)\n[14:31:25] [OBSERVATION] Kubernetes deployment initiated:\n  - Deployment: recommendation-service-green\n  - Replicas: 3\n  - Namespace: production\n  - Pods starting...\n\n[14:31:45] [ACTION] Monitoring pod startup\n[14:31:46] [OBSERVATION] Pod status:\n  - recommendation-green-0: Running\n  - recommendation-green-1: Running\n  - recommendation-green-2: Running\n  - All pods ready and passing health checks\n\n[14:31:46] [ACTION] Step 4: Running smoke tests on green environment\n[14:31:46] [TOOL] Executing: run_smoke_tests(target=\"recommendation-green\")\n[14:32:00] [OBSERVATION] Smoke test results:\n  - Health check: PASSED (200 OK)\n  - Database connectivity: PASSED\n  - Redis connectivity: PASSED\n  - Kafka connectivity: PASSED\n  - Sample recommendation request: PASSED (response time: 120ms)\n\n[14:32:00] [THOUGHT] Green environment healthy. Ready to shift traffic.\n\n[14:32:00] [ACTION] Step 5: Starting gradual traffic shift (Blue → Green)\n[14:32:00] [TOOL] Executing: traffic_shift(\n  from_env=\"blue\",\n  to_env=\"green\",\n  strategy=\"gradual\",\n  initial_percentage=10\n)\n[14:32:05] [OBSERVATION] Traffic shift initiated:\n  - Blue environment: 90% traffic\n  - Green environment: 10% traffic\n  - Monitoring for 5 minutes before next increment\n\n[14:32:05] [ACTION] Monitoring metrics during traffic shift\n[14:33:00] [OBSERVATION] Metrics at 10% traffic (1 minute):\n  - Request rate: 150 req/s to green (1500 req/s total)\n  - Average latency: 125ms (baseline: 110ms)\n  - Error rate: 0.1%\n  - All metrics within acceptable range\n\n[14:35:00] [ACTION] Increasing traffic to 25%\n[14:35:01] [TOOL] Executing: traffic_shift(percentage=25)\n[14:35:05] [OBSERVATION] Traffic updated:\n  - Blue: 75%, Green: 25%\n\n[14:36:00] [OBSERVATION] Metrics at 25% traffic (1 minute):\n  - Request rate: 375 req/s to green\n  - Average latency: 130ms\n  - Error rate: 0.2%\n  - Metrics stable\n\n[14:38:00] [ACTION] Increasing traffic to 50%\n[14:38:01] [TOOL] Executing: traffic_shift(percentage=50)\n[14:38:05] [OBSERVATION] Traffic updated:\n  - Blue: 50%, Green: 50%\n\n[14:39:00] [OBSERVATION] Metrics at 50% traffic (1 minute):\n  - Request rate: 750 req/s to green\n  - Average latency: 135ms\n  - Error rate: 0.3%\n\n[14:40:30] [ERROR] Alert triggered: Error rate spike detected!\n[14:40:31] [TOOL] Executing: check_error_logs()\n[14:40:32] [OBSERVATION] ERROR ANALYSIS:\n  - Error rate jumped from 0.3% to 4.2% (14x increase)\n  - Error type: \"DatabaseConnectionError: too many connections\"\n  - Affected pods: All 3 green pods\n  - Pattern: Started at 50% traffic mark\n\n[14:40:32] [THOUGHT] CRITICAL: Connection pool exhausted at 50% traffic!\n\n[14:40:32] [ACTION] Investigating database connection configuration\n[14:40:32] [TOOL] Executing: check_db_config(service=\"recommendation-green\")\n[14:40:35] [OBSERVATION] Database configuration:\n  - Max connections per pod: 20\n  - Total pods: 3\n  - Total connection capacity: 60 connections\n  - Current active connections: 58 (approaching limit)\n  - Database max_connections: 100\n  - Blue environment using: 40 connections\n  - Green environment using: 58 connections\n  - TOTAL: 98/100 connections (98% utilization!)\n\n[14:40:35] [THOUGHT] Root cause identified: Insufficient connection pool size!\n  - At 50% traffic, green needs ~60 connections\n  - At 100% traffic, would need ~120 connections\n  - Database max_connections=100 is insufficient\n  - We're already at 98% capacity with both environments running\n\n[14:40:35] [ACTION] Initiating emergency rollback\n[14:40:35] [TOOL] Executing: rollback_traffic(to_env=\"blue\")\n[14:40:40] [OBSERVATION] Traffic rollback initiated:\n  - Shifting all traffic back to blue environment\n  - Green: 50% → 25% → 10% → 0%\n\n[14:41:00] [OBSERVATION] Rollback progress:\n  - Traffic: 100% on blue, 0% on green\n  - Error rate: Dropping (4.2% → 2.1% → 0.4%)\n  - Service stabilizing\n\n[14:41:30] [OBSERVATION] Rollback complete:\n  - Blue environment: 100% traffic, stable\n  - Green environment: 0% traffic, shutting down\n  - Error rate: Back to 0.2% (normal baseline)\n  - Service restored\n\n[14:41:30] [FAILURE] Deployment failed - Root cause analysis:\n\nPRIMARY ISSUE: Database connection pool misconfiguration\n  - Database max_connections: 100 (too low for blue+green running simultaneously)\n  - Each environment needs ~60 connections at 50% traffic\n  - During blue-green deployment, total needed: 100+ connections\n  - Database cannot support both environments concurrently\n\nCONTRIBUTING FACTORS:\n  1. Did not account for connection pool sizing during dual environment operation\n  2. No load testing performed with both blue and green running simultaneously\n  3. Connection pool monitoring alerts not in place\n  4. Gradual rollout exposed issue only at 50% mark\n\nLESSONS LEARNED:\n  1. ⚠️ CRITICAL: Database connection pools must accommodate BOTH environments during deployment\n  2. ⚠️ Connection pool size = (peak connections per pod) × (total pods in blue + green)\n  3. ⚠️ Must load test with full capacity of both environments before production\n  4. ⚠️ Set up connection pool monitoring and alerts BEFORE deployment\n  5. ⚠️ Have emergency rollback procedure ready (which saved us here)\n\nREMEDIATION PLAN:\n  1. Increase database max_connections from 100 to 250\n  2. Adjust connection pool per pod from 20 to 30\n  3. Add connection pool utilization monitoring\n  4. Set alert threshold at 70% connection utilization\n  5. Document connection pool requirements in deployment checklist\n\nDEPLOYMENT STATUS: FAILED\nROLLBACK STATUS: SUCCESSFUL (no customer impact beyond brief error spike)\nTIME TO DETECT: 8 minutes\nTIME TO ROLLBACK: 1.5 minutes\nCUSTOMER IMPACT: 1.5 minutes of elevated errors (4.2% error rate)\n\n[14:41:30] [EXPORT] Saving failure report: reports/deployment-attempt1-failure.json\n[14:41:31] [NOTIFICATION] Alerting team about deployment failure and lessons learned\n[14:41:31] [SHUTDOWN] Agent session terminated\n\n=================================================================================\nKEY TAKEAWAYS FOR NEXT ATTEMPT:\n=================================================================================\n1. Fix database connection pool sizing BEFORE retry\n2. Calculate total connections needed: (pods_blue + pods_green) × connections_per_pod\n3. Add connection monitoring to detect issues early\n4. Test with both environments running at full capacity\n5. Verify database can handle combined load before production deployment\n"
  },
  {
    "path": "examples/resources/logs/log2.txt",
    "content": "Agent Runtime Log - Production Microservice Deployment (ATTEMPT 2 - FAILURE)\n================================================================================\nSession ID: deploy-agent-20241121-0930-attempt2\nTask: \"Deploy recommendation microservice to production with zero downtime\"\nAgent: DeploymentAgent v3.2\nStart Time: 2024-11-21 09:30:00 UTC\nAttempt: 2/3\nPrevious Attempt: FAILED (database connection pool exhaustion)\n\n[09:30:00] [INIT] Agent initialization started\n[09:30:00] [INFO] Task: Deploy recommendation-service v2.5.0 to production (RETRY)\n[09:30:00] [INFO] Applying lessons from Attempt 1\n[09:30:01] [INFO] Fixed issues:\n  - ✓ Database max_connections increased: 100 → 250\n  - ✓ Connection pool per pod increased: 20 → 30\n  - ✓ Added connection pool monitoring alerts\n  - ✓ Verified capacity calculation: (3 blue + 3 green) × 30 = 180 connections (72% of 250)\n[09:30:01] [SUCCESS] Agent initialized with fixes applied\n\n[09:30:01] [PLANNING] Creating deployment plan (improved from attempt 1)\n[09:30:02] [THOUGHT] Key improvements for this attempt:\n  1. Pre-deployment connection pool validation\n  2. Monitor connection utilization during traffic shift\n  3. More gradual traffic ramp (10% → 25% → 50% → 75% → 100%)\n  4. Longer monitoring windows between increments (5 min instead of 3 min)\n\n[09:30:02] [ACTION] Step 1: Pre-deployment validation\n[09:30:02] [TOOL] Executing: validate_database_capacity()\n[09:30:05] [OBSERVATION] Database capacity check:\n  - Current connections (blue only): 42/250 (17% utilization)\n  - Projected at 100% blue + 100% green: ~180/250 (72% utilization)\n  - Safety margin: 28%\n  - ✓ PASS: Sufficient capacity for blue-green deployment\n\n[09:30:05] [THOUGHT] Connection pool capacity validated. Safe to proceed.\n\n[09:30:05] [ACTION] Step 2: Building and pushing Docker image\n[09:30:05] [TOOL] Executing: build_and_push_image(tag=\"v2.5.0\")\n[09:30:55] [OBSERVATION] Image ready: gcr.io/company/recommendation-service:v2.5.0\n\n[09:30:55] [ACTION] Step 3: Deploying green environment\n[09:30:55] [TOOL] Executing: deploy_kubernetes(environment=\"green\", replicas=3)\n[09:31:15] [OBSERVATION] Green environment deployed:\n  - All 3 pods running and healthy\n  - Health checks passing\n  - Ready for traffic\n\n[09:31:15] [ACTION] Step 4: Starting gradual traffic shift with enhanced monitoring\n[09:31:15] [TOOL] Executing: traffic_shift(percentage=10, monitor_connections=true)\n[09:31:20] [OBSERVATION] Traffic at 10%:\n  - Blue: 90%, Green: 10%\n  - Monitoring connection pools...\n\n[09:32:20] [OBSERVATION] Metrics after 1 minute at 10%:\n  - Request rate: 150 req/s to green\n  - Average latency: 118ms (good)\n  - Error rate: 0.1% (normal)\n  - DB connections: Blue 40, Green 15, Total 55/250 (22%)\n  - ✓ All metrics healthy\n\n[09:36:20] [ACTION] Increasing traffic to 25% (after 5 minute monitoring)\n[09:36:21] [TOOL] Executing: traffic_shift(percentage=25)\n[09:36:25] [OBSERVATION] Traffic at 25%:\n  - Blue: 75%, Green: 25%\n\n[09:37:25] [OBSERVATION] Metrics after 1 minute at 25%:\n  - Request rate: 375 req/s to green\n  - Average latency: 122ms\n  - Error rate: 0.1%\n  - DB connections: Blue 35, Green 28, Total 63/250 (25%)\n  - ✓ All metrics healthy\n\n[09:41:25] [ACTION] Increasing traffic to 50%\n[09:41:26] [TOOL] Executing: traffic_shift(percentage=50)\n[09:41:30] [OBSERVATION] Traffic at 50%:\n  - Blue: 50%, Green: 50%\n\n[09:42:30] [OBSERVATION] Metrics after 1 minute at 50%:\n  - Request rate: 750 req/s to green\n  - Average latency: 125ms\n  - Error rate: 0.2%\n  - DB connections: Blue 58, Green 62, Total 120/250 (48%)\n  - ✓ No connection issues! (Fixed from Attempt 1)\n\n[09:43:30] [OBSERVATION] Metrics after 2 minutes at 50%:\n  - Latency: 128ms\n  - Error rate: 0.2%\n  - DB connections: 125/250 (50%)\n  - ✓ Stable performance\n\n[09:46:30] [ACTION] Increasing traffic to 75%\n[09:46:31] [TOOL] Executing: traffic_shift(percentage=75)\n[09:46:35] [OBSERVATION] Traffic at 75%:\n  - Blue: 25%, Green: 75%\n\n[09:47:35] [OBSERVATION] Metrics after 1 minute at 75%:\n  - Request rate: 1125 req/s to green\n  - Average latency: 132ms\n  - Error rate: 0.3%\n\n[09:48:35] [OBSERVATION] Metrics after 2 minutes at 75%:\n  - Request rate: 1125 req/s\n  - Average latency: 145ms (increasing trend)\n  - Error rate: 0.4% (slight increase)\n\n[09:49:35] [OBSERVATION] Metrics after 3 minutes at 75%:\n  - Request rate: 1125 req/s\n  - Average latency: 178ms (significant increase!)\n  - Error rate: 0.6%\n  - DB connections: 155/250 (62%)\n\n[09:50:00] [WARNING] Latency threshold exceeded: 178ms (threshold: 150ms)\n[09:50:01] [ACTION] Investigating latency spike\n[09:50:01] [TOOL] Executing: analyze_performance_bottleneck()\n[09:50:05] [OBSERVATION] Performance analysis:\n  - CPU utilization: 45% (normal)\n  - Memory utilization: 58% (normal)\n  - Network I/O: Normal\n  - Database query time: 85ms (elevated from baseline 40ms)\n  - Database CPU: 78% (high!)\n  - Database connections: 155 (healthy)\n\n[09:50:05] [THOUGHT] Database performance degrading under load, but not connection related.\n\n[09:51:00] [OBSERVATION] Continued monitoring:\n  - Average latency: 215ms (getting worse)\n  - P95 latency: 450ms\n  - P99 latency: 780ms (SLO violation: >500ms)\n  - Error rate: 1.2% (increasing)\n\n[09:51:00] [ERROR] SLO violation: P99 latency exceeds 500ms threshold\n\n[09:51:00] [ACTION] Deep dive into database performance\n[09:51:00] [TOOL] Executing: analyze_database_queries()\n[09:51:10] [OBSERVATION] Query performance analysis:\n  - Slow query detected: recommendation_candidates lookup\n  - Query time: 200-400ms (baseline: 30-50ms)\n  - Cause: Missing database index on new query pattern\n  - New v2.5.0 feature uses \"user_segment\" field in WHERE clause\n  - Index exists on \"user_id\" but NOT on \"user_segment\"\n  - Full table scan on 50M row table causing slowdown\n\n[09:51:10] [THOUGHT] ROOT CAUSE: Missing database index for new query pattern!\n  - V2.5.0 introduced new recommendation algorithm\n  - Uses user_segment field not previously indexed\n  - Works fine at low traffic but degrades at high volume\n  - This was not caught in staging (smaller data volume)\n\n[09:51:10] [ERROR] CRITICAL: Cannot fix index during deployment (requires DB downtime or long rebuild)\n\n[09:51:10] [ACTION] Initiating rollback to prevent further SLO violations\n[09:51:10] [TOOL] Executing: rollback_traffic(to_env=\"blue\")\n[09:51:15] [OBSERVATION] Rolling back traffic:\n  - Green: 75% → 50% → 25% → 10% → 0%\n\n[09:52:00] [OBSERVATION] Rollback progress:\n  - Traffic: 100% on blue\n  - Latency dropping: 215ms → 145ms → 110ms\n  - Error rate: 1.2% → 0.5% → 0.2%\n  - Service stabilizing\n\n[09:52:30] [OBSERVATION] Rollback complete:\n  - Blue environment: 100% traffic, stable\n  - Green environment: 0% traffic, shutting down\n  - Average latency: 108ms (back to normal)\n  - Error rate: 0.2% (baseline)\n  - Service fully restored\n\n[09:52:30] [FAILURE] Deployment failed - Root cause analysis:\n\nPRIMARY ISSUE: Missing database index for new query pattern\n  - V2.5.0 code uses new WHERE clause on \"user_segment\" field\n  - No index exists for this field\n  - Full table scan on 50M rows causes 200-400ms queries\n  - Acceptable at low traffic, degrades at high volume\n\nCONTRIBUTING FACTORS:\n  1. Code review did not flag new query pattern requiring index\n  2. Staging environment has smaller dataset (5M rows vs 50M in prod)\n  3. Performance testing in staging didn't reveal the issue\n  4. Database indexing not part of deployment checklist\n  5. No query performance monitoring during deployment\n\nWHY ATTEMPT 1 FIX WASN'T ENOUGH:\n  - Fixed connection pool issue ✓\n  - But didn't discover the index issue ✗\n  - Different root causes require different solutions\n\nLESSONS LEARNED (New from Attempt 2):\n  1. ⚠️ CRITICAL: Analyze ALL new query patterns for index requirements\n  2. ⚠️ Run EXPLAIN ANALYZE on new queries before deployment\n  3. ⚠️ Performance test with production-scale data volumes\n  4. ⚠️ Monitor query execution time during deployment\n  5. ⚠️ Create required indexes BEFORE deploying code that needs them\n  6. ⚠️ Staging must have production-like data volume for realistic testing\n\nREMEDIATION PLAN:\n  1. Create index: CREATE INDEX idx_user_segment ON users(user_segment)\n  2. Wait for index build to complete (~2 hours on 50M rows)\n  3. Add index creation to deployment prerequisites checklist\n  4. Update staging database to match production size\n  5. Add query performance monitoring to deployment pipeline\n\nDEPLOYMENT STATUS: FAILED\nROLLBACK STATUS: SUCCESSFUL\nTIME TO DETECT: 5 minutes at 75% traffic\nTIME TO ROLLBACK: 1.5 minutes\nCUSTOMER IMPACT: 5 minutes of degraded performance (P99 latency 500-780ms, 1.2% errors)\n\n[09:52:30] [EXPORT] Saving failure report: reports/deployment-attempt2-failure.json\n[09:52:31] [NOTIFICATION] Alerting team: Index required before retry\n[09:52:31] [SHUTDOWN] Agent session terminated\n\n=================================================================================\nKEY TAKEAWAYS FOR NEXT ATTEMPT:\n=================================================================================\n1. Database index on user_segment MUST be created before retry\n2. Verify index exists: SELECT * FROM pg_indexes WHERE indexname = 'idx_user_segment'\n3. Add query performance monitoring to catch slow queries early\n4. Test with production-scale data before deploying\n5. Include \"Database Schema Changes\" section in deployment checklist\n\nCUMULATIVE LESSONS (From Both Attempts):\n- Connection pool sizing for dual environments (Attempt 1)\n- Database index requirements for new queries (Attempt 2)\n- Both are critical and both must be fixed for success\n"
  },
  {
    "path": "examples/resources/logs/log3.txt",
    "content": "Agent Runtime Log - Production Microservice Deployment (ATTEMPT 3 - SUCCESS)\n================================================================================\nSession ID: deploy-agent-20241122-1000-attempt3\nTask: \"Deploy recommendation microservice to production with zero downtime\"\nAgent: DeploymentAgent v3.2\nStart Time: 2024-11-22 10:00:00 UTC\nAttempt: 3/3\nPrevious Attempts:\n  - Attempt 1: FAILED (database connection pool exhaustion)\n  - Attempt 2: FAILED (missing database index)\n\n[10:00:00] [INIT] Agent initialization started\n[10:00:00] [INFO] Task: Deploy recommendation-service v2.5.0 to production (FINAL ATTEMPT)\n[10:00:00] [INFO] Applying ALL lessons from previous attempts\n[10:00:01] [INFO] Pre-deployment checklist completed:\n  - ✓ Database max_connections: 250 (increased from 100)\n  - ✓ Connection pool per pod: 30 (increased from 20)\n  - ✓ Connection pool monitoring: ACTIVE\n  - ✓ Database index idx_user_segment: CREATED and READY\n  - ✓ Index verified: SELECT * FROM pg_indexes → Found\n  - ✓ Query performance tested with EXPLAIN ANALYZE → 35ms (excellent)\n  - ✓ Staging tests passed with production-scale data\n  - ✓ All monitoring alerts configured\n[10:00:01] [SUCCESS] Agent initialized - Ready for deployment with all fixes applied\n\n[10:00:01] [PLANNING] Creating comprehensive deployment plan\n[10:00:02] [THOUGHT] This attempt incorporates:\n  FROM ATTEMPT 1: Connection pool sizing for dual environment operation\n  FROM ATTEMPT 2: Database index requirements and query performance monitoring\n  NEW FOR ATTEMPT 3: Enhanced pre-deployment validation and longer monitoring windows\n\n[10:00:02] [ACTION] Step 1: Comprehensive pre-deployment validation\n[10:00:02] [TOOL] Executing: run_predeploy_checks()\n[10:00:10] [OBSERVATION] Pre-deployment validation results:\n\n  ✓ Database Capacity Check:\n    - Current connections: 45/250 (18%)\n    - Projected peak: 180/250 (72%)\n    - Safety margin: 28% ✓\n\n  ✓ Database Index Check:\n    - idx_user_segment exists: YES\n    - Index size: 1.2 GB\n    - Index scan cost: Low (35ms query time)\n\n  ✓ Query Performance Check:\n    - EXPLAIN ANALYZE user_segment query: 35ms ✓\n    - All queries under 100ms threshold ✓\n\n  ✓ Resource Availability:\n    - Kubernetes nodes: 8 available\n    - CPU capacity: 45% utilization\n    - Memory capacity: 52% utilization\n\n  ✓ Dependency Health:\n    - PostgreSQL: Healthy\n    - Redis: Healthy\n    - Kafka: Healthy\n\n  ✓ Monitoring Systems:\n    - Prometheus: Active\n    - Grafana: Active\n    - Alert manager: Active\n    - All deployment alerts configured\n\n[10:00:10] [THOUGHT] All validation checks passed. Proceeding with high confidence.\n\n[10:00:10] [ACTION] Step 2: Building and pushing Docker image\n[10:00:10] [TOOL] Executing: build_and_push_image(tag=\"v2.5.0\")\n[10:01:00] [OBSERVATION] Image ready: gcr.io/company/recommendation-service:v2.5.0\n\n[10:01:00] [ACTION] Step 3: Deploying green environment\n[10:01:00] [TOOL] Executing: deploy_kubernetes(\n  environment=\"green\",\n  replicas=3,\n  resource_limits={\"cpu\": \"2\", \"memory\": \"4Gi\"}\n)\n[10:01:05] [OBSERVATION] Kubernetes deployment started...\n\n[10:01:25] [OBSERVATION] Green environment status:\n  - recommendation-green-0: Running, Ready\n  - recommendation-green-1: Running, Ready\n  - recommendation-green-2: Running, Ready\n  - Health checks: All passing\n  - Database connections: 15 (healthy)\n\n[10:01:25] [ACTION] Step 4: Running comprehensive smoke tests\n[10:01:25] [TOOL] Executing: run_smoke_tests(environment=\"green\", comprehensive=true)\n[10:01:45] [OBSERVATION] Smoke test results:\n  ✓ Health endpoint: 200 OK\n  ✓ Database connectivity: Connected, 15 connections\n  ✓ Redis connectivity: Connected\n  ✓ Kafka connectivity: Connected\n  ✓ Sample recommendation request: Success (117ms)\n  ✓ User segment query performance: 33ms (excellent!)\n  ✓ Cache hit rate: 78%\n  ✓ All functional tests: PASSED (20/20)\n\n[10:01:45] [THOUGHT] Green environment fully validated. Beginning gradual traffic shift.\n\n[10:01:45] [ACTION] Step 5: Gradual traffic shift - Stage 1 (10%)\n[10:01:45] [TOOL] Executing: traffic_shift(percentage=10, monitoring_window=\"5min\")\n[10:01:50] [OBSERVATION] Traffic distribution:\n  - Blue: 90%, Green: 10%\n  - Monitoring for 5 minutes...\n\n[10:02:50] [OBSERVATION] Metrics at 10% (1 minute):\n  - Request rate: 150 req/s to green\n  - Average latency: 115ms\n  - P95 latency: 145ms\n  - P99 latency: 185ms\n  - Error rate: 0.1%\n  - DB connections: Blue 40, Green 15, Total 55/250 (22%)\n  - Query performance: user_segment queries 30-40ms ✓\n  - ✓ All metrics within target ranges\n\n[10:04:50] [OBSERVATION] Metrics at 10% (3 minutes):\n  - Average latency: 118ms (stable)\n  - Error rate: 0.1% (normal)\n  - DB connections: 57/250 (23%)\n  - ✓ Performance stable\n\n[10:06:50] [OBSERVATION] Metrics at 10% (5 minutes):\n  - Average latency: 116ms (stable)\n  - Error rate: 0.1% (normal)\n  - DB connections: 56/250 (22%)\n  - ✓ Stage 1 successful - Ready for next increment\n\n[10:06:50] [ACTION] Step 6: Gradual traffic shift - Stage 2 (25%)\n[10:06:51] [TOOL] Executing: traffic_shift(percentage=25, monitoring_window=\"5min\")\n[10:06:55] [OBSERVATION] Traffic distribution:\n  - Blue: 75%, Green: 25%\n\n[10:07:55] [OBSERVATION] Metrics at 25% (1 minute):\n  - Request rate: 375 req/s to green\n  - Average latency: 120ms\n  - P99 latency: 192ms\n  - Error rate: 0.1%\n  - DB connections: Blue 35, Green 28, Total 63/250 (25%)\n  - ✓ Healthy\n\n[10:11:55] [OBSERVATION] Metrics at 25% (5 minutes):\n  - Average latency: 122ms (stable)\n  - Error rate: 0.1% (normal)\n  - DB connections: 65/250 (26%)\n  - ✓ Stage 2 successful\n\n[10:11:55] [ACTION] Step 7: Gradual traffic shift - Stage 3 (50%)\n[10:11:56] [TOOL] Executing: traffic_shift(percentage=50, monitoring_window=\"8min\")\n[10:12:00] [OBSERVATION] Traffic distribution:\n  - Blue: 50%, Green: 50%\n\n[10:13:00] [OBSERVATION] Metrics at 50% (1 minute):\n  - Request rate: 750 req/s to green\n  - Average latency: 125ms\n  - P99 latency: 198ms\n  - Error rate: 0.2%\n  - DB connections: Blue 58, Green 62, Total 120/250 (48%)\n  - ✓ No connection pool issues (fixed from Attempt 1)\n\n[10:15:00] [OBSERVATION] Metrics at 50% (3 minutes):\n  - Average latency: 127ms (stable)\n  - P99 latency: 195ms\n  - Error rate: 0.2%\n  - DB connections: 122/250 (49%)\n  - Query performance: 32-38ms (excellent - fixed from Attempt 2)\n  - ✓ Both previous issues resolved!\n\n[10:20:00] [OBSERVATION] Metrics at 50% (8 minutes):\n  - Average latency: 126ms (stable)\n  - P99 latency: 196ms (under 500ms SLO ✓)\n  - Error rate: 0.2%\n  - DB connections: 125/250 (50%)\n  - ✓ Stage 3 successful - Performance excellent\n\n[10:20:00] [ACTION] Step 8: Gradual traffic shift - Stage 4 (75%)\n[10:20:01] [TOOL] Executing: traffic_shift(percentage=75, monitoring_window=\"8min\")\n[10:20:05] [OBSERVATION] Traffic distribution:\n  - Blue: 25%, Green: 75%\n\n[10:21:05] [OBSERVATION] Metrics at 75% (1 minute):\n  - Request rate: 1125 req/s to green\n  - Average latency: 132ms\n  - P99 latency: 205ms\n  - Error rate: 0.2%\n  - DB connections: Blue 20, Green 88, Total 108/250 (43%)\n  - ✓ Healthy\n\n[10:24:05] [OBSERVATION] Metrics at 75% (4 minutes):\n  - Average latency: 134ms (stable, no degradation!)\n  - P99 latency: 208ms (well under SLO)\n  - Error rate: 0.2%\n  - DB connections: 110/250 (44%)\n  - Query performance: 35-42ms (consistent, index working perfectly)\n  - ✓ No latency spike (Attempt 2 issue resolved!)\n\n[10:28:05] [OBSERVATION] Metrics at 75% (8 minutes):\n  - Average latency: 135ms (stable)\n  - P99 latency: 210ms\n  - Error rate: 0.2%\n  - DB connections: 112/250 (45%)\n  - ✓ Stage 4 successful - Ready for full cutover\n\n[10:28:05] [THOUGHT] All stages successful. Proceeding to 100% with confidence.\n\n[10:28:05] [ACTION] Step 9: Final traffic shift - 100% to Green\n[10:28:06] [TOOL] Executing: traffic_shift(percentage=100, monitoring_window=\"10min\")\n[10:28:10] [OBSERVATION] Traffic distribution:\n  - Blue: 0%, Green: 100%\n  - All production traffic now on v2.5.0\n\n[10:29:10] [OBSERVATION] Metrics at 100% (1 minute):\n  - Request rate: 1500 req/s (full production load)\n  - Average latency: 138ms\n  - P95 latency: 172ms\n  - P99 latency: 215ms (well under 500ms SLO ✓)\n  - Error rate: 0.2% (baseline)\n  - DB connections: 115/250 (46%)\n  - ✓ Full cutover successful!\n\n[10:33:10] [OBSERVATION] Metrics at 100% (5 minutes):\n  - Average latency: 137ms (stable)\n  - P99 latency: 218ms\n  - Error rate: 0.2%\n  - DB connections: 118/250 (47%)\n  - Cache hit rate: 79%\n  - Throughput: 1500 req/s\n  - ✓ Service performing excellently\n\n[10:38:10] [OBSERVATION] Metrics at 100% (10 minutes):\n  - Average latency: 136ms (stable and excellent)\n  - P99 latency: 216ms (SLO: <500ms ✓)\n  - Error rate: 0.2% (normal baseline)\n  - DB connections: 120/250 (48% - plenty of headroom)\n  - Query performance: 33-40ms (index performing perfectly)\n  - ✓ Service stable at full production load\n\n[10:38:10] [SUCCESS] Deployment completed successfully!\n\n[10:38:10] [ACTION] Step 10: Cleanup old blue environment\n[10:38:10] [TOOL] Executing: cleanup_old_environment(environment=\"blue\")\n[10:38:15] [OBSERVATION] Blue environment shutdown:\n  - Scaled down to 0 replicas\n  - Resources released\n  - DNS cutover complete\n  - Only green (v2.5.0) running\n\n[10:38:15] [ACTION] Final validation and monitoring setup\n[10:38:15] [TOOL] Executing: setup_continuous_monitoring()\n[10:38:20] [OBSERVATION] Continuous monitoring configured:\n  - Latency alerts: P99 > 500ms\n  - Error rate alerts: > 1%\n  - Connection pool alerts: > 70%\n  - Query performance alerts: > 100ms\n  - Resource utilization alerts: > 80%\n  - All dashboards updated\n\n[10:38:20] [SUCCESS] Deployment fully complete and stable!\n\n=================================================================================\nDEPLOYMENT SUCCESS SUMMARY\n=================================================================================\n\nFINAL METRICS:\n- Average latency: 136ms (target: <200ms) ✓\n- P99 latency: 216ms (SLO: <500ms) ✓\n- Error rate: 0.2% (baseline, target: <1%) ✓\n- Throughput: 1500 req/s (full production) ✓\n- DB connections: 120/250 (48% utilization) ✓\n- Zero downtime achieved ✓\n\nDEPLOYMENT TIMELINE:\n- Total deployment time: 38 minutes\n- Traffic shift duration: 36 minutes (gradual)\n- Monitoring windows: Conservative (5-10 min per stage)\n- Zero customer impact ✓\n\nISSUES RESOLVED:\n✓ Attempt 1 issue: Database connection pool sizing\n  - Fixed: Increased max_connections 100→250, pool per pod 20→30\n  - Result: 48% utilization at full load (plenty of headroom)\n\n✓ Attempt 2 issue: Missing database index\n  - Fixed: Created idx_user_segment before deployment\n  - Result: Query time 33-40ms (was 200-400ms without index)\n\n=================================================================================\nCOMPREHENSIVE LESSONS LEARNED (Across All 3 Attempts)\n=================================================================================\n\n1. DATABASE CAPACITY PLANNING (From Attempt 1):\n   ⚠️ Calculate connections for BOTH environments during blue-green deployment\n   ⚠️ Formula: (pods_blue + pods_green) × connections_per_pod + safety_margin\n   ⚠️ Monitor connection utilization in real-time during deployment\n   ⚠️ Set alerts at 70% threshold, rollback at 85%\n\n2. DATABASE SCHEMA CHANGES (From Attempt 2):\n   ⚠️ Analyze ALL new query patterns for index requirements\n   ⚠️ Run EXPLAIN ANALYZE on new queries before production\n   ⚠️ Create required indexes BEFORE deploying code\n   ⚠️ Test with production-scale data volumes (not just staging)\n   ⚠️ Monitor query execution times during deployment\n\n3. DEPLOYMENT BEST PRACTICES (From Attempt 3):\n   ⚠️ Comprehensive pre-deployment checklist is MANDATORY\n   ⚠️ Gradual traffic shift with adequate monitoring windows\n   ⚠️ Conservative approach: 10% → 25% → 50% → 75% → 100%\n   ⚠️ Longer monitoring at critical stages (50%, 75%, 100%)\n   ⚠️ Have automated rollback ready at every stage\n\n4. MONITORING AND OBSERVABILITY:\n   ⚠️ Connection pool utilization monitoring\n   ⚠️ Query performance monitoring (slow query detection)\n   ⚠️ Latency tracking (average, P95, P99)\n   ⚠️ Error rate monitoring with automatic alerts\n   ⚠️ Resource utilization (CPU, memory, network)\n\n5. TESTING REQUIREMENTS:\n   ⚠️ Staging must have production-like data volumes\n   ⚠️ Load testing with both blue and green at capacity\n   ⚠️ Query performance testing with EXPLAIN ANALYZE\n   ⚠️ End-to-end smoke tests on green before traffic shift\n   ⚠️ Rollback procedure testing in staging\n\n=================================================================================\nDEPLOYMENT CHECKLIST (For Future Use)\n=================================================================================\n\nPRE-DEPLOYMENT:\n□ Database capacity validated (connection pools)\n□ All required indexes created and verified\n□ Query performance tested with EXPLAIN ANALYZE\n□ Staging tests passed with production-scale data\n□ Monitoring alerts configured\n□ Rollback procedure tested\n□ Team notified of deployment window\n\nDURING DEPLOYMENT:\n□ Green environment health checks passing\n□ Smoke tests passed\n□ Gradual traffic shift with monitoring\n□ Connection pool utilization < 70%\n□ Query performance < 100ms\n□ Latency within SLOs\n□ Error rate < 1%\n\nPOST-DEPLOYMENT:\n□ Blue environment cleaned up\n□ Continuous monitoring active\n□ Documentation updated\n□ Lessons learned documented\n□ Team notified of success\n\n[10:38:20] [EXPORT] Saving success report: reports/deployment-attempt3-success.json\n[10:38:21] [EXPORT] Updating deployment playbook with lessons learned\n[10:38:22] [NOTIFICATION] Alerting team: Deployment successful! 🎉\n[10:38:22] [SHUTDOWN] Agent session terminated\n\n=================================================================================\nKEY SUCCESS FACTORS:\n=================================================================================\n1. ✓ Applied lessons from BOTH previous failures\n2. ✓ Comprehensive pre-deployment validation\n3. ✓ Conservative, gradual traffic shift approach\n4. ✓ Adequate monitoring windows at each stage\n5. ✓ Fixed root causes, not just symptoms\n6. ✓ Documented everything for future reference\n\nATTEMPTS SUMMARY:\n- Attempt 1: Failed (connection pools) → Learned capacity planning\n- Attempt 2: Failed (missing index) → Learned schema validation\n- Attempt 3: Success! → Applied all lessons learned\n\nThis demonstrates the power of learning from failures! 🚀\n"
  },
  {
    "path": "examples/sealos-assistant/README.md",
    "content": "# MemU Assistant - Sealos DevBox Example\n\nA personal AI assistant with long-term memory, designed for deployment on [Sealos DevBox](https://sealos.io/products/devbox).\n\n## Features\n\n- **Persistent Memory**: Remembers user preferences and past conversations\n- **REST API**: Simple endpoints for chat, memorize, and recall\n- **OpenAI Compatible**: Works with OpenAI, Nebius, Groq, and other providers\n- **1-Click Deploy**: Ready for Sealos DevBox deployment\n\n## Quick Start\n\n### Local Development\n\n```bash\n# Install dependencies\npip install -r requirements.txt\n\n# Configure environment\ncp .env.example .env\n# Edit .env with your API key\n\n# Run the server\npython main.py\n```\n\n### Deploy on Sealos DevBox\n\n1. Create a Python DevBox on [Sealos](https://cloud.sealos.io)\n2. Clone this project\n3. Set environment variables\n4. Click **Deploy**\n\nSee the full guide: [docs/sealos-devbox-guide.md](../../docs/sealos-devbox-guide.md)\n\n## API Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/` | GET | Service info |\n| `/health` | GET | Health check |\n| `/chat` | POST | Chat with memory |\n| `/memorize` | POST | Store information |\n| `/recall` | GET | Query memories |\n\n## Example Usage\n\n```bash\n# Store a memory\ncurl -X POST http://localhost:8000/memorize \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"content\": \"I prefer Python and dark mode\"}'\n\n# Chat with memory\ncurl -X POST http://localhost:8000/chat \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"message\": \"What are my preferences?\"}'\n\n# Recall memories\ncurl \"http://localhost:8000/recall?query=preferences\"\n```\n\n## License\n\nMIT - Part of the MemU project\n"
  },
  {
    "path": "examples/sealos-assistant/entrypoint.sh",
    "content": "#!/bin/bash\n# Sealos DevBox entrypoint script\n# This script is executed when deploying to production\n\nset -e\n\n# Activate virtual environment if it exists\nif [ -d \"venv\" ]; then\n    source venv/bin/activate\nfi\n\n# Start the application\nexec uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}\n"
  },
  {
    "path": "examples/sealos-assistant/main.py",
    "content": "\"\"\"\nPersonal AI Assistant with Long-Term Memory\nPowered by MemU + FastAPI on Sealos DevBox\n\nThis example demonstrates how to build a memory-enabled AI assistant\nthat can be deployed on Sealos DevBox with 1-click deployment.\n\nUsage:\n    # Local development\n    pip install -r requirements.txt\n    python main.py\n\n    # Or with uvicorn\n    uvicorn main:app --reload --host 0.0.0.0 --port 8000\n\"\"\"\n\nimport os\nimport sys\nfrom contextlib import asynccontextmanager\n\nfrom dotenv import load_dotenv\nfrom fastapi import FastAPI, HTTPException\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom pydantic import BaseModel\n\nload_dotenv()\n\n# Add src to path for local development\nsrc_path = os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\", \"..\", \"src\"))\nif os.path.exists(src_path):\n    sys.path.insert(0, src_path)\n\nfrom memu.app import MemoryService\n\n# Global memory service instance\nmemory_service: MemoryService | None = None\n\n\ndef get_llm_profiles() -> dict:\n    \"\"\"Build LLM profiles from environment variables.\"\"\"\n    base_url = os.getenv(\"OPENAI_BASE_URL\", \"https://api.openai.com/v1\")\n    api_key = os.getenv(\"OPENAI_API_KEY\")\n\n    if not api_key:\n        raise ValueError(\"OPENAI_API_KEY environment variable is required\")\n\n    return {\n        \"default\": {\n            \"provider\": \"openai\",\n            \"base_url\": base_url,\n            \"api_key\": api_key,\n            \"chat_model\": os.getenv(\"CHAT_MODEL\", \"gpt-4o-mini\"),\n            \"client_backend\": \"sdk\",\n        },\n        \"embedding\": {\n            \"provider\": \"openai\",\n            \"base_url\": base_url,\n            \"api_key\": api_key,\n            \"embed_model\": os.getenv(\"EMBED_MODEL\", \"text-embedding-3-small\"),\n            \"client_backend\": \"sdk\",\n        },\n    }\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    \"\"\"Initialize MemU memory service on startup.\"\"\"\n    global memory_service\n\n    try:\n        llm_profiles = get_llm_profiles()\n        memory_service = MemoryService(llm_profiles=llm_profiles)\n        print(\"✓ MemU Memory Service initialized successfully\")\n    except Exception as e:\n        print(f\"✗ Failed to initialize MemU: {e}\")\n        raise\n\n    yield\n\n    print(\"Shutting down MemU Assistant...\")\n\n\napp = FastAPI(\n    title=\"MemU Assistant\",\n    description=\"AI Assistant with Long-Term Memory powered by MemU\",\n    version=\"1.0.0\",\n    lifespan=lifespan,\n)\n\n# Enable CORS for web clients\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"*\"],\n    allow_credentials=True,\n    allow_methods=[\"*\"],\n    allow_headers=[\"*\"],\n)\n\n\n# Request/Response Models\nclass ChatRequest(BaseModel):\n    message: str\n    user_id: str = \"default\"\n\n\nclass ChatResponse(BaseModel):\n    response: str\n    memories_used: int\n    memories_stored: int\n\n\nclass MemorizeRequest(BaseModel):\n    content: str\n    user_id: str = \"default\"\n\n\nclass MemorizeResponse(BaseModel):\n    status: str\n    items_created: int\n    categories: int\n\n\nclass RecallResponse(BaseModel):\n    query: str\n    memories_found: int\n    memories: list[dict]\n\n\n# API Endpoints\n@app.get(\"/\")\nasync def root():\n    \"\"\"Service information and available endpoints.\"\"\"\n    return {\n        \"service\": \"MemU Assistant\",\n        \"description\": \"AI Assistant with Long-Term Memory\",\n        \"version\": \"1.0.0\",\n        \"status\": \"running\",\n        \"endpoints\": {\n            \"GET /\": \"This info\",\n            \"GET /health\": \"Health check\",\n            \"POST /chat\": \"Chat with memory-aware AI\",\n            \"POST /memorize\": \"Store information in memory\",\n            \"GET /recall\": \"Query stored memories\",\n        },\n    }\n\n\n@app.get(\"/health\")\nasync def health():\n    \"\"\"Health check endpoint.\"\"\"\n    return {\n        \"status\": \"healthy\",\n        \"memory_service_initialized\": memory_service is not None,\n    }\n\n\n@app.post(\"/chat\", response_model=ChatResponse)\nasync def chat(request: ChatRequest):\n    \"\"\"\n    Chat with the AI assistant.\n\n    The assistant will:\n    1. Retrieve relevant memories from past conversations\n    2. Use those memories as context for the response\n    3. Store new information from the conversation\n    \"\"\"\n    if not memory_service:\n        raise HTTPException(status_code=503, detail=\"Memory service not initialized\")\n\n    try:\n        # Retrieve relevant memories\n        retrieve_result = await memory_service.retrieve(queries=[{\"role\": \"user\", \"content\": request.message}])\n\n        memories = retrieve_result.get(\"items\", [])\n\n        # Build context from memories\n        memory_context = []\n        for mem in memories[:5]:\n            if isinstance(mem, dict):\n                summary = mem.get(\"summary\", str(mem))\n                memory_context.append(summary)\n\n        # Generate response (in production, use full LLM with memory context)\n        if memory_context:\n            response_text = (\n                f\"Based on what I remember about you, here's my response to: '{request.message}'\\n\\n\"\n                f\"Relevant context from our past conversations:\\n\"\n                + \"\\n\".join(f\"- {ctx[:100]}...\" if len(ctx) > 100 else f\"- {ctx}\" for ctx in memory_context)\n            )\n        else:\n            response_text = f\"I received your message: '{request.message}'. I don't have any relevant memories yet.\"\n\n        # Store the conversation\n        import tempfile\n\n        with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".txt\", delete=False, encoding=\"utf-8\") as f:\n            f.write(f\"User ({request.user_id}) said: {request.message}\")\n            temp_file = f.name\n\n        try:\n            memorize_result = await memory_service.memorize(\n                resource_url=temp_file,\n                modality=\"text\",\n            )\n            memories_stored = len(memorize_result.get(\"items\", []))\n        finally:\n            os.unlink(temp_file)\n\n        return ChatResponse(\n            response=response_text,\n            memories_used=len(memories),\n            memories_stored=memories_stored,\n        )\n\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=f\"Chat failed: {e!s}\")\n\n\n@app.post(\"/memorize\", response_model=MemorizeResponse)\nasync def memorize(request: MemorizeRequest):\n    \"\"\"Store information in long-term memory.\"\"\"\n    if not memory_service:\n        raise HTTPException(status_code=503, detail=\"Memory service not initialized\")\n\n    try:\n        import tempfile\n\n        with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".txt\", delete=False, encoding=\"utf-8\") as f:\n            f.write(f\"[User: {request.user_id}] {request.content}\")\n            temp_file = f.name\n\n        try:\n            result = await memory_service.memorize(\n                resource_url=temp_file,\n                modality=\"text\",\n            )\n            return MemorizeResponse(\n                status=\"stored\",\n                items_created=len(result.get(\"items\", [])),\n                categories=len(result.get(\"categories\", [])),\n            )\n        finally:\n            os.unlink(temp_file)\n\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=f\"Memorize failed: {e!s}\")\n\n\n@app.get(\"/recall\", response_model=RecallResponse)\nasync def recall(query: str, limit: int = 5):\n    \"\"\"Recall memories related to a query.\"\"\"\n    if not memory_service:\n        raise HTTPException(status_code=503, detail=\"Memory service not initialized\")\n\n    try:\n        result = await memory_service.retrieve(queries=[{\"role\": \"user\", \"content\": query}])\n\n        items = result.get(\"items\", [])[:limit]\n        memories = []\n        for item in items:\n            if isinstance(item, dict):\n                memories.append({\n                    \"summary\": item.get(\"summary\", str(item)),\n                    \"category\": item.get(\"category\", \"unknown\"),\n                })\n            else:\n                memories.append({\"summary\": str(item), \"category\": \"unknown\"})\n\n        return RecallResponse(\n            query=query,\n            memories_found=len(memories),\n            memories=memories,\n        )\n\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=f\"Recall failed: {e!s}\")\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n\n    host = os.getenv(\"HOST\", \"0.0.0.0\")\n    port = int(os.getenv(\"PORT\", 8000))\n\n    print(f\"Starting MemU Assistant on {host}:{port}\")\n    uvicorn.run(\"main:app\", host=host, port=port, reload=True)\n"
  },
  {
    "path": "examples/sealos-assistant/requirements.txt",
    "content": "memu>=0.1.0\nfastapi>=0.100.0\nuvicorn[standard]>=0.23.0\npython-dotenv>=1.0.0\n"
  },
  {
    "path": "examples/sealos_support_agent.py",
    "content": "import sys\nimport time\n\n# Intentamos importar la librería instalada por uv\ntry:\n    from memu import Memory  # noqa: F401\n\n    MEMU_INSTALLED = True\nexcept ImportError as e:\n    # Si falla, guardamos el error para debug\n    MEMU_INSTALLED = False\n    IMPORT_ERROR = str(e)\n\n\ndef print_slow(text, delay=0.02):\n    \"\"\"Typing effect for realism\"\"\"\n    for char in text:\n        sys.stdout.write(char)\n        sys.stdout.flush()\n        time.sleep(delay)\n    print()\n\n\ndef run_rigorous_demo():\n    print(\"\\n🚀 Starting Sealos Support Agent Demo (Offline Mode)\")\n    print(\"===================================================\\n\")\n\n    # 1. ENVIRONMENT CHECK\n    if MEMU_INSTALLED:\n        print(\"✅ Environment Check: MemU Library detected (Installed via uv).\")\n        print(\"✅ Runtime: Sealos Devbox (Python 3.13+)\")\n    else:\n        # En caso de error, mostramos advertencia pero permitimos la captura\n        print(\"⚠️ Warning: MemU library not detected. Running in Simulation Mode.\")\n        if \"IMPORT_ERROR\" in globals():\n            print(f\"   Debug Error: {IMPORT_ERROR}\")\n\n    time.sleep(0.5)\n\n    # 2. MEMORY INGESTION (PHASE 1)\n    print(\"\\n📝 --- Phase 1: Ingesting Conversation History ---\")\n    print('👤 Captain: \"I\\'m getting a 502 Bad Gateway error on port 3000.\"')\n    print_slow(\"🤖 Agent: (Processing input through Memory Pipeline...)\", delay=0.01)\n\n    time.sleep(1.0)\n    print(\"✅ Memory stored! extracted 2 items:\")\n    print(\"   - [issue] 502 Bad Gateway error\")\n    print(\"   - [context] port 3000 configuration\")\n\n    # 3. CONTEXT RETRIEVAL (PHASE 2)\n    print(\"\\n🔍 --- Phase 2: Retrieval on New Interaction (New Session) ---\")\n    print('👤 Captain: \"Hello, any updates?\"')\n    print_slow(\"🤖 Agent: (Searching vector store for user 'Captain'...)\", delay=0.01)\n\n    time.sleep(1.0)\n    print(\"\\n💡 Retrieved Context:\")\n    print(\"   Found Memory (Score: 0.98): User reported 502 error on port 3000\")\n    print(\"   Found Memory (Score: 0.95): User was frustrated with timeout\")\n\n    # 4. AGENT RESPONSE (PHASE 3)\n    print(\"\\n💬 --- Phase 3: Agent Response ---\")\n    response = '🤖 Agent: \"Welcome back, Captain. Regarding the 502 Bad Gateway error on port 3000 you reported earlier - have you tried checking the firewall logs?\"'\n    print_slow(response)\n\n    print(\"\\n✨ Demo Completed Successfully\")\n    print(\"===================================================\")\n\n\nif __name__ == \"__main__\":\n    run_rigorous_demo()\n"
  },
  {
    "path": "examples/test_nebius_provider.py",
    "content": "\"\"\"\nTest script to verify Nebius AI works with MemU.\n\nBefore running:\n1. Get your Nebius API key from https://tokenfactory.nebius.com\n2. Set environment variable: NEBIUS_API_KEY=your_key_here\n\nUsage:\n    cd memU\n    set NEBIUS_API_KEY=your_key_here\n    python examples/test_nebius_provider.py\n\nNebius provides:\n- Chat models: Qwen, DeepSeek, Llama, etc.\n- Embedding models: BGE, E5, Qwen3-Embedding\n- All via OpenAI-compatible API\n\"\"\"\n\nimport asyncio\nimport os\nimport sys\n\n# Add src to path for local development\nsrc_path = os.path.abspath(\"src\")\nsys.path.insert(0, src_path)\n\n# Nebius configuration\nNEBIUS_BASE_URL = \"https://api.tokenfactory.nebius.com/v1/\"\n# Available chat models (pick one):\n# - \"Qwen/Qwen3-30B-A3B-Instruct-2507\" (fast, cheap)\n# - \"Qwen/Qwen3-32B\" (good balance)\n# - \"deepseek-ai/DeepSeek-V3-0324\" (powerful)\n# - \"meta-llama/Llama-3.3-70B-Instruct\" (reliable)\nNEBIUS_CHAT_MODEL = \"Qwen/Qwen3-30B-A3B-Instruct-2507\"\n# Available embedding models:\n# - \"BAAI/bge-multilingual-gemma2\" (3584 dims, multilingual)\n# - \"BAAI/BGE-ICL\" (4096 dims)\n# - \"intfloat/e5-mistral-7b-instruct\" (4096 dims)\n# - \"Qwen/Qwen3-Embedding-8B\" (4096 dims)\nNEBIUS_EMBED_MODEL = \"BAAI/bge-multilingual-gemma2\"\n\n\nasync def test_nebius_chat():\n    \"\"\"Test Nebius chat completion.\"\"\"\n    from openai import AsyncOpenAI\n\n    api_key = os.environ.get(\"NEBIUS_API_KEY\")\n    if not api_key:\n        print(\"ERROR: Set NEBIUS_API_KEY environment variable\")\n        return False\n\n    client = AsyncOpenAI(\n        base_url=NEBIUS_BASE_URL,\n        api_key=api_key,\n    )\n\n    print(f\"Testing Nebius Chat API ({NEBIUS_CHAT_MODEL})...\")\n    try:\n        response = await client.chat.completions.create(\n            model=NEBIUS_CHAT_MODEL,\n            messages=[\n                {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n                {\"role\": \"user\", \"content\": \"Say hello in one sentence.\"},\n            ],\n            max_tokens=50,\n        )\n        content = response.choices[0].message.content\n        # Truncate long responses for display\n        display = content[:100] + \"...\" if len(content) > 100 else content\n        print(f\"  Response: {display}\")\n        print(\"  ✓ Chat API works!\")\n        return True\n    except Exception as e:\n        print(f\"  ✗ Chat API failed: {e}\")\n        return False\n\n\nasync def test_nebius_embeddings():\n    \"\"\"Test Nebius embeddings API.\"\"\"\n    from openai import AsyncOpenAI\n\n    api_key = os.environ.get(\"NEBIUS_API_KEY\")\n    if not api_key:\n        print(\"ERROR: Set NEBIUS_API_KEY environment variable\")\n        return False\n\n    client = AsyncOpenAI(\n        base_url=NEBIUS_BASE_URL,\n        api_key=api_key,\n    )\n\n    print(f\"\\nTesting Nebius Embeddings API ({NEBIUS_EMBED_MODEL})...\")\n    try:\n        response = await client.embeddings.create(\n            model=NEBIUS_EMBED_MODEL,\n            input=[\"Hello world\", \"This is a test\"],\n        )\n        print(f\"  Embedding dimensions: {len(response.data[0].embedding)}\")\n        print(f\"  Number of embeddings: {len(response.data)}\")\n        print(\"  ✓ Embeddings API works!\")\n        return True\n    except Exception as e:\n        print(f\"  ✗ Embeddings API failed: {e}\")\n        return False\n\n\nasync def test_memu_with_nebius():\n    \"\"\"Test MemU with Nebius as the LLM provider.\"\"\"\n    from memu.app import MemoryService\n\n    api_key = os.environ.get(\"NEBIUS_API_KEY\")\n    if not api_key:\n        print(\"ERROR: Set NEBIUS_API_KEY environment variable\")\n        return False\n\n    print(\"\\nTesting MemU with Nebius provider...\")\n\n    # Configure MemU to use Nebius (using dict config like example_1)\n    llm_profiles = {\n        \"default\": {\n            \"provider\": \"openai\",\n            \"base_url\": NEBIUS_BASE_URL,\n            \"api_key\": api_key,\n            \"chat_model\": NEBIUS_CHAT_MODEL,\n            \"client_backend\": \"sdk\",\n        },\n        \"embedding\": {\n            \"provider\": \"openai\",\n            \"base_url\": NEBIUS_BASE_URL,\n            \"api_key\": api_key,\n            \"embed_model\": NEBIUS_EMBED_MODEL,\n            \"client_backend\": \"sdk\",\n        },\n    }\n\n    try:\n        # Create MemU service with Nebius\n        service = MemoryService(llm_profiles=llm_profiles)\n        print(\"  ✓ MemoryService initialized with Nebius!\")\n\n        # Test memorize with a file (create temp file)\n        print(\"\\n  Testing memorize...\")\n        import tempfile\n\n        with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".txt\", delete=False, encoding=\"utf-8\") as f:\n            f.write(\"User likes Python programming and AI development. They prefer dark mode in their IDE.\")\n            temp_file = f.name\n\n        try:\n            result = await service.memorize(\n                resource_url=temp_file,\n                modality=\"text\",\n            )\n            items_count = len(result.get(\"items\", []))\n            categories_count = len(result.get(\"categories\", []))\n            print(f\"  ✓ Memorized! Items: {items_count}, Categories: {categories_count}\")\n\n            # Show what was extracted\n            for item in result.get(\"items\", [])[:3]:\n                summary = item.get(\"summary\", \"\")[:80]\n                print(f\"    - {summary}...\")\n        finally:\n            os.unlink(temp_file)\n\n        # Test retrieve\n        print(\"\\n  Testing retrieve...\")\n        retrieve_result = await service.retrieve(\n            queries=[{\"role\": \"user\", \"content\": \"What programming language does the user like?\"}]\n        )\n        print(f\"  ✓ Retrieved! Needs retrieval: {retrieve_result.get('needs_retrieval')}\")\n\n        items = retrieve_result.get(\"items\", [])\n        if items:\n            print(f\"  Found {len(items)} relevant items:\")\n            for item in items[:3]:\n                if isinstance(item, dict):\n                    summary = item.get(\"summary\", str(item))[:60]\n                else:\n                    summary = str(item)[:60]\n                print(f\"    - {summary}...\")\n\n        print(\"\\n\" + \"=\" * 60)\n        print(\"✓ SUCCESS: MemU works with Nebius!\")\n        print(\"=\" * 60)\n        return True\n\n    except Exception as e:\n        print(f\"  ✗ MemU with Nebius failed: {e}\")\n        import traceback\n\n        traceback.print_exc()\n        return False\n\n\nasync def main():\n    print(\"=\" * 60)\n    print(\"Nebius AI + MemU Integration Test\")\n    print(\"=\" * 60)\n    print(f\"Base URL: {NEBIUS_BASE_URL}\")\n    print(f\"Chat Model: {NEBIUS_CHAT_MODEL}\")\n    print(f\"Embed Model: {NEBIUS_EMBED_MODEL}\")\n    print(\"=\" * 60 + \"\\n\")\n\n    # Check for API key first\n    if not os.environ.get(\"NEBIUS_API_KEY\"):\n        print(\"ERROR: NEBIUS_API_KEY environment variable not set!\")\n        print(\"\\nTo get your API key:\")\n        print(\"1. Go to https://tokenfactory.nebius.com\")\n        print(\"2. Create an account / Log in\")\n        print(\"3. Get your API key\")\n        print(\"4. Run: set NEBIUS_API_KEY=your_key_here\")\n        return\n\n    # Test individual APIs first\n    chat_ok = await test_nebius_chat()\n    embed_ok = await test_nebius_embeddings()\n\n    if chat_ok and embed_ok:\n        # Test full MemU integration\n        await test_memu_with_nebius()\n    else:\n        print(\"\\n\" + \"=\" * 60)\n        print(\"✗ FAILED: Basic API tests failed, skipping MemU test\")\n        print(\"=\" * 60)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"memu-py\"\nversion = \"1.5.0\"\nauthors = [\n    {name = \"MemU Team\", email = \"contact@nevamind.ai\"},\n]\ndescription = \"AI Memory and Conversation Management Framework - Simple as mem0, Powerful as MemU\"\nreadme = \"README.md\"\n# license = {file = \"LICENSE\"}\nrequires-python = \">=3.13\"\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n\n    \"Operating System :: OS Independent\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n]\nkeywords = [\"ai\", \"memory\", \"conversation\", \"llm\", \"chatbot\", \"agent\"]\ndependencies = [\n    \"defusedxml>=0.7.1\",\n    \"httpx>=0.28.1\",\n    \"numpy>=2.3.4\",\n    \"openai>=2.8.0\",\n    \"pydantic>=2.12.4\",\n    \"sqlmodel>=0.0.27\",\n    \"alembic>=1.14.0\",\n    \"pendulum>=3.1.0\",\n    \"langchain-core>=1.2.7\",\n    \"lazyllm>=0.7.3\",\n]\n\n[build-system]\nrequires = [\"maturin>=1.0,<2.0\"]\nbuild-backend = \"maturin\"\n\n[tool.maturin]\nmodule-name = \"memu._core\"\npython-packages = [\"memu\"]\npython-source = \"src\"\n\n[dependency-groups]\ndev = [\n    {include-group = \"docs\"},\n    {include-group = \"lint\"},\n    {include-group = \"test\"},\n    \"langchain-openai>=1.1.7\",\n    \"langgraph>=1.0.6\",\n]\ndocs = [\n    \"mkdocs>=1.6.1\",\n    \"mkdocs-material>=9.6.23\",\n    \"mkdocstrings>=0.30.1\",\n    \"mkdocstrings-python>=1.18.2\",\n]\nlint = [\n    \"deptry>=0.23.1\",\n    \"mypy>=1.18.2\",\n    \"pre-commit>=4.3.0\",\n    \"ruff>=0.14.3\",\n    \"types-defusedxml>=0.7.0\",\n]\ntest = [\n    \"pytest>=8.4.2\",\n    \"pytest-asyncio>=0.24.0\",\n    \"pytest-cov>=7.0.0\",\n]\n\n[project.optional-dependencies]\npostgres = [\"pgvector>=0.3.4\", \"sqlalchemy[postgresql-psycopgbinary]>=2.0.36\"]\nlanggraph = [\"langgraph>=0.0.10\", \"langchain-core>=0.1.0\"]\nclaude = [\"claude-agent-sdk>=0.1.24\"]\n\n[project.urls]\n\"Homepage\" = \"https://github.com/NevaMind-AI/MemU\"\n\"Bug Tracker\" = \"https://github.com/NevaMind-AI/MemU/issues\"\n\"Documentation\" = \"https://github.com/NevaMind-AI/MemU#readme\"\n\n[project.scripts]\nmemu-server = \"memu.server.cli:main\"\n\n[tool.deptry.per_rule_ignores]\n# Optional dependencies used in examples/\nDEP002 = [\"claude-agent-sdk\"]\n\n[tool.mypy]\nfiles = [\"src\", \"tests\"]\npython_version = \"3.13\"\ndisallow_untyped_defs = true\ndisallow_any_unimported = true\nno_implicit_optional = true\ncheck_untyped_defs = true\nwarn_return_any = true\nwarn_unused_ignores = true\nshow_error_codes = true\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\ndisallow_incomplete_defs = false\nwarn_unused_ignores = false\ndisable_error_code = [\"attr-defined\", \"call-arg\"]\n\n[[tool.mypy.overrides]]\nmodule = [\"lazyllm.*\"]\nignore_missing_imports = true\n\n[[tool.mypy.overrides]]\nmodule = [\"pgvector.*\"]\nignore_missing_imports = true\n\n[[tool.mypy.overrides]]\nmodule = [\"memu.client.openai_wrapper\"]\ndisallow_untyped_defs = false\nwarn_return_any = false\n\n[tool.ruff]\ntarget-version = \"py313\"\nline-length = 120\nfix = true\n\n[tool.ruff.lint]\nselect = [\n    # flake8-2020\n    \"YTT\",\n    # flake8-bandit\n    \"S\",\n    # flake8-bugbear\n    \"B\",\n    # flake8-builtins\n    \"A\",\n    # flake8-comprehensions\n    \"C4\",\n    # flake8-debugger\n    \"T10\",\n    # flake8-simplify\n    \"SIM\",\n    # isort\n    \"I\",\n    # mccabe\n    \"C90\",\n    # pycodestyle\n    \"E\", \"W\",\n    # pyflakes\n    \"F\",\n    # pygrep-hooks\n    \"PGH\",\n    # pyupgrade\n    \"UP\",\n    # ruff\n    \"RUF\",\n    # tryceratops\n    \"TRY\",\n]\nignore = [\n    # LineTooLong\n    \"E501\",\n    # DoNotAssignLambda\n    \"E731\",\n]\n\n[tool.ruff.lint.per-file-ignores]\n\"tests/*\" = [\"S101\"]\n\"examples/*\" = [\"E402\", \"TRY003\", \"B904\", \"S104\", \"TRY300\", \"SIM108\"]\n\n[tool.ruff.format]\npreview = true\n\n[tool.coverage.report]\nskip_empty = true\n\n[tool.coverage.run]\nbranch = true\nsource = [\"memu\"]\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\nlog_cli = true\nlog_cli_level = \"INFO\"\nasyncio_mode = \"auto\"\n"
  },
  {
    "path": "readme/README_en.md",
    "content": "![MemU Banner](../assets/banner.png)\n\n<div align=\"center\">\n\n# memU\n\n### Always-On Proactive Memory for AI Agents\n\n[![PyPI version](https://badge.fury.io/py/memu-py.svg)](https://badge.fury.io/py/memu-py)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/memu)\n[![Twitter](https://img.shields.io/badge/Twitter-Follow-1DA1F2?logo=x&logoColor=white)](https://x.com/memU_ai)\n\n<a href=\"https://trendshift.io/repositories/17374\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/17374\" alt=\"NevaMind-AI%2FmemU | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n**[English](README_en.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Français](README_fr.md)**\n\n</div>\n\n---\n\nmemU is a memory framework built for **24/7 proactive agents**.\nIt is designed for long-running use and greatly **reduces the LLM token cost** of keeping agents always online, making always-on, evolving agents practical in production systems.\nmemU **continuously captures and understands user intent**. Even without a command, the agent can tell what you are about to do and act on it by itself.\n\n---\n\n## 🤖 [OpenClaw (Moltbot, Clawdbot) Alternative](https://memu.bot)\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/memUbot.png\" />\n\n- **Download-and-use and simple** to get started.\n- Builds long-term memory to **understand user intent** and act proactively.\n- **Cuts LLM token cost** with smaller context.\n\nTry now: [memU bot](https://memu.bot)\n\n---\n\n## 🗃️ Memory as File System, File System as Memory\n\nmemU treats **memory like a file system**—structured, hierarchical, and instantly accessible.\n\n| File System | memU Memory |\n|-------------|-------------|\n| 📁 Folders | 🏷️ Categories (auto-organized topics) |\n| 📄 Files | 🧠 Memory Items (extracted facts, preferences, skills) |\n| 🔗 Symlinks | 🔄 Cross-references (related memories linked) |\n| 📂 Mount points | 📥 Resources (conversations, documents, images) |\n\n**Why this matters:**\n- **Navigate memories** like browsing directories—drill down from broad categories to specific facts\n- **Mount new knowledge** instantly—conversations and documents become queryable memory\n- **Cross-link everything**—memories reference each other, building a connected knowledge graph\n- **Persistent & portable**—export, backup, and transfer memory like files\n\n```\nmemory/\n├── preferences/\n│   ├── communication_style.md\n│   └── topic_interests.md\n├── relationships/\n│   ├── contacts/\n│   └── interaction_history/\n├── knowledge/\n│   ├── domain_expertise/\n│   └── learned_skills/\n└── context/\n    ├── recent_conversations/\n    └── pending_tasks/\n```\n\nJust as a file system turns raw bytes into organized data, memU transforms raw interactions into **structured, searchable, proactive intelligence**.\n\n---\n\n## ⭐️ Star the repository\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/star.gif\" />\nIf you find memU useful or interesting, a GitHub Star ⭐️ would be greatly appreciated.\n\n---\n\n\n## ✨ Core Features\n\n| Capability | Description |\n|------------|-------------|\n| 🤖 **24/7 Proactive Agent** | Always-on memory agent that works continuously in the background—never sleeps, never forgets |\n| 🎯 **User Intention Capture** | Understands and remembers user goals, preferences, and context across sessions automatically |\n| 💰 **Cost Efficient** | Reduces long-running token costs by caching insights and avoiding redundant LLM calls |\n---\n\n## 🔄 How Proactive Memory Works\n\n```bash\n\ncd examples/proactive\npython proactive.py\n\n```\n\n---\n\n### Proactive Memory Lifecycle\n```\n┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\n│                                         USER QUERY                                               │\n└──────────────────────────────────────────────────────────────────────────────────────────────────┘\n                 │                                                           │\n                 ▼                                                           ▼\n┌────────────────────────────────────────┐         ┌────────────────────────────────────────────────┐\n│         🤖 MAIN AGENT                  │         │              🧠 MEMU BOT                       │\n│                                        │         │                                                │\n│  Handle user queries & execute tasks   │  ◄───►  │  Monitor, memorize & proactive intelligence   │\n├────────────────────────────────────────┤         ├────────────────────────────────────────────────┤\n│                                        │         │                                                │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  1. RECEIVE USER INPUT           │  │         │  │  1. MONITOR INPUT/OUTPUT                 │  │\n│  │     Parse query, understand      │  │   ───►  │  │     Observe agent interactions           │  │\n│  │     context and intent           │  │         │  │     Track conversation flow              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  2. PLAN & EXECUTE               │  │         │  │  2. MEMORIZE & EXTRACT                   │  │\n│  │     Break down tasks             │  │   ◄───  │  │     Store insights, facts, preferences   │  │\n│  │     Call tools, retrieve data    │  │  inject │  │     Extract skills & knowledge           │  │\n│  │     Generate responses           │  │  memory │  │     Update user profile                  │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  3. RESPOND TO USER              │  │         │  │  3. PREDICT USER INTENT                  │  │\n│  │     Deliver answer/result        │  │   ───►  │  │     Anticipate next steps                │  │\n│  │     Continue conversation        │  │         │  │     Identify upcoming needs              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  4. LOOP                         │  │         │  │  4. RUN PROACTIVE TASKS                  │  │\n│  │     Wait for next user input     │  │   ◄───  │  │     Pre-fetch relevant context           │  │\n│  │     or proactive suggestions     │  │  suggest│  │     Prepare recommendations              │  │\n│  └──────────────────────────────────┘  │         │  │     Update todolist autonomously         │  │\n│                                        │         │  └──────────────────────────────────────────┘  │\n└────────────────────────────────────────┘         └────────────────────────────────────────────────┘\n                 │                                                           │\n                 └───────────────────────────┬───────────────────────────────┘\n                                             ▼\n                              ┌──────────────────────────────┐\n                              │     CONTINUOUS SYNC LOOP     │\n                              │  Agent ◄──► MemU Bot ◄──► DB │\n                              └──────────────────────────────┘\n```\n\n---\n\n## 🎯 Proactive Use Cases\n\n### 1. **Information Recommendation**\n*Agent monitors interests and proactively surfaces relevant content*\n```python\n# User has been researching AI topics\nMemU tracks: reading history, saved articles, search queries\n\n# When new content arrives:\nAgent: \"I found 3 new papers on RAG optimization that align with\n        your recent research on retrieval systems. One author\n        (Dr. Chen) you've cited before published yesterday.\"\n\n# Proactive behaviors:\n- Learns topic preferences from browsing patterns\n- Tracks author/source credibility preferences\n- Filters noise based on engagement history\n- Times recommendations for optimal attention\n```\n\n### 2. **Email Management**\n*Agent learns communication patterns and handles routine correspondence*\n```python\n# MemU observes email patterns over time:\n- Response templates for common scenarios\n- Priority contacts and urgent keywords\n- Scheduling preferences and availability\n- Writing style and tone variations\n\n# Proactive email assistance:\nAgent: \"You have 12 new emails. I've drafted responses for 3 routine\n        requests and flagged 2 urgent items from your priority contacts.\n        Should I also reschedule tomorrow's meeting based on the\n        conflict John mentioned?\"\n\n# Autonomous actions:\n✓ Draft context-aware replies\n✓ Categorize and prioritize inbox\n✓ Detect scheduling conflicts\n✓ Summarize long threads with key decisions\n```\n\n### 3. **Trading & Financial Monitoring**\n*Agent tracks market context and user investment behavior*\n```python\n# MemU learns trading preferences:\n- Risk tolerance from historical decisions\n- Preferred sectors and asset classes\n- Response patterns to market events\n- Portfolio rebalancing triggers\n\n# Proactive alerts:\nAgent: \"NVDA dropped 5% in after-hours trading. Based on your past\n        behavior, you typically buy tech dips above 3%. Your current\n        allocation allows for $2,000 additional exposure while\n        maintaining your 70/30 equity-bond target.\"\n\n# Continuous monitoring:\n- Track price alerts tied to user-defined thresholds\n- Correlate news events with portfolio impact\n- Learn from executed vs. ignored recommendations\n- Anticipate tax-loss harvesting opportunities\n```\n\n\n...\n\n---\n\n## 🗂️ Hierarchical Memory Architecture\n\nMemU's three-layer system enables both **reactive queries** and **proactive context loading**:\n\n<img width=\"100%\" alt=\"structure\" src=\"../assets/structure.png\" />\n\n| Layer | Reactive Use | Proactive Use |\n|-------|--------------|---------------|\n| **Resource** | Direct access to original data | Background monitoring for new patterns |\n| **Item** | Targeted fact retrieval | Real-time extraction from ongoing interactions |\n| **Category** | Summary-level overview | Automatic context assembly for anticipation |\n\n**Proactive Benefits:**\n- **Auto-categorization**: New memories self-organize into topics\n- **Pattern Detection**: System identifies recurring themes\n- **Context Prediction**: Anticipates what information will be needed next\n\n---\n\n## 🚀 Quick Start\n\n### Option 1: Cloud Version\n\nExperience proactive memory instantly:\n\n👉 **[memu.so](https://memu.so)** - Hosted service with 7×24 continuous learning\n\nFor enterprise deployment with custom proactive workflows, contact **info@nevamind.ai**\n\n#### Cloud API (v3)\n\n| Base URL | `https://api.memu.so` |\n|----------|----------------------|\n| Auth | `Authorization: Bearer YOUR_API_KEY` |\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `/api/v3/memory/memorize` | Register continuous learning task |\n| `GET` | `/api/v3/memory/memorize/status/{task_id}` | Check real-time processing status |\n| `POST` | `/api/v3/memory/categories` | List auto-generated categories |\n| `POST` | `/api/v3/memory/retrieve` | Query memory (supports proactive context loading) |\n\n📚 **[Full API Documentation](https://memu.pro/docs#cloud-version)**\n\n---\n\n### Option 2: Self-Hosted\n\n#### Installation\n```bash\npip install -e .\n```\n\n#### Basic Example\n\n> **Requirements**: Python 3.13+ and an OpenAI API key\n\n**Test Continuous Learning** (in-memory):\n```bash\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_inmemory.py\n```\n\n**Test with Persistent Storage** (PostgreSQL):\n```bash\n# Start PostgreSQL with pgvector\ndocker run -d \\\n  --name memu-postgres \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=memu \\\n  -p 5432:5432 \\\n  pgvector/pgvector:pg16\n\n# Run continuous learning test\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_postgres.py\n```\n\nBoth examples demonstrate **proactive memory workflows**:\n1. **Continuous Ingestion**: Process multiple files sequentially\n2. **Auto-Extraction**: Immediate memory creation\n3. **Proactive Retrieval**: Context-aware memory surfacing\n\nSee [`tests/test_inmemory.py`](../tests/test_inmemory.py) and [`tests/test_postgres.py`](../tests/test_postgres.py) for implementation details.\n\n---\n\n### Custom LLM and Embedding Providers\n\nMemU supports custom LLM and embedding providers beyond OpenAI. Configure them via `llm_profiles`:\n```python\nfrom memu import MemUService\n\nservice = MemUService(\n    llm_profiles={\n        # Default profile for LLM operations\n        \"default\": {\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n            \"api_key\": \"your_api_key\",\n            \"chat_model\": \"qwen3-max\",\n            \"client_backend\": \"sdk\"  # \"sdk\" or \"http\"\n        },\n        # Separate profile for embeddings\n        \"embedding\": {\n            \"base_url\": \"https://api.voyageai.com/v1\",\n            \"api_key\": \"your_voyage_api_key\",\n            \"embed_model\": \"voyage-3.5-lite\"\n        }\n    },\n    # ... other configuration\n)\n```\n\n---\n\n### OpenRouter Integration\n\nMemU supports [OpenRouter](https://openrouter.ai) as a model provider, giving you access to multiple LLM providers through a single API.\n\n#### Configuration\n```python\nfrom memu import MemoryService\n\nservice = MemoryService(\n    llm_profiles={\n        \"default\": {\n            \"provider\": \"openrouter\",\n            \"client_backend\": \"httpx\",\n            \"base_url\": \"https://openrouter.ai\",\n            \"api_key\": \"your_openrouter_api_key\",\n            \"chat_model\": \"anthropic/claude-3.5-sonnet\",  # Any OpenRouter model\n            \"embed_model\": \"openai/text-embedding-3-small\",  # Embedding model\n        },\n    },\n    database_config={\n        \"metadata_store\": {\"provider\": \"inmemory\"},\n    },\n)\n```\n\n#### Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `OPENROUTER_API_KEY` | Your OpenRouter API key from [openrouter.ai/keys](https://openrouter.ai/keys) |\n\n#### Supported Features\n\n| Feature | Status | Notes |\n|---------|--------|-------|\n| Chat Completions | Supported | Works with any OpenRouter chat model |\n| Embeddings | Supported | Use OpenAI embedding models via OpenRouter |\n| Vision | Supported | Use vision-capable models (e.g., `openai/gpt-4o`) |\n\n#### Running OpenRouter Tests\n```bash\nexport OPENROUTER_API_KEY=your_api_key\n\n# Full workflow test (memorize + retrieve)\npython tests/test_openrouter.py\n\n# Embedding-specific tests\npython tests/test_openrouter_embedding.py\n\n# Vision-specific tests\npython tests/test_openrouter_vision.py\n```\n\nSee [`examples/example_4_openrouter_memory.py`](../examples/example_4_openrouter_memory.py) for a complete working example.\n\n---\n\n## 📖 Core APIs\n\n### `memorize()` - Continuous Learning Pipeline\n\nProcesses inputs in real-time and immediately updates memory:\n\n<img width=\"100%\" alt=\"memorize\" src=\"../assets/memorize.png\" />\n\n```python\nresult = await service.memorize(\n    resource_url=\"path/to/file.json\",  # File path or URL\n    modality=\"conversation\",            # conversation | document | image | video | audio\n    user={\"user_id\": \"123\"}             # Optional: scope to a user\n)\n\n# Returns immediately with extracted memory:\n{\n    \"resource\": {...},      # Stored resource metadata\n    \"items\": [...],         # Extracted memory items (available instantly)\n    \"categories\": [...]     # Auto-updated category structure\n}\n```\n\n**Proactive Features:**\n- Zero-delay processing—memories available immediately\n- Automatic categorization without manual tagging\n- Cross-reference with existing memories for pattern detection\n\n### `retrieve()` - Dual-Mode Intelligence\n\nMemU supports both **proactive context loading** and **reactive querying**:\n\n<img width=\"100%\" alt=\"retrieve\" src=\"../assets/retrieve.png\" />\n\n#### RAG-based Retrieval (`method=\"rag\"`)\n\nFast **proactive context assembly** using embeddings:\n\n- ✅ **Instant context**: Sub-second memory surfacing\n- ✅ **Background monitoring**: Can run continuously without LLM costs\n- ✅ **Similarity scoring**: Identifies most relevant memories automatically\n\n#### LLM-based Retrieval (`method=\"llm\"`)\n\nDeep **anticipatory reasoning** for complex contexts:\n\n- ✅ **Intent prediction**: LLM infers what user needs before they ask\n- ✅ **Query evolution**: Automatically refines search as context develops\n- ✅ **Early termination**: Stops when sufficient context is gathered\n\n#### Comparison\n\n| Aspect | RAG (Fast Context) | LLM (Deep Reasoning) |\n|--------|-------------------|---------------------|\n| **Speed** | ⚡ Milliseconds | 🐢 Seconds |\n| **Cost** | 💰 Embedding only | 💰💰 LLM inference |\n| **Proactive use** | Continuous monitoring | Triggered context loading |\n| **Best for** | Real-time suggestions | Complex anticipation |\n\n#### Usage\n```python\n# Proactive retrieval with context history\nresult = await service.retrieve(\n    queries=[\n        {\"role\": \"user\", \"content\": {\"text\": \"What are their preferences?\"}},\n        {\"role\": \"user\", \"content\": {\"text\": \"Tell me about work habits\"}}\n    ],\n    where={\"user_id\": \"123\"},  # Optional: scope filter\n    method=\"rag\"  # or \"llm\" for deeper reasoning\n)\n\n# Returns context-aware results:\n{\n    \"categories\": [...],     # Relevant topic areas (auto-prioritized)\n    \"items\": [...],          # Specific memory facts\n    \"resources\": [...],      # Original sources for traceability\n    \"next_step_query\": \"...\" # Predicted follow-up context\n}\n```\n\n**Proactive Filtering**: Use `where` to scope continuous monitoring:\n- `where={\"user_id\": \"123\"}` - User-specific context\n- `where={\"agent_id__in\": [\"1\", \"2\"]}` - Multi-agent coordination\n- Omit `where` for global context awareness\n\n> 📚 **For complete API documentation**, see [SERVICE_API.md](../docs/SERVICE_API.md) - includes proactive workflow patterns, pipeline configuration, and real-time update handling.\n\n---\n\n## 💡 Proactive Scenarios\n\n### Example 1: Always-Learning Assistant\n\nContinuously learns from every interaction without explicit memory commands:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_1_conversation_memory.py\n```\n\n**Proactive Behavior:**\n- Automatically extracts preferences from casual mentions\n- Builds relationship models from interaction patterns\n- Surfaces relevant context in future conversations\n- Adapts communication style based on learned preferences\n\n**Best for:** Personal AI assistants, customer support that remembers, social chatbots\n\n---\n\n### Example 2: Self-Improving Agent\n\nLearns from execution logs and proactively suggests optimizations:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_2_skill_extraction.py\n```\n\n**Proactive Behavior:**\n- Monitors agent actions and outcomes continuously\n- Identifies patterns in successes and failures\n- Auto-generates skill guides from experience\n- Proactively suggests strategies for similar future tasks\n\n**Best for:** DevOps automation, agent self-improvement, knowledge capture\n\n---\n\n### Example 3: Multimodal Context Builder\n\nUnifies memory across different input types for comprehensive context:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_3_multimodal_memory.py\n```\n\n**Proactive Behavior:**\n- Cross-references text, images, and documents automatically\n- Builds unified understanding across modalities\n- Surfaces visual context when discussing related topics\n- Anticipates information needs by combining multiple sources\n\n**Best for:** Documentation systems, learning platforms, research assistants\n\n---\n\n## 📊 Performance\n\nMemU achieves **92.09% average accuracy** on the Locomo benchmark across all reasoning tasks, demonstrating reliable proactive memory operations.\n\n<img width=\"100%\" alt=\"benchmark\" src=\"https://github.com/user-attachments/assets/6fec4884-94e5-4058-ad5c-baac3d7e76d9\" />\n\nView detailed experimental data: [memU-experiment](https://github.com/NevaMind-AI/memU-experiment)\n\n---\n\n## 🧩 Ecosystem\n\n| Repository | Description | Proactive Features |\n|------------|-------------|-------------------|\n| **[memU](https://github.com/NevaMind-AI/memU)** | Core proactive memory engine | 7×24 learning pipeline, auto-categorization |\n| **[memU-server](https://github.com/NevaMind-AI/memU-server)** | Backend with continuous sync | Real-time memory updates, webhook triggers |\n| **[memU-ui](https://github.com/NevaMind-AI/memU-ui)** | Visual memory dashboard | Live memory evolution monitoring |\n\n**Quick Links:**\n- 🚀 [Try MemU Cloud](https://app.memu.so/quick-start)\n- 📚 [API Documentation](https://memu.pro/docs)\n- 💬 [Discord Community](https://discord.gg/memu)\n\n---\n\n## 🤝 Partners\n\n<div align=\"center\">\n\n<a href=\"https://github.com/TEN-framework/ten-framework\"><img src=\"https://avatars.githubusercontent.com/u/113095513?s=200&v=4\" alt=\"Ten\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://openagents.org\"><img src=\"../assets/partners/openagents.png\" alt=\"OpenAgents\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/milvus-io/milvus\"><img src=\"https://miro.medium.com/v2/resize:fit:2400/1*-VEGyAgcIBD62XtZWavy8w.png\" alt=\"Milvus\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://xroute.ai/\"><img src=\"../assets/partners/xroute.png\" alt=\"xRoute\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://jaaz.app/\"><img src=\"../assets/partners/jazz.png\" alt=\"Jazz\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/Buddie-AI/Buddie\"><img src=\"../assets/partners/buddie.png\" alt=\"Buddie\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/bytebase/bytebase\"><img src=\"../assets/partners/bytebase.png\" alt=\"Bytebase\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/LazyAGI/LazyLLM\"><img src=\"../assets/partners/LazyLLM.png\" alt=\"LazyLLM\" height=\"40\" style=\"margin: 10px;\"></a>\n\n</div>\n\n---\n\n## 🤝 How to Contribute\n\nWe welcome contributions from the community! Whether you're fixing bugs, adding features, or improving documentation, your help is appreciated.\n\n### Getting Started\n\nTo start contributing to MemU, you'll need to set up your development environment:\n\n#### Prerequisites\n- Python 3.13+\n- [uv](https://github.com/astral-sh/uv) (Python package manager)\n- Git\n\n#### Setup Development Environment\n```bash\n# 1. Fork and clone the repository\ngit clone https://github.com/YOUR_USERNAME/memU.git\ncd memU\n\n# 2. Install development dependencies\nmake install\n```\n\nThe `make install` command will:\n- Create a virtual environment using `uv`\n- Install all project dependencies\n- Set up pre-commit hooks for code quality checks\n\n#### Running Quality Checks\n\nBefore submitting your contribution, ensure your code passes all quality checks:\n```bash\nmake check\n```\n\nThe `make check` command runs:\n- **Lock file verification**: Ensures `pyproject.toml` consistency\n- **Pre-commit hooks**: Lints code with Ruff, formats with Black\n- **Type checking**: Runs `mypy` for static type analysis\n- **Dependency analysis**: Uses `deptry` to find obsolete dependencies\n\n### Contributing Guidelines\n\nFor detailed contribution guidelines, code standards, and development practices, please see [CONTRIBUTING.md](../CONTRIBUTING.md).\n\n**Quick tips:**\n- Create a new branch for each feature or bug fix\n- Write clear commit messages\n- Add tests for new functionality\n- Update documentation as needed\n- Run `make check` before pushing\n\n---\n\n## 📄 License\n\n[Apache License 2.0](../LICENSE.txt)\n\n---\n\n## 🌍 Community\n\n- **GitHub Issues**: [Report bugs & request features](https://github.com/NevaMind-AI/memU/issues)\n- **Discord**: [Join the community](https://discord.com/invite/hQZntfGsbJ)\n- **X (Twitter)**: [Follow @memU_ai](https://x.com/memU_ai)\n- **Contact**: info@nevamind.ai\n\n---\n\n<div align=\"center\">\n\n⭐ **Star us on GitHub** to get notified about new releases!\n\n</div>\n"
  },
  {
    "path": "readme/README_es.md",
    "content": "![MemU Banner](../assets/banner.png)\n\n<div align=\"center\">\n\n# memU\n\n### Memoria Proactiva Siempre Activa para Agentes de IA\n\n[![PyPI version](https://badge.fury.io/py/memu-py.svg)](https://badge.fury.io/py/memu-py)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/memu)\n[![Twitter](https://img.shields.io/badge/Twitter-Follow-1DA1F2?logo=x&logoColor=white)](https://x.com/memU_ai)\n\n<a href=\"https://trendshift.io/repositories/17374\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/17374\" alt=\"NevaMind-AI%2FmemU | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n**[English](README_en.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Français](README_fr.md)**\n\n</div>\n\n---\n\nmemU es un framework de memoria construido para **agentes proactivos 24/7**.\nEstá diseñado para uso prolongado y **reduce enormemente el costo de tokens LLM** de mantener agentes siempre en línea, haciendo que los agentes siempre activos y en evolución sean prácticos en sistemas de producción.\nmemU **captura y comprende continuamente la intención del usuario**. Incluso sin un comando, el agente puede detectar lo que estás a punto de hacer y actuar por sí mismo.\n\n---\n\n## 🤖 [OpenClaw (Moltbot, Clawdbot) Alternative](https://memu.bot)\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/memUbot.png\" />\n\n- **Download-and-use and simple** to get started.\n- Builds long-term memory to **understand user intent** and act proactively.\n- **Cuts LLM token cost** with smaller context.\n\nTry now: [memU bot](https://memu.bot)\n\n---\n\n## 🗃️ Memoria como Sistema de Archivos, Sistema de Archivos como Memoria\n\nmemU trata la **memoria como un sistema de archivos**—estructurada, jerárquica e instantáneamente accesible.\n\n| Sistema de Archivos | Memoria memU |\n|--------------------|--------------|\n| 📁 Carpetas | 🏷️ Categorías (temas auto-organizados) |\n| 📄 Archivos | 🧠 Elementos de Memoria (hechos, preferencias, habilidades extraídas) |\n| 🔗 Enlaces simbólicos | 🔄 Referencias cruzadas (memorias relacionadas enlazadas) |\n| 📂 Puntos de montaje | 📥 Recursos (conversaciones, documentos, imágenes) |\n\n**Por qué esto importa:**\n- **Navega memorias** como si exploraras directorios—profundiza desde categorías amplias a hechos específicos\n- **Monta nuevo conocimiento** instantáneamente—conversaciones y documentos se convierten en memoria consultable\n- **Enlaza todo cruzadamente**—las memorias se referencian entre sí, construyendo un grafo de conocimiento conectado\n- **Persistente y portable**—exporta, respalda y transfiere memoria como archivos\n\n```\nmemory/\n├── preferences/\n│   ├── communication_style.md\n│   └── topic_interests.md\n├── relationships/\n│   ├── contacts/\n│   └── interaction_history/\n├── knowledge/\n│   ├── domain_expertise/\n│   └── learned_skills/\n└── context/\n    ├── recent_conversations/\n    └── pending_tasks/\n```\n\nAsí como un sistema de archivos convierte bytes crudos en datos organizados, memU transforma interacciones crudas en **inteligencia estructurada, buscable y proactiva**.\n\n---\n\n## ⭐️ Dale una estrella al repositorio\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/star.gif\" />\nSi encuentras memU útil o interesante, te agradeceríamos mucho una estrella en GitHub ⭐️.\n\n---\n\n\n## ✨ Características Principales\n\n| Capacidad | Descripción |\n|-----------|-------------|\n| 🤖 **Agente Proactivo 24/7** | Agente de memoria siempre activo que trabaja continuamente en segundo plano—nunca duerme, nunca olvida |\n| 🎯 **Captura de Intención del Usuario** | Comprende y recuerda automáticamente objetivos, preferencias y contexto del usuario a través de sesiones |\n| 💰 **Eficiente en Costos** | Reduce costos de tokens a largo plazo mediante caché de insights y evitando llamadas LLM redundantes |\n---\n\n## 🔄 Cómo Funciona la Memoria Proactiva\n\n```bash\n\ncd examples/proactive\npython proactive.py\n\n```\n\n---\n\n### Proactive Memory Lifecycle\n```\n┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\n│                                         USER QUERY                                               │\n└──────────────────────────────────────────────────────────────────────────────────────────────────┘\n                 │                                                           │\n                 ▼                                                           ▼\n┌────────────────────────────────────────┐         ┌────────────────────────────────────────────────┐\n│         🤖 MAIN AGENT                  │         │              🧠 MEMU BOT                       │\n│                                        │         │                                                │\n│  Handle user queries & execute tasks   │  ◄───►  │  Monitor, memorize & proactive intelligence   │\n├────────────────────────────────────────┤         ├────────────────────────────────────────────────┤\n│                                        │         │                                                │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  1. RECEIVE USER INPUT           │  │         │  │  1. MONITOR INPUT/OUTPUT                 │  │\n│  │     Parse query, understand      │  │   ───►  │  │     Observe agent interactions           │  │\n│  │     context and intent           │  │         │  │     Track conversation flow              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  2. PLAN & EXECUTE               │  │         │  │  2. MEMORIZE & EXTRACT                   │  │\n│  │     Break down tasks             │  │   ◄───  │  │     Store insights, facts, preferences   │  │\n│  │     Call tools, retrieve data    │  │  inject │  │     Extract skills & knowledge           │  │\n│  │     Generate responses           │  │  memory │  │     Update user profile                  │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  3. RESPOND TO USER              │  │         │  │  3. PREDICT USER INTENT                  │  │\n│  │     Deliver answer/result        │  │   ───►  │  │     Anticipate next steps                │  │\n│  │     Continue conversation        │  │         │  │     Identify upcoming needs              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  4. LOOP                         │  │         │  │  4. RUN PROACTIVE TASKS                  │  │\n│  │     Wait for next user input     │  │   ◄───  │  │     Pre-fetch relevant context           │  │\n│  │     or proactive suggestions     │  │  suggest│  │     Prepare recommendations              │  │\n│  └──────────────────────────────────┘  │         │  │     Update todolist autonomously         │  │\n│                                        │         │  └──────────────────────────────────────────┘  │\n└────────────────────────────────────────┘         └────────────────────────────────────────────────┘\n                 │                                                           │\n                 └───────────────────────────┬───────────────────────────────┘\n                                             ▼\n                              ┌──────────────────────────────┐\n                              │     CONTINUOUS SYNC LOOP     │\n                              │  Agent ◄──► MemU Bot ◄──► DB │\n                              └──────────────────────────────┘\n```\n\n---\n\n## 🎯 Casos de Uso Proactivos\n\n### 1. **Recomendación de Información**\n*El agente monitorea intereses y muestra proactivamente contenido relevante*\n```python\n# El usuario ha estado investigando temas de IA\nMemU rastrea: historial de lectura, artículos guardados, consultas de búsqueda\n\n# Cuando llega nuevo contenido:\nAgente: \"Encontré 3 nuevos papers sobre optimización RAG que se alinean con\n        tu investigación reciente sobre sistemas de recuperación. Un autor\n        (Dr. Chen) que has citado antes publicó ayer.\"\n\n# Comportamientos proactivos:\n- Aprende preferencias de temas de patrones de navegación\n- Rastrea preferencias de credibilidad de autor/fuente\n- Filtra ruido basado en historial de interacción\n- Programa recomendaciones para atención óptima\n```\n\n### 2. **Gestión de Email**\n*El agente aprende patrones de comunicación y maneja correspondencia rutinaria*\n```python\n# MemU observa patrones de email con el tiempo:\n- Plantillas de respuesta para escenarios comunes\n- Contactos prioritarios y palabras clave urgentes\n- Preferencias de programación y disponibilidad\n- Variaciones de estilo de escritura y tono\n\n# Asistencia proactiva de email:\nAgente: \"Tienes 12 nuevos emails. He redactado respuestas para 3 solicitudes\n        rutinarias y marcado 2 elementos urgentes de tus contactos prioritarios.\n        ¿Debería también reprogramar la reunión de mañana basándome en el\n        conflicto que mencionó John?\"\n\n# Acciones autónomas:\n✓ Redactar respuestas conscientes del contexto\n✓ Categorizar y priorizar bandeja de entrada\n✓ Detectar conflictos de programación\n✓ Resumir hilos largos con decisiones clave\n```\n\n### 3. **Trading y Monitoreo Financiero**\n*El agente rastrea contexto del mercado y comportamiento de inversión del usuario*\n```python\n# MemU aprende preferencias de trading:\n- Tolerancia al riesgo de decisiones históricas\n- Sectores y clases de activos preferidos\n- Patrones de respuesta a eventos del mercado\n- Disparadores de rebalanceo de portafolio\n\n# Alertas proactivas:\nAgente: \"NVDA cayó 5% en trading after-hours. Basándome en tu comportamiento\n        pasado, típicamente compras caídas tech superiores al 3%. Tu asignación\n        actual permite $2,000 de exposición adicional manteniendo tu objetivo\n        70/30 acciones-bonos.\"\n\n# Monitoreo continuo:\n- Rastrear alertas de precio vinculadas a umbrales definidos por usuario\n- Correlacionar eventos de noticias con impacto en portafolio\n- Aprender de recomendaciones ejecutadas vs. ignoradas\n- Anticipar oportunidades de cosecha de pérdidas fiscales\n```\n\n\n...\n\n---\n\n## 🗂️ Arquitectura de Memoria Jerárquica\n\nEl sistema de tres capas de MemU permite tanto **consultas reactivas** como **carga proactiva de contexto**:\n\n<img width=\"100%\" alt=\"structure\" src=\"../assets/structure.png\" />\n\n| Capa | Uso Reactivo | Uso Proactivo |\n|------|--------------|---------------|\n| **Recurso** | Acceso directo a datos originales | Monitoreo en segundo plano de nuevos patrones |\n| **Elemento** | Recuperación de hechos específicos | Extracción en tiempo real de interacciones en curso |\n| **Categoría** | Vista general a nivel de resumen | Ensamblaje automático de contexto para anticipación |\n\n**Beneficios Proactivos:**\n- **Auto-categorización**: Nuevas memorias se auto-organizan en temas\n- **Detección de Patrones**: El sistema identifica temas recurrentes\n- **Predicción de Contexto**: Anticipa qué información se necesitará después\n\n---\n\n## 🚀 Inicio Rápido\n\n### Opción 1: Versión en la Nube\n\nExperimenta la memoria proactiva instantáneamente:\n\n👉 **[memu.so](https://memu.so)** - Servicio hospedado con aprendizaje continuo 7×24\n\nPara despliegue empresarial con flujos de trabajo proactivos personalizados, contacta **info@nevamind.ai**\n\n#### API en la Nube (v3)\n\n| URL Base | `https://api.memu.so` |\n|----------|----------------------|\n| Auth | `Authorization: Bearer YOUR_API_KEY` |\n\n| Método | Endpoint | Descripción |\n|--------|----------|-------------|\n| `POST` | `/api/v3/memory/memorize` | Registrar tarea de aprendizaje continuo |\n| `GET` | `/api/v3/memory/memorize/status/{task_id}` | Verificar estado de procesamiento en tiempo real |\n| `POST` | `/api/v3/memory/categories` | Listar categorías auto-generadas |\n| `POST` | `/api/v3/memory/retrieve` | Consultar memoria (soporta carga proactiva de contexto) |\n\n📚 **[Documentación Completa de API](https://memu.pro/docs#cloud-version)**\n\n---\n\n### Opción 2: Auto-Hospedado\n\n#### Instalación\n```bash\npip install -e .\n```\n\n#### Ejemplo Básico\n\n> **Requisitos**: Python 3.13+ y una clave API de OpenAI\n\n**Probar Aprendizaje Continuo** (en memoria):\n```bash\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_inmemory.py\n```\n\n**Probar con Almacenamiento Persistente** (PostgreSQL):\n```bash\n# Iniciar PostgreSQL con pgvector\ndocker run -d \\\n  --name memu-postgres \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=memu \\\n  -p 5432:5432 \\\n  pgvector/pgvector:pg16\n\n# Ejecutar prueba de aprendizaje continuo\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_postgres.py\n```\n\nAmbos ejemplos demuestran **flujos de trabajo de memoria proactiva**:\n1. **Ingesta Continua**: Procesar múltiples archivos secuencialmente\n2. **Auto-Extracción**: Creación inmediata de memoria\n3. **Recuperación Proactiva**: Presentación de memoria consciente del contexto\n\nVer [`tests/test_inmemory.py`](../tests/test_inmemory.py) y [`tests/test_postgres.py`](../tests/test_postgres.py) para detalles de implementación.\n\n---\n\n### Proveedores Personalizados de LLM y Embeddings\n\nMemU soporta proveedores personalizados de LLM y embeddings más allá de OpenAI. Configúralos via `llm_profiles`:\n```python\nfrom memu import MemUService\n\nservice = MemUService(\n    llm_profiles={\n        # Perfil predeterminado para operaciones LLM\n        \"default\": {\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n            \"api_key\": \"your_api_key\",\n            \"chat_model\": \"qwen3-max\",\n            \"client_backend\": \"sdk\"  # \"sdk\" o \"http\"\n        },\n        # Perfil separado para embeddings\n        \"embedding\": {\n            \"base_url\": \"https://api.voyageai.com/v1\",\n            \"api_key\": \"your_voyage_api_key\",\n            \"embed_model\": \"voyage-3.5-lite\"\n        }\n    },\n    # ... otra configuración\n)\n```\n\n---\n\n### Integración con OpenRouter\n\nMemU soporta [OpenRouter](https://openrouter.ai) como proveedor de modelos, dándote acceso a múltiples proveedores de LLM a través de una sola API.\n\n#### Configuración\n```python\nfrom memu import MemoryService\n\nservice = MemoryService(\n    llm_profiles={\n        \"default\": {\n            \"provider\": \"openrouter\",\n            \"client_backend\": \"httpx\",\n            \"base_url\": \"https://openrouter.ai\",\n            \"api_key\": \"your_openrouter_api_key\",\n            \"chat_model\": \"anthropic/claude-3.5-sonnet\",  # Cualquier modelo de OpenRouter\n            \"embed_model\": \"openai/text-embedding-3-small\",  # Modelo de embedding\n        },\n    },\n    database_config={\n        \"metadata_store\": {\"provider\": \"inmemory\"},\n    },\n)\n```\n\n#### Variables de Entorno\n\n| Variable | Descripción |\n|----------|-------------|\n| `OPENROUTER_API_KEY` | Tu clave API de OpenRouter de [openrouter.ai/keys](https://openrouter.ai/keys) |\n\n#### Características Soportadas\n\n| Característica | Estado | Notas |\n|----------------|--------|-------|\n| Completaciones de Chat | Soportado | Funciona con cualquier modelo de chat de OpenRouter |\n| Embeddings | Soportado | Usa modelos de embedding de OpenAI via OpenRouter |\n| Visión | Soportado | Usa modelos con capacidad de visión (ej., `openai/gpt-4o`) |\n\n#### Ejecutar Pruebas de OpenRouter\n```bash\nexport OPENROUTER_API_KEY=your_api_key\n\n# Prueba de flujo completo (memorize + retrieve)\npython tests/test_openrouter.py\n\n# Pruebas específicas de embedding\npython tests/test_openrouter_embedding.py\n\n# Pruebas específicas de visión\npython tests/test_openrouter_vision.py\n```\n\nVer [`examples/example_4_openrouter_memory.py`](../examples/example_4_openrouter_memory.py) para un ejemplo completo funcional.\n\n---\n\n## 📖 APIs Principales\n\n### `memorize()` - Pipeline de Aprendizaje Continuo\n\nProcesa entradas en tiempo real y actualiza la memoria inmediatamente:\n\n<img width=\"100%\" alt=\"memorize\" src=\"../assets/memorize.png\" />\n\n```python\nresult = await service.memorize(\n    resource_url=\"path/to/file.json\",  # Ruta de archivo o URL\n    modality=\"conversation\",            # conversation | document | image | video | audio\n    user={\"user_id\": \"123\"}             # Opcional: limitar a un usuario\n)\n\n# Retorna inmediatamente con la memoria extraída:\n{\n    \"resource\": {...},      # Metadatos del recurso almacenado\n    \"items\": [...],         # Elementos de memoria extraídos (disponibles instantáneamente)\n    \"categories\": [...]     # Estructura de categorías auto-actualizada\n}\n```\n\n**Características Proactivas:**\n- Procesamiento sin demora—memorias disponibles inmediatamente\n- Categorización automática sin etiquetado manual\n- Referencia cruzada con memorias existentes para detección de patrones\n\n### `retrieve()` - Inteligencia de Doble Modo\n\nMemU soporta tanto **carga proactiva de contexto** como **consultas reactivas**:\n\n<img width=\"100%\" alt=\"retrieve\" src=\"../assets/retrieve.png\" />\n\n#### Recuperación basada en RAG (`method=\"rag\"`)\n\n**Ensamblaje proactivo de contexto** rápido usando embeddings:\n\n- ✅ **Contexto instantáneo**: Presentación de memoria en sub-segundos\n- ✅ **Monitoreo en segundo plano**: Puede ejecutarse continuamente sin costos de LLM\n- ✅ **Puntuación de similitud**: Identifica automáticamente las memorias más relevantes\n\n#### Recuperación basada en LLM (`method=\"llm\"`)\n\n**Razonamiento anticipatorio** profundo para contextos complejos:\n\n- ✅ **Predicción de intención**: LLM infiere lo que el usuario necesita antes de preguntar\n- ✅ **Evolución de consulta**: Refina automáticamente la búsqueda mientras el contexto se desarrolla\n- ✅ **Terminación temprana**: Se detiene cuando se recopila suficiente contexto\n\n#### Comparación\n\n| Aspecto | RAG (Contexto Rápido) | LLM (Razonamiento Profundo) |\n|---------|----------------------|----------------------------|\n| **Velocidad** | ⚡ Milisegundos | 🐢 Segundos |\n| **Costo** | 💰 Solo embedding | 💰💰 Inferencia LLM |\n| **Uso proactivo** | Monitoreo continuo | Carga de contexto activada |\n| **Mejor para** | Sugerencias en tiempo real | Anticipación compleja |\n\n#### Uso\n```python\n# Recuperación proactiva con historial de contexto\nresult = await service.retrieve(\n    queries=[\n        {\"role\": \"user\", \"content\": {\"text\": \"¿Cuáles son sus preferencias?\"}},\n        {\"role\": \"user\", \"content\": {\"text\": \"Cuéntame sobre los hábitos de trabajo\"}}\n    ],\n    where={\"user_id\": \"123\"},  # Opcional: filtro de alcance\n    method=\"rag\"  # o \"llm\" para razonamiento más profundo\n)\n\n# Retorna resultados conscientes del contexto:\n{\n    \"categories\": [...],     # Áreas temáticas relevantes (auto-priorizadas)\n    \"items\": [...],          # Hechos de memoria específicos\n    \"resources\": [...],      # Fuentes originales para trazabilidad\n    \"next_step_query\": \"...\" # Contexto de seguimiento predicho\n}\n```\n\n**Filtrado Proactivo**: Usa `where` para delimitar el monitoreo continuo:\n- `where={\"user_id\": \"123\"}` - Contexto específico del usuario\n- `where={\"agent_id__in\": [\"1\", \"2\"]}` - Coordinación multi-agente\n- Omitir `where` para conciencia de contexto global\n\n> 📚 **Para documentación completa de API**, ver [SERVICE_API.md](../docs/SERVICE_API.md) - incluye patrones de flujo de trabajo proactivo, configuración de pipeline y manejo de actualizaciones en tiempo real.\n\n---\n\n## 💡 Escenarios Proactivos\n\n### Ejemplo 1: Asistente que Siempre Aprende\n\nAprende continuamente de cada interacción sin comandos explícitos de memoria:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_1_conversation_memory.py\n```\n\n**Comportamiento Proactivo:**\n- Extrae automáticamente preferencias de menciones casuales\n- Construye modelos de relación a partir de patrones de interacción\n- Presenta contexto relevante en conversaciones futuras\n- Adapta el estilo de comunicación basándose en preferencias aprendidas\n\n**Mejor para:** Asistentes personales de IA, soporte al cliente que recuerda, chatbots sociales\n\n---\n\n### Ejemplo 2: Agente Auto-Mejorador\n\nAprende de logs de ejecución y sugiere proactivamente optimizaciones:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_2_skill_extraction.py\n```\n\n**Comportamiento Proactivo:**\n- Monitorea acciones y resultados del agente continuamente\n- Identifica patrones en éxitos y fracasos\n- Auto-genera guías de habilidades a partir de experiencia\n- Sugiere proactivamente estrategias para tareas futuras similares\n\n**Mejor para:** Automatización DevOps, auto-mejora de agentes, captura de conocimiento\n\n---\n\n### Ejemplo 3: Constructor de Contexto Multimodal\n\nUnifica memoria a través de diferentes tipos de entrada para contexto comprehensivo:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_3_multimodal_memory.py\n```\n\n**Comportamiento Proactivo:**\n- Referencia cruzada de texto, imágenes y documentos automáticamente\n- Construye comprensión unificada a través de modalidades\n- Presenta contexto visual cuando se discuten temas relacionados\n- Anticipa necesidades de información combinando múltiples fuentes\n\n**Mejor para:** Sistemas de documentación, plataformas de aprendizaje, asistentes de investigación\n\n---\n\n## 📊 Rendimiento\n\nMemU alcanza **92.09% de precisión promedio** en el benchmark Locomo en todas las tareas de razonamiento, demostrando operaciones confiables de memoria proactiva.\n\n<img width=\"100%\" alt=\"benchmark\" src=\"https://github.com/user-attachments/assets/6fec4884-94e5-4058-ad5c-baac3d7e76d9\" />\n\nVer datos experimentales detallados: [memU-experiment](https://github.com/NevaMind-AI/memU-experiment)\n\n---\n\n## 🧩 Ecosistema\n\n| Repositorio | Descripción | Características Proactivas |\n|-------------|-------------|---------------------------|\n| **[memU](https://github.com/NevaMind-AI/memU)** | Motor principal de memoria proactiva | Pipeline de aprendizaje 7×24, auto-categorización |\n| **[memU-server](https://github.com/NevaMind-AI/memU-server)** | Backend con sincronización continua | Actualizaciones de memoria en tiempo real, triggers de webhook |\n| **[memU-ui](https://github.com/NevaMind-AI/memU-ui)** | Dashboard visual de memoria | Monitoreo de evolución de memoria en vivo |\n\n**Enlaces Rápidos:**\n- 🚀 [Probar MemU Cloud](https://app.memu.so/quick-start)\n- 📚 [Documentación de API](https://memu.pro/docs)\n- 💬 [Comunidad Discord](https://discord.gg/memu)\n\n---\n\n## 🤝 Socios\n\n<div align=\"center\">\n\n<a href=\"https://github.com/TEN-framework/ten-framework\"><img src=\"https://avatars.githubusercontent.com/u/113095513?s=200&v=4\" alt=\"Ten\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://openagents.org\"><img src=\"../assets/partners/openagents.png\" alt=\"OpenAgents\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/milvus-io/milvus\"><img src=\"https://miro.medium.com/v2/resize:fit:2400/1*-VEGyAgcIBD62XtZWavy8w.png\" alt=\"Milvus\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://xroute.ai/\"><img src=\"../assets/partners/xroute.png\" alt=\"xRoute\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://jaaz.app/\"><img src=\"../assets/partners/jazz.png\" alt=\"Jazz\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/Buddie-AI/Buddie\"><img src=\"../assets/partners/buddie.png\" alt=\"Buddie\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/bytebase/bytebase\"><img src=\"../assets/partners/bytebase.png\" alt=\"Bytebase\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/LazyAGI/LazyLLM\"><img src=\"../assets/partners/LazyLLM.png\" alt=\"LazyLLM\" height=\"40\" style=\"margin: 10px;\"></a>\n\n</div>\n\n---\n\n## 🤝 Cómo Contribuir\n\n¡Damos la bienvenida a contribuciones de la comunidad! Ya sea arreglando bugs, agregando características o mejorando documentación, tu ayuda es apreciada.\n\n### Comenzando\n\nPara empezar a contribuir a MemU, necesitarás configurar tu entorno de desarrollo:\n\n#### Prerrequisitos\n- Python 3.13+\n- [uv](https://github.com/astral-sh/uv) (gestor de paquetes Python)\n- Git\n\n#### Configurar Entorno de Desarrollo\n```bash\n# 1. Fork y clonar el repositorio\ngit clone https://github.com/YOUR_USERNAME/memU.git\ncd memU\n\n# 2. Instalar dependencias de desarrollo\nmake install\n```\n\nEl comando `make install` hará:\n- Crear un entorno virtual usando `uv`\n- Instalar todas las dependencias del proyecto\n- Configurar hooks de pre-commit para verificaciones de calidad de código\n\n#### Ejecutar Verificaciones de Calidad\n\nAntes de enviar tu contribución, asegúrate de que tu código pase todas las verificaciones de calidad:\n```bash\nmake check\n```\n\nEl comando `make check` ejecuta:\n- **Verificación de archivo lock**: Asegura consistencia de `pyproject.toml`\n- **Hooks de pre-commit**: Lint de código con Ruff, formateo con Black\n- **Verificación de tipos**: Ejecuta `mypy` para análisis de tipos estáticos\n- **Análisis de dependencias**: Usa `deptry` para encontrar dependencias obsoletas\n\n### Guías de Contribución\n\nPara guías detalladas de contribución, estándares de código y prácticas de desarrollo, ver [CONTRIBUTING.md](../CONTRIBUTING.md).\n\n**Tips rápidos:**\n- Crear una nueva rama para cada característica o corrección de bug\n- Escribir mensajes de commit claros\n- Agregar tests para nueva funcionalidad\n- Actualizar documentación según sea necesario\n- Ejecutar `make check` antes de hacer push\n\n---\n\n## 📄 Licencia\n\n[Apache License 2.0](../LICENSE.txt)\n\n---\n\n## 🌍 Comunidad\n\n- **GitHub Issues**: [Reportar bugs y solicitar características](https://github.com/NevaMind-AI/memU/issues)\n- **Discord**: [Unirse a la comunidad](https://discord.com/invite/hQZntfGsbJ)\n- **X (Twitter)**: [Seguir @memU_ai](https://x.com/memU_ai)\n- **Contacto**: info@nevamind.ai\n\n---\n\n<div align=\"center\">\n\n⭐ **¡Danos una estrella en GitHub** para recibir notificaciones de nuevos lanzamientos!\n\n</div>\n"
  },
  {
    "path": "readme/README_fr.md",
    "content": "![MemU Banner](../assets/banner.png)\n\n<div align=\"center\">\n\n# memU\n\n### Mémoire Proactive Toujours Active pour les Agents IA\n\n[![PyPI version](https://badge.fury.io/py/memu-py.svg)](https://badge.fury.io/py/memu-py)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/memu)\n[![Twitter](https://img.shields.io/badge/Twitter-Follow-1DA1F2?logo=x&logoColor=white)](https://x.com/memU_ai)\n\n<a href=\"https://trendshift.io/repositories/17374\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/17374\" alt=\"NevaMind-AI%2FmemU | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n**[English](README_en.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Français](README_fr.md)**\n\n</div>\n\n---\n\nmemU est un framework de mémoire conçu pour les **agents proactifs 24/7**.\nIl est conçu pour une utilisation prolongée et **réduit considérablement le coût en tokens LLM** pour maintenir les agents toujours en ligne, rendant les agents toujours actifs et évolutifs pratiques dans les systèmes de production.\nmemU **capture et comprend continuellement l'intention de l'utilisateur**. Même sans commande, l'agent peut détecter ce que vous êtes sur le point de faire et agir de lui-même.\n\n---\n\n## 🤖 [OpenClaw (Moltbot, Clawdbot) Alternative](https://memu.bot)\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/memUbot.png\" />\n\n- **Download-and-use and simple** to get started.\n- Builds long-term memory to **understand user intent** and act proactively.\n- **Cuts LLM token cost** with smaller context.\n\nTry now: [memU bot](https://memu.bot)\n\n---\n\n## 🗃️ La Mémoire comme Système de Fichiers, le Système de Fichiers comme Mémoire\n\nmemU traite la **mémoire comme un système de fichiers**—structurée, hiérarchique et instantanément accessible.\n\n| Système de Fichiers | Mémoire memU |\n|--------------------|--------------|\n| 📁 Dossiers | 🏷️ Catégories (sujets auto-organisés) |\n| 📄 Fichiers | 🧠 Éléments de Mémoire (faits, préférences, compétences extraites) |\n| 🔗 Liens symboliques | 🔄 Références croisées (mémoires liées connectées) |\n| 📂 Points de montage | 📥 Ressources (conversations, documents, images) |\n\n**Pourquoi c'est important :**\n- **Naviguez dans les mémoires** comme dans des répertoires—explorez des catégories larges jusqu'aux faits spécifiques\n- **Montez de nouvelles connaissances** instantanément—les conversations et documents deviennent de la mémoire interrogeable\n- **Liez tout de manière croisée**—les mémoires se référencent mutuellement, construisant un graphe de connaissances connecté\n- **Persistant et portable**—exportez, sauvegardez et transférez la mémoire comme des fichiers\n\n```\nmemory/\n├── preferences/\n│   ├── communication_style.md\n│   └── topic_interests.md\n├── relationships/\n│   ├── contacts/\n│   └── interaction_history/\n├── knowledge/\n│   ├── domain_expertise/\n│   └── learned_skills/\n└── context/\n    ├── recent_conversations/\n    └── pending_tasks/\n```\n\nTout comme un système de fichiers transforme des octets bruts en données organisées, memU transforme les interactions brutes en **intelligence structurée, recherchable et proactive**.\n\n---\n\n## ⭐️ Mettez une étoile au dépôt\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/star.gif\" />\nSi vous trouvez memU utile ou intéressant, une étoile GitHub ⭐️ serait grandement appréciée.\n\n---\n\n\n## ✨ Fonctionnalités Principales\n\n| Capacité | Description |\n|----------|-------------|\n| 🤖 **Agent Proactif 24/7** | Agent de mémoire toujours actif qui travaille continuellement en arrière-plan—ne dort jamais, n'oublie jamais |\n| 🎯 **Capture d'Intention Utilisateur** | Comprend et mémorise automatiquement les objectifs, préférences et contexte de l'utilisateur à travers les sessions |\n| 💰 **Économique** | Réduit les coûts de tokens à long terme en cachant les insights et en évitant les appels LLM redondants |\n---\n\n## 🔄 Comment Fonctionne la Mémoire Proactive\n\n```bash\n\ncd examples/proactive\npython proactive.py\n\n```\n\n---\n\n### Proactive Memory Lifecycle\n```\n┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\n│                                         USER QUERY                                               │\n└──────────────────────────────────────────────────────────────────────────────────────────────────┘\n                 │                                                           │\n                 ▼                                                           ▼\n┌────────────────────────────────────────┐         ┌────────────────────────────────────────────────┐\n│         🤖 MAIN AGENT                  │         │              🧠 MEMU BOT                       │\n│                                        │         │                                                │\n│  Handle user queries & execute tasks   │  ◄───►  │  Monitor, memorize & proactive intelligence   │\n├────────────────────────────────────────┤         ├────────────────────────────────────────────────┤\n│                                        │         │                                                │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  1. RECEIVE USER INPUT           │  │         │  │  1. MONITOR INPUT/OUTPUT                 │  │\n│  │     Parse query, understand      │  │   ───►  │  │     Observe agent interactions           │  │\n│  │     context and intent           │  │         │  │     Track conversation flow              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  2. PLAN & EXECUTE               │  │         │  │  2. MEMORIZE & EXTRACT                   │  │\n│  │     Break down tasks             │  │   ◄───  │  │     Store insights, facts, preferences   │  │\n│  │     Call tools, retrieve data    │  │  inject │  │     Extract skills & knowledge           │  │\n│  │     Generate responses           │  │  memory │  │     Update user profile                  │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  3. RESPOND TO USER              │  │         │  │  3. PREDICT USER INTENT                  │  │\n│  │     Deliver answer/result        │  │   ───►  │  │     Anticipate next steps                │  │\n│  │     Continue conversation        │  │         │  │     Identify upcoming needs              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  4. LOOP                         │  │         │  │  4. RUN PROACTIVE TASKS                  │  │\n│  │     Wait for next user input     │  │   ◄───  │  │     Pre-fetch relevant context           │  │\n│  │     or proactive suggestions     │  │  suggest│  │     Prepare recommendations              │  │\n│  └──────────────────────────────────┘  │         │  │     Update todolist autonomously         │  │\n│                                        │         │  └──────────────────────────────────────────┘  │\n└────────────────────────────────────────┘         └────────────────────────────────────────────────┘\n                 │                                                           │\n                 └───────────────────────────┬───────────────────────────────┘\n                                             ▼\n                              ┌──────────────────────────────┐\n                              │     CONTINUOUS SYNC LOOP     │\n                              │  Agent ◄──► MemU Bot ◄──► DB │\n                              └──────────────────────────────┘\n```\n\n---\n\n## 🎯 Cas d'Usage Proactifs\n\n### 1. **Recommandation d'Information**\n*L'agent surveille les intérêts et affiche proactivement du contenu pertinent*\n```python\n# L'utilisateur recherche des sujets sur l'IA\nMemU suit: historique de lecture, articles sauvegardés, requêtes de recherche\n\n# Quand du nouveau contenu arrive:\nAgent: \"J'ai trouvé 3 nouveaux articles sur l'optimisation RAG qui\n        correspondent à vos recherches récentes sur les systèmes de\n        récupération. Un auteur (Dr. Chen) que vous avez cité a publié hier.\"\n\n# Comportements proactifs:\n- Apprend les préférences de sujets des patterns de navigation\n- Suit les préférences de crédibilité auteur/source\n- Filtre le bruit selon l'historique d'engagement\n- Planifie les recommandations pour attention optimale\n```\n\n### 2. **Gestion d'Email**\n*L'agent apprend les patterns de communication et gère la correspondance routinière*\n```python\n# MemU observe les patterns email au fil du temps:\n- Templates de réponse pour scénarios courants\n- Contacts prioritaires et mots-clés urgents\n- Préférences de planning et disponibilité\n- Variations de style d'écriture et de ton\n\n# Assistance email proactive:\nAgent: \"Vous avez 12 nouveaux emails. J'ai rédigé des réponses pour 3\n        demandes routinières et marqué 2 éléments urgents de vos contacts\n        prioritaires. Dois-je aussi reprogrammer la réunion de demain\n        selon le conflit mentionné par John?\"\n\n# Actions autonomes:\n✓ Rédiger des réponses contextuelles\n✓ Catégoriser et prioriser la boîte de réception\n✓ Détecter les conflits de planning\n✓ Résumer les longs fils avec décisions clés\n```\n\n### 3. **Trading & Surveillance Financière**\n*L'agent suit le contexte marché et le comportement d'investissement utilisateur*\n```python\n# MemU apprend les préférences de trading:\n- Tolérance au risque des décisions historiques\n- Secteurs et classes d'actifs préférés\n- Patterns de réponse aux événements marché\n- Déclencheurs de rééquilibrage de portefeuille\n\n# Alertes proactives:\nAgent: \"NVDA a chuté de 5% en after-hours. Selon votre comportement passé,\n        vous achetez typiquement les baisses tech supérieures à 3%. Votre\n        allocation actuelle permet $2,000 d'exposition supplémentaire tout\n        en maintenant votre cible 70/30 actions-obligations.\"\n\n# Surveillance continue:\n- Suivre les alertes prix liées aux seuils définis\n- Corréler événements d'actualité et impact portefeuille\n- Apprendre des recommandations exécutées vs. ignorées\n- Anticiper les opportunités de récolte de pertes fiscales\n```\n\n\n...\n\n---\n\n## 🗂️ Architecture de Mémoire Hiérarchique\n\nLe système à trois couches de MemU permet à la fois **les requêtes réactives** et **le chargement proactif de contexte** :\n\n<img width=\"100%\" alt=\"structure\" src=\"../assets/structure.png\" />\n\n| Couche | Usage Réactif | Usage Proactif |\n|--------|---------------|----------------|\n| **Ressource** | Accès direct aux données originales | Surveillance en arrière-plan des nouveaux patterns |\n| **Élément** | Récupération de faits ciblée | Extraction en temps réel des interactions en cours |\n| **Catégorie** | Vue d'ensemble au niveau résumé | Assemblage automatique de contexte pour anticipation |\n\n**Avantages Proactifs:**\n- **Auto-catégorisation**: Les nouvelles mémoires s'auto-organisent en sujets\n- **Détection de Patterns**: Le système identifie les thèmes récurrents\n- **Prédiction de Contexte**: Anticipe quelle information sera nécessaire ensuite\n\n---\n\n## 🚀 Démarrage Rapide\n\n### Option 1: Version Cloud\n\nExpérimentez la mémoire proactive instantanément:\n\n👉 **[memu.so](https://memu.so)** - Service hébergé avec apprentissage continu 7×24\n\nPour un déploiement entreprise avec des workflows proactifs personnalisés, contactez **info@nevamind.ai**\n\n#### API Cloud (v3)\n\n| URL de Base | `https://api.memu.so` |\n|-------------|----------------------|\n| Auth | `Authorization: Bearer YOUR_API_KEY` |\n\n| Méthode | Endpoint | Description |\n|---------|----------|-------------|\n| `POST` | `/api/v3/memory/memorize` | Enregistrer une tâche d'apprentissage continu |\n| `GET` | `/api/v3/memory/memorize/status/{task_id}` | Vérifier le statut de traitement en temps réel |\n| `POST` | `/api/v3/memory/categories` | Lister les catégories auto-générées |\n| `POST` | `/api/v3/memory/retrieve` | Interroger la mémoire (supporte le chargement proactif de contexte) |\n\n📚 **[Documentation Complète de l'API](https://memu.pro/docs#cloud-version)**\n\n---\n\n### Option 2: Auto-Hébergé\n\n#### Installation\n```bash\npip install -e .\n```\n\n#### Exemple de Base\n\n> **Prérequis**: Python 3.13+ et une clé API OpenAI\n\n**Tester l'Apprentissage Continu** (en mémoire):\n```bash\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_inmemory.py\n```\n\n**Tester avec Stockage Persistant** (PostgreSQL):\n```bash\n# Démarrer PostgreSQL avec pgvector\ndocker run -d \\\n  --name memu-postgres \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=memu \\\n  -p 5432:5432 \\\n  pgvector/pgvector:pg16\n\n# Exécuter le test d'apprentissage continu\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_postgres.py\n```\n\nLes deux exemples démontrent **les workflows de mémoire proactive**:\n1. **Ingestion Continue**: Traiter plusieurs fichiers séquentiellement\n2. **Auto-Extraction**: Création immédiate de mémoire\n3. **Récupération Proactive**: Affichage de mémoire contextuel\n\nVoir [`tests/test_inmemory.py`](../tests/test_inmemory.py) et [`tests/test_postgres.py`](../tests/test_postgres.py) pour les détails d'implémentation.\n\n---\n\n### Fournisseurs LLM et Embeddings Personnalisés\n\nMemU supporte des fournisseurs LLM et embeddings personnalisés au-delà d'OpenAI. Configurez-les via `llm_profiles`:\n```python\nfrom memu import MemUService\n\nservice = MemUService(\n    llm_profiles={\n        # Profil par défaut pour les opérations LLM\n        \"default\": {\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n            \"api_key\": \"your_api_key\",\n            \"chat_model\": \"qwen3-max\",\n            \"client_backend\": \"sdk\"  # \"sdk\" ou \"http\"\n        },\n        # Profil séparé pour les embeddings\n        \"embedding\": {\n            \"base_url\": \"https://api.voyageai.com/v1\",\n            \"api_key\": \"your_voyage_api_key\",\n            \"embed_model\": \"voyage-3.5-lite\"\n        }\n    },\n    # ... autre configuration\n)\n```\n\n---\n\n### Intégration OpenRouter\n\nMemU supporte [OpenRouter](https://openrouter.ai) comme fournisseur de modèles, vous donnant accès à plusieurs fournisseurs LLM via une seule API.\n\n#### Configuration\n```python\nfrom memu import MemoryService\n\nservice = MemoryService(\n    llm_profiles={\n        \"default\": {\n            \"provider\": \"openrouter\",\n            \"client_backend\": \"httpx\",\n            \"base_url\": \"https://openrouter.ai\",\n            \"api_key\": \"your_openrouter_api_key\",\n            \"chat_model\": \"anthropic/claude-3.5-sonnet\",  # N'importe quel modèle OpenRouter\n            \"embed_model\": \"openai/text-embedding-3-small\",  # Modèle d'embedding\n        },\n    },\n    database_config={\n        \"metadata_store\": {\"provider\": \"inmemory\"},\n    },\n)\n```\n\n#### Variables d'Environnement\n\n| Variable | Description |\n|----------|-------------|\n| `OPENROUTER_API_KEY` | Votre clé API OpenRouter de [openrouter.ai/keys](https://openrouter.ai/keys) |\n\n#### Fonctionnalités Supportées\n\n| Fonctionnalité | Statut | Notes |\n|----------------|--------|-------|\n| Complétion de Chat | Supporté | Fonctionne avec n'importe quel modèle de chat OpenRouter |\n| Embeddings | Supporté | Utilisez les modèles d'embedding OpenAI via OpenRouter |\n| Vision | Supporté | Utilisez des modèles avec capacité vision (ex., `openai/gpt-4o`) |\n\n#### Exécuter les Tests OpenRouter\n```bash\nexport OPENROUTER_API_KEY=your_api_key\n\n# Test de workflow complet (memorize + retrieve)\npython tests/test_openrouter.py\n\n# Tests spécifiques aux embeddings\npython tests/test_openrouter_embedding.py\n\n# Tests spécifiques à la vision\npython tests/test_openrouter_vision.py\n```\n\nVoir [`examples/example_4_openrouter_memory.py`](../examples/example_4_openrouter_memory.py) pour un exemple complet fonctionnel.\n\n---\n\n## 📖 APIs Principales\n\n### `memorize()` - Pipeline d'Apprentissage Continu\n\nTraite les entrées en temps réel et met à jour la mémoire immédiatement:\n\n<img width=\"100%\" alt=\"memorize\" src=\"../assets/memorize.png\" />\n\n```python\nresult = await service.memorize(\n    resource_url=\"path/to/file.json\",  # Chemin de fichier ou URL\n    modality=\"conversation\",            # conversation | document | image | video | audio\n    user={\"user_id\": \"123\"}             # Optionnel: limiter à un utilisateur\n)\n\n# Retourne immédiatement avec la mémoire extraite:\n{\n    \"resource\": {...},      # Métadonnées de ressource stockées\n    \"items\": [...],         # Éléments de mémoire extraits (disponibles instantanément)\n    \"categories\": [...]     # Structure de catégories auto-mise à jour\n}\n```\n\n**Fonctionnalités Proactives:**\n- Traitement sans délai—mémoires disponibles immédiatement\n- Catégorisation automatique sans étiquetage manuel\n- Référence croisée avec les mémoires existantes pour détection de patterns\n\n### `retrieve()` - Intelligence Double Mode\n\nMemU supporte à la fois **le chargement proactif de contexte** et **les requêtes réactives**:\n\n<img width=\"100%\" alt=\"retrieve\" src=\"../assets/retrieve.png\" />\n\n#### Récupération basée sur RAG (`method=\"rag\"`)\n\n**Assemblage proactif de contexte** rapide utilisant les embeddings:\n\n- ✅ **Contexte instantané**: Affichage de mémoire en sous-seconde\n- ✅ **Surveillance en arrière-plan**: Peut s'exécuter continuellement sans coûts LLM\n- ✅ **Score de similarité**: Identifie automatiquement les mémoires les plus pertinentes\n\n#### Récupération basée sur LLM (`method=\"llm\"`)\n\n**Raisonnement anticipatoire** profond pour contextes complexes:\n\n- ✅ **Prédiction d'intention**: LLM infère ce dont l'utilisateur a besoin avant de demander\n- ✅ **Évolution de requête**: Affine automatiquement la recherche au fur et à mesure que le contexte se développe\n- ✅ **Terminaison précoce**: S'arrête quand suffisamment de contexte est collecté\n\n#### Comparaison\n\n| Aspect | RAG (Contexte Rapide) | LLM (Raisonnement Profond) |\n|--------|----------------------|---------------------------|\n| **Vitesse** | ⚡ Millisecondes | 🐢 Secondes |\n| **Coût** | 💰 Embedding seulement | 💰💰 Inférence LLM |\n| **Usage proactif** | Surveillance continue | Chargement de contexte déclenché |\n| **Meilleur pour** | Suggestions temps réel | Anticipation complexe |\n\n#### Utilisation\n```python\n# Récupération proactive avec historique de contexte\nresult = await service.retrieve(\n    queries=[\n        {\"role\": \"user\", \"content\": {\"text\": \"Quelles sont leurs préférences?\"}},\n        {\"role\": \"user\", \"content\": {\"text\": \"Parle-moi des habitudes de travail\"}}\n    ],\n    where={\"user_id\": \"123\"},  # Optionnel: filtre de portée\n    method=\"rag\"  # ou \"llm\" pour raisonnement plus profond\n)\n\n# Retourne des résultats contextuels:\n{\n    \"categories\": [...],     # Domaines thématiques pertinents (auto-priorisés)\n    \"items\": [...],          # Faits de mémoire spécifiques\n    \"resources\": [...],      # Sources originales pour traçabilité\n    \"next_step_query\": \"...\" # Contexte de suivi prédit\n}\n```\n\n**Filtrage Proactif**: Utilisez `where` pour délimiter la surveillance continue:\n- `where={\"user_id\": \"123\"}` - Contexte spécifique à l'utilisateur\n- `where={\"agent_id__in\": [\"1\", \"2\"]}` - Coordination multi-agent\n- Omettre `where` pour conscience de contexte globale\n\n> 📚 **Pour la documentation API complète**, voir [SERVICE_API.md](../docs/SERVICE_API.md) - inclut les patterns de workflow proactif, configuration de pipeline et gestion des mises à jour en temps réel.\n\n---\n\n## 💡 Scénarios Proactifs\n\n### Exemple 1: Assistant Toujours Apprenant\n\nApprend continuellement de chaque interaction sans commandes de mémoire explicites:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_1_conversation_memory.py\n```\n\n**Comportement Proactif:**\n- Extrait automatiquement les préférences des mentions occasionnelles\n- Construit des modèles de relation à partir des patterns d'interaction\n- Affiche le contexte pertinent dans les conversations futures\n- Adapte le style de communication basé sur les préférences apprises\n\n**Meilleur pour:** Assistants IA personnels, support client qui se souvient, chatbots sociaux\n\n---\n\n### Exemple 2: Agent Auto-Améliorant\n\nApprend des logs d'exécution et suggère proactivement des optimisations:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_2_skill_extraction.py\n```\n\n**Comportement Proactif:**\n- Surveille les actions et résultats de l'agent continuellement\n- Identifie les patterns dans les succès et échecs\n- Auto-génère des guides de compétences à partir de l'expérience\n- Suggère proactivement des stratégies pour des tâches futures similaires\n\n**Meilleur pour:** Automatisation DevOps, auto-amélioration d'agent, capture de connaissances\n\n---\n\n### Exemple 3: Constructeur de Contexte Multimodal\n\nUnifie la mémoire à travers différents types d'entrée pour un contexte complet:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_3_multimodal_memory.py\n```\n\n**Comportement Proactif:**\n- Référence croisée de texte, images et documents automatiquement\n- Construit une compréhension unifiée à travers les modalités\n- Affiche le contexte visuel lors de la discussion de sujets associés\n- Anticipe les besoins d'information en combinant plusieurs sources\n\n**Meilleur pour:** Systèmes de documentation, plateformes d'apprentissage, assistants de recherche\n\n---\n\n## 📊 Performance\n\nMemU atteint **92.09% de précision moyenne** sur le benchmark Locomo à travers toutes les tâches de raisonnement, démontrant des opérations de mémoire proactive fiables.\n\n<img width=\"100%\" alt=\"benchmark\" src=\"https://github.com/user-attachments/assets/6fec4884-94e5-4058-ad5c-baac3d7e76d9\" />\n\nVoir les données expérimentales détaillées: [memU-experiment](https://github.com/NevaMind-AI/memU-experiment)\n\n---\n\n## 🧩 Écosystème\n\n| Dépôt | Description | Fonctionnalités Proactives |\n|-------|-------------|---------------------------|\n| **[memU](https://github.com/NevaMind-AI/memU)** | Moteur principal de mémoire proactive | Pipeline d'apprentissage 7×24, auto-catégorisation |\n| **[memU-server](https://github.com/NevaMind-AI/memU-server)** | Backend avec synchronisation continue | Mises à jour de mémoire en temps réel, déclencheurs webhook |\n| **[memU-ui](https://github.com/NevaMind-AI/memU-ui)** | Dashboard visuel de mémoire | Surveillance de l'évolution de la mémoire en direct |\n\n**Liens Rapides:**\n- 🚀 [Essayer MemU Cloud](https://app.memu.so/quick-start)\n- 📚 [Documentation API](https://memu.pro/docs)\n- 💬 [Communauté Discord](https://discord.gg/memu)\n\n---\n\n## 🤝 Partenaires\n\n<div align=\"center\">\n\n<a href=\"https://github.com/TEN-framework/ten-framework\"><img src=\"https://avatars.githubusercontent.com/u/113095513?s=200&v=4\" alt=\"Ten\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://openagents.org\"><img src=\"../assets/partners/openagents.png\" alt=\"OpenAgents\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/milvus-io/milvus\"><img src=\"https://miro.medium.com/v2/resize:fit:2400/1*-VEGyAgcIBD62XtZWavy8w.png\" alt=\"Milvus\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://xroute.ai/\"><img src=\"../assets/partners/xroute.png\" alt=\"xRoute\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://jaaz.app/\"><img src=\"../assets/partners/jazz.png\" alt=\"Jazz\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/Buddie-AI/Buddie\"><img src=\"../assets/partners/buddie.png\" alt=\"Buddie\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/bytebase/bytebase\"><img src=\"../assets/partners/bytebase.png\" alt=\"Bytebase\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/LazyAGI/LazyLLM\"><img src=\"../assets/partners/LazyLLM.png\" alt=\"LazyLLM\" height=\"40\" style=\"margin: 10px;\"></a>\n\n</div>\n\n---\n\n## 🤝 Comment Contribuer\n\nNous accueillons les contributions de la communauté! Que vous corrigiez des bugs, ajoutiez des fonctionnalités ou amélioriez la documentation, votre aide est appréciée.\n\n### Pour Commencer\n\nPour commencer à contribuer à MemU, vous devrez configurer votre environnement de développement:\n\n#### Prérequis\n- Python 3.13+\n- [uv](https://github.com/astral-sh/uv) (gestionnaire de paquets Python)\n- Git\n\n#### Configurer l'Environnement de Développement\n```bash\n# 1. Fork et cloner le dépôt\ngit clone https://github.com/YOUR_USERNAME/memU.git\ncd memU\n\n# 2. Installer les dépendances de développement\nmake install\n```\n\nLa commande `make install` va:\n- Créer un environnement virtuel en utilisant `uv`\n- Installer toutes les dépendances du projet\n- Configurer les hooks pre-commit pour les vérifications de qualité de code\n\n#### Exécuter les Vérifications de Qualité\n\nAvant de soumettre votre contribution, assurez-vous que votre code passe toutes les vérifications de qualité:\n```bash\nmake check\n```\n\nLa commande `make check` exécute:\n- **Vérification du fichier lock**: Assure la cohérence de `pyproject.toml`\n- **Hooks pre-commit**: Lint le code avec Ruff, formate avec Black\n- **Vérification de types**: Exécute `mypy` pour l'analyse de types statiques\n- **Analyse de dépendances**: Utilise `deptry` pour trouver les dépendances obsolètes\n\n### Directives de Contribution\n\nPour des directives de contribution détaillées, standards de code et pratiques de développement, voir [CONTRIBUTING.md](../CONTRIBUTING.md).\n\n**Conseils rapides:**\n- Créer une nouvelle branche pour chaque fonctionnalité ou correction de bug\n- Écrire des messages de commit clairs\n- Ajouter des tests pour les nouvelles fonctionnalités\n- Mettre à jour la documentation si nécessaire\n- Exécuter `make check` avant de pousser\n\n---\n\n## 📄 Licence\n\n[Apache License 2.0](../LICENSE.txt)\n\n---\n\n## 🌍 Communauté\n\n- **GitHub Issues**: [Signaler des bugs & demander des fonctionnalités](https://github.com/NevaMind-AI/memU/issues)\n- **Discord**: [Rejoindre la communauté](https://discord.com/invite/hQZntfGsbJ)\n- **X (Twitter)**: [Suivre @memU_ai](https://x.com/memU_ai)\n- **Contact**: info@nevamind.ai\n\n---\n\n<div align=\"center\">\n\n⭐ **Mettez-nous une étoile sur GitHub** pour être notifié des nouvelles versions!\n\n</div>\n"
  },
  {
    "path": "readme/README_ja.md",
    "content": "![MemU Banner](../assets/banner.png)\n\n<div align=\"center\">\n\n# memU\n\n### AIエージェントのための常時稼働プロアクティブメモリ\n\n[![PyPI version](https://badge.fury.io/py/memu-py.svg)](https://badge.fury.io/py/memu-py)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/memu)\n[![Twitter](https://img.shields.io/badge/Twitter-Follow-1DA1F2?logo=x&logoColor=white)](https://x.com/memU_ai)\n\n<a href=\"https://trendshift.io/repositories/17374\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/17374\" alt=\"NevaMind-AI%2FmemU | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n**[English](README_en.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Français](README_fr.md)**\n\n</div>\n\n---\n\nmemUは**24/7プロアクティブエージェント**向けに構築されたメモリフレームワークです。\n長時間稼働向けに設計されており、エージェントを常時オンラインに保つための**LLMトークンコストを大幅に削減**し、本番システムで常時稼働・進化し続けるエージェントを実用的にします。\nmemUは**ユーザーの意図を継続的にキャプチャして理解**します。コマンドがなくても、エージェントはあなたが何をしようとしているかを判断し、自ら行動します。\n\n---\n\n## 🤖 [OpenClaw (Moltbot, Clawdbot) Alternative](https://memu.bot)\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/memUbot.png\" />\n\n- **Download-and-use and simple** to get started.\n- Builds long-term memory to **understand user intent** and act proactively.\n- **Cuts LLM token cost** with smaller context.\n\nTry now: [memU bot](https://memu.bot)\n\n---\n\n## 🗃️ メモリはファイルシステム、ファイルシステムはメモリ\n\nmemUは**メモリをファイルシステムのように**扱います—構造化され、階層的で、即座にアクセス可能。\n\n| ファイルシステム | memU メモリ |\n|----------------|------------|\n| 📁 フォルダ | 🏷️ カテゴリ（自動整理されたトピック） |\n| 📄 ファイル | 🧠 メモリアイテム（抽出された事実、好み、スキル） |\n| 🔗 シンボリックリンク | 🔄 クロスリファレンス（関連メモリのリンク） |\n| 📂 マウントポイント | 📥 リソース（会話、ドキュメント、画像） |\n\n**なぜ重要か：**\n- **ディレクトリを閲覧するようにメモリをナビゲート**—広いカテゴリから具体的な事実にドリルダウン\n- **新しい知識を即座にマウント**—会話やドキュメントがクエリ可能なメモリに\n- **すべてをクロスリンク**—メモリが相互参照し、接続されたナレッジグラフを構築\n- **永続的でポータブル**—ファイルのようにメモリをエクスポート、バックアップ、転送\n\n```\nmemory/\n├── preferences/\n│   ├── communication_style.md\n│   └── topic_interests.md\n├── relationships/\n│   ├── contacts/\n│   └── interaction_history/\n├── knowledge/\n│   ├── domain_expertise/\n│   └── learned_skills/\n└── context/\n    ├── recent_conversations/\n    └── pending_tasks/\n```\n\nファイルシステムが生のバイトを整理されたデータに変換するように、memUは生のインタラクションを**構造化された、検索可能な、プロアクティブなインテリジェンス**に変換します。\n\n---\n\n## ⭐️ リポジトリにスターを\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/star.gif\" />\nmemUが役立つまたは興味深いと思われた場合は、GitHub Star ⭐️をいただけると大変嬉しいです。\n\n---\n\n\n## ✨ コア機能\n\n| 機能 | 説明 |\n|------|------|\n| 🤖 **24/7プロアクティブエージェント** | バックグラウンドで継続的に動作する常時稼働メモリエージェント—眠らない、忘れない |\n| 🎯 **ユーザー意図キャプチャ** | セッション間でユーザーの目標、好み、コンテキストを自動的に理解して記憶 |\n| 💰 **コスト効率** | インサイトをキャッシュし、冗長なLLM呼び出しを避けることで長時間稼働のトークンコストを削減 |\n---\n\n## 🔄 プロアクティブメモリの仕組み\n\n```bash\n\ncd examples/proactive\npython proactive.py\n\n```\n\n---\n\n### Proactive Memory Lifecycle\n```\n┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\n│                                         USER QUERY                                               │\n└──────────────────────────────────────────────────────────────────────────────────────────────────┘\n                 │                                                           │\n                 ▼                                                           ▼\n┌────────────────────────────────────────┐         ┌────────────────────────────────────────────────┐\n│         🤖 MAIN AGENT                  │         │              🧠 MEMU BOT                       │\n│                                        │         │                                                │\n│  Handle user queries & execute tasks   │  ◄───►  │  Monitor, memorize & proactive intelligence   │\n├────────────────────────────────────────┤         ├────────────────────────────────────────────────┤\n│                                        │         │                                                │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  1. RECEIVE USER INPUT           │  │         │  │  1. MONITOR INPUT/OUTPUT                 │  │\n│  │     Parse query, understand      │  │   ───►  │  │     Observe agent interactions           │  │\n│  │     context and intent           │  │         │  │     Track conversation flow              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  2. PLAN & EXECUTE               │  │         │  │  2. MEMORIZE & EXTRACT                   │  │\n│  │     Break down tasks             │  │   ◄───  │  │     Store insights, facts, preferences   │  │\n│  │     Call tools, retrieve data    │  │  inject │  │     Extract skills & knowledge           │  │\n│  │     Generate responses           │  │  memory │  │     Update user profile                  │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  3. RESPOND TO USER              │  │         │  │  3. PREDICT USER INTENT                  │  │\n│  │     Deliver answer/result        │  │   ───►  │  │     Anticipate next steps                │  │\n│  │     Continue conversation        │  │         │  │     Identify upcoming needs              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  4. LOOP                         │  │         │  │  4. RUN PROACTIVE TASKS                  │  │\n│  │     Wait for next user input     │  │   ◄───  │  │     Pre-fetch relevant context           │  │\n│  │     or proactive suggestions     │  │  suggest│  │     Prepare recommendations              │  │\n│  └──────────────────────────────────┘  │         │  │     Update todolist autonomously         │  │\n│                                        │         │  └──────────────────────────────────────────┘  │\n└────────────────────────────────────────┘         └────────────────────────────────────────────────┘\n                 │                                                           │\n                 └───────────────────────────┬───────────────────────────────┘\n                                             ▼\n                              ┌──────────────────────────────┐\n                              │     CONTINUOUS SYNC LOOP     │\n                              │  Agent ◄──► MemU Bot ◄──► DB │\n                              └──────────────────────────────┘\n```\n\n---\n\n## 🎯 プロアクティブなユースケース\n\n### 1. **情報レコメンデーション**\n*エージェントが興味を監視し、関連コンテンツをプロアクティブに表示*\n```python\n# ユーザーがAIトピックを研究している\nMemUが追跡: 閲覧履歴、保存した記事、検索クエリ\n\n# 新しいコンテンツが到着したとき:\nエージェント: \"検索システムに関する最近の研究と一致する\n              RAG最適化に関する3つの新しい論文を見つけました。\n              以前引用した著者（チェン博士）が昨日発表しました。\"\n\n# プロアクティブな行動:\n- 閲覧パターンからトピックの好みを学習\n- 著者/ソースの信頼性の好みを追跡\n- エンゲージメント履歴に基づいてノイズをフィルタリング\n- 最適な注意を引くタイミングでレコメンド\n```\n\n### 2. **メール管理**\n*エージェントがコミュニケーションパターンを学習し、日常的な通信を処理*\n```python\n# MemUが時間をかけてメールパターンを観察:\n- 一般的なシナリオ用の応答テンプレート\n- 優先連絡先と緊急キーワード\n- スケジュールの好みと空き状況\n- 文体とトーンのバリエーション\n\n# プロアクティブなメールアシスタンス:\nエージェント: \"12件の新着メールがあります。3件の定型リクエストへの\n              返信を下書きし、優先連絡先からの2件の緊急事項に\n              フラグを付けました。ジョンが言及した競合に基づいて\n              明日のミーティングを再スケジュールしますか？\"\n\n# 自律的なアクション:\n✓ コンテキストを考慮した返信を下書き\n✓ 受信トレイを分類して優先順位付け\n✓ スケジュールの競合を検出\n✓ 長いスレッドの主要な決定を要約\n```\n\n### 3. **トレーディング＆財務モニタリング**\n*エージェントが市場コンテキストとユーザーの投資行動を追跡*\n```python\n# MemUがトレーディングの好みを学習:\n- 過去の決定からのリスク許容度\n- 好みのセクターと資産クラス\n- 市場イベントへの反応パターン\n- ポートフォリオリバランスのトリガー\n\n# プロアクティブなアラート:\nエージェント: \"NVDAが時間外取引で5%下落しました。過去の行動から、\n              通常3%以上のテック株の下落時に購入しています。\n              現在の配分では、70/30の株式-債券目標を維持しながら\n              $2,000の追加エクスポージャーが可能です。\"\n\n# 継続的な監視:\n- ユーザー定義のしきい値に関連する価格アラートを追跡\n- ニュースイベントとポートフォリオへの影響を相関\n- 実行された推奨と無視された推奨から学習\n- タックスロスハーベスティングの機会を予測\n```\n\n\n...\n\n---\n\n## 🗂️ 階層メモリアーキテクチャ\n\nMemUの3層システムは、**リアクティブクエリ**と**プロアクティブコンテキストロード**の両方を可能にします：\n\n<img width=\"100%\" alt=\"structure\" src=\"../assets/structure.png\" />\n\n| レイヤー | リアクティブ使用 | プロアクティブ使用 |\n|---------|-----------------|-------------------|\n| **リソース** | 元データへの直接アクセス | 新パターンのバックグラウンド監視 |\n| **アイテム** | ターゲットを絞った事実検索 | 進行中のインタラクションからのリアルタイム抽出 |\n| **カテゴリ** | サマリーレベルの概要 | 予測のための自動コンテキスト組み立て |\n\n**プロアクティブな利点：**\n- **自動分類**：新しいメモリがトピックに自己組織化\n- **パターン検出**：システムが繰り返し現れるテーマを特定\n- **コンテキスト予測**：次に必要な情報を予測\n\n---\n\n## 🚀 クイックスタート\n\n### オプション1：クラウドバージョン\n\nプロアクティブメモリを即座に体験：\n\n👉 **[memu.so](https://memu.so)** - 7×24継続学習を備えたホストサービス\n\nカスタムプロアクティブワークフローを含むエンタープライズデプロイメントについては、**info@nevamind.ai** にお問い合わせください\n\n#### クラウドAPI (v3)\n\n| ベースURL | `https://api.memu.so` |\n|-----------|----------------------|\n| 認証 | `Authorization: Bearer YOUR_API_KEY` |\n\n| メソッド | エンドポイント | 説明 |\n|---------|--------------|------|\n| `POST` | `/api/v3/memory/memorize` | 継続学習タスクを登録 |\n| `GET` | `/api/v3/memory/memorize/status/{task_id}` | リアルタイム処理ステータスを確認 |\n| `POST` | `/api/v3/memory/categories` | 自動生成されたカテゴリを一覧表示 |\n| `POST` | `/api/v3/memory/retrieve` | メモリをクエリ（プロアクティブコンテキストロードをサポート） |\n\n📚 **[完全なAPIドキュメント](https://memu.pro/docs#cloud-version)**\n\n---\n\n### オプション2：セルフホスト\n\n#### インストール\n```bash\npip install -e .\n```\n\n#### 基本例\n\n> **要件**：Python 3.13+ と OpenAI APIキー\n\n**継続学習をテスト**（インメモリ）：\n```bash\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_inmemory.py\n```\n\n**永続ストレージでテスト**（PostgreSQL）：\n```bash\n# pgvectorを含むPostgreSQLを起動\ndocker run -d \\\n  --name memu-postgres \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=memu \\\n  -p 5432:5432 \\\n  pgvector/pgvector:pg16\n\n# 継続学習テストを実行\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_postgres.py\n```\n\n両方の例は**プロアクティブメモリワークフロー**を示しています：\n1. **継続的な取り込み**：複数のファイルを順次処理\n2. **自動抽出**：即座のメモリ作成\n3. **プロアクティブ検索**：コンテキストに応じたメモリ表示\n\n実装の詳細については [`tests/test_inmemory.py`](../tests/test_inmemory.py) と [`tests/test_postgres.py`](../tests/test_postgres.py) を参照してください。\n\n---\n\n### カスタムLLMと埋め込みプロバイダー\n\nMemUはOpenAI以外のカスタムLLMと埋め込みプロバイダーをサポートしています。`llm_profiles`で設定：\n```python\nfrom memu import MemUService\n\nservice = MemUService(\n    llm_profiles={\n        # LLM操作のデフォルトプロファイル\n        \"default\": {\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n            \"api_key\": \"your_api_key\",\n            \"chat_model\": \"qwen3-max\",\n            \"client_backend\": \"sdk\"  # \"sdk\" または \"http\"\n        },\n        # 埋め込み用の別プロファイル\n        \"embedding\": {\n            \"base_url\": \"https://api.voyageai.com/v1\",\n            \"api_key\": \"your_voyage_api_key\",\n            \"embed_model\": \"voyage-3.5-lite\"\n        }\n    },\n    # ... その他の設定\n)\n```\n\n---\n\n### OpenRouter統合\n\nMemUは[OpenRouter](https://openrouter.ai)をモデルプロバイダーとしてサポートし、単一のAPIを通じて複数のLLMプロバイダーにアクセスできます。\n\n#### 設定\n```python\nfrom memu import MemoryService\n\nservice = MemoryService(\n    llm_profiles={\n        \"default\": {\n            \"provider\": \"openrouter\",\n            \"client_backend\": \"httpx\",\n            \"base_url\": \"https://openrouter.ai\",\n            \"api_key\": \"your_openrouter_api_key\",\n            \"chat_model\": \"anthropic/claude-3.5-sonnet\",  # 任意のOpenRouterモデル\n            \"embed_model\": \"openai/text-embedding-3-small\",  # 埋め込みモデル\n        },\n    },\n    database_config={\n        \"metadata_store\": {\"provider\": \"inmemory\"},\n    },\n)\n```\n\n#### 環境変数\n\n| 変数 | 説明 |\n|------|------|\n| `OPENROUTER_API_KEY` | [openrouter.ai/keys](https://openrouter.ai/keys) からのOpenRouter APIキー |\n\n#### サポートされている機能\n\n| 機能 | ステータス | 注記 |\n|------|-----------|------|\n| チャット補完 | サポート済み | 任意のOpenRouterチャットモデルで動作 |\n| 埋め込み | サポート済み | OpenRouter経由でOpenAI埋め込みモデルを使用 |\n| ビジョン | サポート済み | ビジョン対応モデルを使用（例：`openai/gpt-4o`） |\n\n#### OpenRouterテストの実行\n```bash\nexport OPENROUTER_API_KEY=your_api_key\n\n# フルワークフローテスト（メモリ化 + 検索）\npython tests/test_openrouter.py\n\n# 埋め込み固有のテスト\npython tests/test_openrouter_embedding.py\n\n# ビジョン固有のテスト\npython tests/test_openrouter_vision.py\n```\n\n完全な動作例については [`examples/example_4_openrouter_memory.py`](../examples/example_4_openrouter_memory.py) を参照してください。\n\n---\n\n## 📖 コアAPI\n\n### `memorize()` - 継続学習パイプライン\n\n入力をリアルタイムで処理し、メモリを即座に更新：\n\n<img width=\"100%\" alt=\"memorize\" src=\"../assets/memorize.png\" />\n\n```python\nresult = await service.memorize(\n    resource_url=\"path/to/file.json\",  # ファイルパスまたはURL\n    modality=\"conversation\",            # conversation | document | image | video | audio\n    user={\"user_id\": \"123\"}             # オプション：ユーザーにスコープ\n)\n\n# 抽出されたメモリを即座に返す:\n{\n    \"resource\": {...},      # 保存されたリソースメタデータ\n    \"items\": [...],         # 抽出されたメモリアイテム（即座に利用可能）\n    \"categories\": [...]     # 自動更新されたカテゴリ構造\n}\n```\n\n**プロアクティブ機能：**\n- 遅延ゼロの処理—メモリが即座に利用可能\n- 手動タグ付けなしの自動分類\n- パターン検出のための既存メモリとの相互参照\n\n### `retrieve()` - デュアルモードインテリジェンス\n\nMemUは**プロアクティブコンテキストロード**と**リアクティブクエリ**の両方をサポート：\n\n<img width=\"100%\" alt=\"retrieve\" src=\"../assets/retrieve.png\" />\n\n#### RAGベースの検索 (`method=\"rag\"`)\n\n埋め込みを使用した高速な**プロアクティブコンテキスト組み立て**：\n\n- ✅ **インスタントコンテキスト**：サブ秒のメモリ表示\n- ✅ **バックグラウンド監視**：LLMコストなしで継続的に実行可能\n- ✅ **類似度スコアリング**：最も関連性の高いメモリを自動的に特定\n\n#### LLMベースの検索 (`method=\"llm\"`)\n\n複雑なコンテキストのための深い**予測的推論**：\n\n- ✅ **意図予測**：LLMがユーザーが尋ねる前にニーズを推測\n- ✅ **クエリ進化**：コンテキストの発展に応じて検索を自動的に改善\n- ✅ **早期終了**：十分なコンテキストが収集されたら停止\n\n#### 比較\n\n| 側面 | RAG（高速コンテキスト） | LLM（深い推論） |\n|------|----------------------|----------------|\n| **速度** | ⚡ ミリ秒 | 🐢 秒 |\n| **コスト** | 💰 埋め込みのみ | 💰💰 LLM推論 |\n| **プロアクティブ使用** | 継続的な監視 | トリガーされたコンテキストロード |\n| **最適な用途** | リアルタイムの提案 | 複雑な予測 |\n\n#### 使用方法\n```python\n# コンテキスト履歴を含むプロアクティブ検索\nresult = await service.retrieve(\n    queries=[\n        {\"role\": \"user\", \"content\": {\"text\": \"彼らの好みは何ですか？\"}},\n        {\"role\": \"user\", \"content\": {\"text\": \"仕事の習慣について教えて\"}}\n    ],\n    where={\"user_id\": \"123\"},  # オプション：スコープフィルター\n    method=\"rag\"  # または \"llm\" でより深い推論\n)\n\n# コンテキストに応じた結果を返す:\n{\n    \"categories\": [...],     # 関連トピック領域（自動優先順位付け）\n    \"items\": [...],          # 具体的なメモリファクト\n    \"resources\": [...],      # 追跡可能な元ソース\n    \"next_step_query\": \"...\" # 予測されたフォローアップコンテキスト\n}\n```\n\n**プロアクティブフィルタリング**：`where`を使用して継続的な監視のスコープを設定：\n- `where={\"user_id\": \"123\"}` - ユーザー固有のコンテキスト\n- `where={\"agent_id__in\": [\"1\", \"2\"]}` - マルチエージェント調整\n- `where`を省略してグローバルコンテキスト認識\n\n> 📚 **完全なAPIドキュメント**については、[SERVICE_API.md](../docs/SERVICE_API.md) を参照 - プロアクティブワークフローパターン、パイプライン設定、リアルタイム更新処理を含む。\n\n---\n\n## 💡 プロアクティブシナリオ\n\n### 例1：常に学習するアシスタント\n\n明示的なメモリコマンドなしで、すべてのインタラクションから継続的に学習：\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_1_conversation_memory.py\n```\n\n**プロアクティブな動作：**\n- カジュアルな言及から好みを自動的に抽出\n- インタラクションパターンから関係モデルを構築\n- 将来の会話で関連コンテキストを表示\n- 学習した好みに基づいてコミュニケーションスタイルを適応\n\n**最適な用途：** パーソナルAIアシスタント、記憶するカスタマーサポート、ソーシャルチャットボット\n\n---\n\n### 例2：自己改善エージェント\n\n実行ログから学習し、最適化をプロアクティブに提案：\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_2_skill_extraction.py\n```\n\n**プロアクティブな動作：**\n- エージェントのアクションと結果を継続的に監視\n- 成功と失敗のパターンを特定\n- 経験からスキルガイドを自動生成\n- 類似の将来のタスクに対する戦略をプロアクティブに提案\n\n**最適な用途：** DevOps自動化、エージェントの自己改善、ナレッジキャプチャ\n\n---\n\n### 例3：マルチモーダルコンテキストビルダー\n\n包括的なコンテキストのために異なる入力タイプ全体でメモリを統合：\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_3_multimodal_memory.py\n```\n\n**プロアクティブな動作：**\n- テキスト、画像、ドキュメントを自動的に相互参照\n- モダリティ全体で統一された理解を構築\n- 関連トピックについて議論する際に視覚的コンテキストを表示\n- 複数のソースを組み合わせて情報ニーズを予測\n\n**最適な用途：** ドキュメントシステム、学習プラットフォーム、研究アシスタント\n\n---\n\n## 📊 パフォーマンス\n\nMemUは、すべての推論タスクでLocomoベンチマークで**92.09%の平均精度**を達成し、信頼性の高いプロアクティブメモリ操作を実証しています。\n\n<img width=\"100%\" alt=\"benchmark\" src=\"https://github.com/user-attachments/assets/6fec4884-94e5-4058-ad5c-baac3d7e76d9\" />\n\n詳細な実験データを見る：[memU-experiment](https://github.com/NevaMind-AI/memU-experiment)\n\n---\n\n## 🧩 エコシステム\n\n| リポジトリ | 説明 | プロアクティブ機能 |\n|-----------|------|------------------|\n| **[memU](https://github.com/NevaMind-AI/memU)** | コアプロアクティブメモリエンジン | 7×24学習パイプライン、自動分類 |\n| **[memU-server](https://github.com/NevaMind-AI/memU-server)** | 継続同期を備えたバックエンド | リアルタイムメモリ更新、webhookトリガー |\n| **[memU-ui](https://github.com/NevaMind-AI/memU-ui)** | ビジュアルメモリダッシュボード | ライブメモリ進化モニタリング |\n\n**クイックリンク：**\n- 🚀 [MemU Cloudを試す](https://app.memu.so/quick-start)\n- 📚 [APIドキュメント](https://memu.pro/docs)\n- 💬 [Discordコミュニティ](https://discord.gg/memu)\n\n---\n\n## 🤝 パートナー\n\n<div align=\"center\">\n\n<a href=\"https://github.com/TEN-framework/ten-framework\"><img src=\"https://avatars.githubusercontent.com/u/113095513?s=200&v=4\" alt=\"Ten\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://openagents.org\"><img src=\"../assets/partners/openagents.png\" alt=\"OpenAgents\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/milvus-io/milvus\"><img src=\"https://miro.medium.com/v2/resize:fit:2400/1*-VEGyAgcIBD62XtZWavy8w.png\" alt=\"Milvus\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://xroute.ai/\"><img src=\"../assets/partners/xroute.png\" alt=\"xRoute\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://jaaz.app/\"><img src=\"../assets/partners/jazz.png\" alt=\"Jazz\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/Buddie-AI/Buddie\"><img src=\"../assets/partners/buddie.png\" alt=\"Buddie\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/bytebase/bytebase\"><img src=\"../assets/partners/bytebase.png\" alt=\"Bytebase\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/LazyAGI/LazyLLM\"><img src=\"../assets/partners/LazyLLM.png\" alt=\"LazyLLM\" height=\"40\" style=\"margin: 10px;\"></a>\n\n</div>\n\n---\n\n## 🤝 コントリビュート方法\n\nコミュニティからのコントリビュートを歓迎します！バグの修正、機能の追加、ドキュメントの改善など、あなたの助けに感謝します。\n\n### はじめに\n\nMemUへのコントリビュートを開始するには、開発環境をセットアップする必要があります：\n\n#### 前提条件\n- Python 3.13+\n- [uv](https://github.com/astral-sh/uv)（Pythonパッケージマネージャー）\n- Git\n\n#### 開発環境のセットアップ\n```bash\n# 1. リポジトリをフォークしてクローン\ngit clone https://github.com/YOUR_USERNAME/memU.git\ncd memU\n\n# 2. 開発依存関係をインストール\nmake install\n```\n\n`make install` コマンドは：\n- `uv`を使用して仮想環境を作成\n- すべてのプロジェクト依存関係をインストール\n- コード品質チェックのためのpre-commitフックをセットアップ\n\n#### 品質チェックの実行\n\nコントリビュートを提出する前に、コードがすべての品質チェックに合格していることを確認してください：\n```bash\nmake check\n```\n\n`make check` コマンドは：\n- **ロックファイル検証**：`pyproject.toml`の一貫性を確認\n- **Pre-commitフック**：Ruffでコードをリント、Blackでフォーマット\n- **型チェック**：静的型分析のための`mypy`を実行\n- **依存関係分析**：`deptry`で古い依存関係を検索\n\n### コントリビューションガイドライン\n\n詳細なコントリビューションガイドライン、コード標準、開発プラクティスについては、[CONTRIBUTING.md](../CONTRIBUTING.md) を参照してください。\n\n**クイックヒント：**\n- 各機能またはバグ修正用に新しいブランチを作成\n- 明確なコミットメッセージを書く\n- 新機能にテストを追加\n- 必要に応じてドキュメントを更新\n- プッシュ前に `make check` を実行\n\n---\n\n## 📄 ライセンス\n\n[Apache License 2.0](../LICENSE.txt)\n\n---\n\n## 🌍 コミュニティ\n\n- **GitHub Issues**：[バグを報告 & 機能をリクエスト](https://github.com/NevaMind-AI/memU/issues)\n- **Discord**：[コミュニティに参加](https://discord.com/invite/hQZntfGsbJ)\n- **X (Twitter)**：[@memU_ai をフォロー](https://x.com/memU_ai)\n- **お問い合わせ**：info@nevamind.ai\n\n---\n\n<div align=\"center\">\n\n⭐ **GitHubでスターを付けて**、新しいリリースの通知を受け取りましょう！\n\n</div>\n"
  },
  {
    "path": "readme/README_ko.md",
    "content": "![MemU Banner](../assets/banner.png)\n\n<div align=\"center\">\n\n# memU\n\n### AI 에이전트를 위한 상시 가동 프로액티브 메모리\n\n[![PyPI version](https://badge.fury.io/py/memu-py.svg)](https://badge.fury.io/py/memu-py)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/memu)\n[![Twitter](https://img.shields.io/badge/Twitter-Follow-1DA1F2?logo=x&logoColor=white)](https://x.com/memU_ai)\n\n<a href=\"https://trendshift.io/repositories/17374\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/17374\" alt=\"NevaMind-AI%2FmemU | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n**[English](README_en.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Français](README_fr.md)**\n\n</div>\n\n---\n\nmemU는 **24/7 프로액티브 에이전트**를 위해 구축된 메모리 프레임워크입니다.\n장시간 실행을 위해 설계되었으며, 에이전트를 항상 온라인으로 유지하는 **LLM 토큰 비용을 크게 절감**하여 상시 가동되고 진화하는 에이전트를 프로덕션 시스템에서 실용적으로 만듭니다.\nmemU는 **사용자 의도를 지속적으로 캡처하고 이해**합니다. 명령이 없어도 에이전트는 당신이 무엇을 하려는지 파악하고 스스로 행동합니다.\n\n---\n\n## 🤖 [OpenClaw (Moltbot, Clawdbot) Alternative](https://memu.bot)\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/memUbot.png\" />\n\n- **Download-and-use and simple** to get started.\n- Builds long-term memory to **understand user intent** and act proactively.\n- **Cuts LLM token cost** with smaller context.\n\nTry now: [memU bot](https://memu.bot)\n\n---\n\n## 🗃️ 메모리는 파일 시스템, 파일 시스템은 메모리\n\nmemU는 **메모리를 파일 시스템처럼** 다룹니다—구조화되고, 계층적이며, 즉시 접근 가능합니다.\n\n| 파일 시스템 | memU 메모리 |\n|------------|------------|\n| 📁 폴더 | 🏷️ 카테고리 (자동 정리된 주제) |\n| 📄 파일 | 🧠 메모리 아이템 (추출된 사실, 선호도, 스킬) |\n| 🔗 심볼릭 링크 | 🔄 교차 참조 (연관된 메모리 연결) |\n| 📂 마운트 포인트 | 📥 리소스 (대화, 문서, 이미지) |\n\n**왜 중요한가:**\n- **디렉토리를 탐색하듯 메모리를 탐색**—넓은 카테고리에서 구체적인 사실로 드릴다운\n- **새로운 지식을 즉시 마운트**—대화와 문서가 쿼리 가능한 메모리로 변환\n- **모든 것을 교차 연결**—메모리가 서로를 참조하여 연결된 지식 그래프 구축\n- **영구적이고 이식 가능**—파일처럼 메모리를 내보내기, 백업, 전송\n\n```\nmemory/\n├── preferences/\n│   ├── communication_style.md\n│   └── topic_interests.md\n├── relationships/\n│   ├── contacts/\n│   └── interaction_history/\n├── knowledge/\n│   ├── domain_expertise/\n│   └── learned_skills/\n└── context/\n    ├── recent_conversations/\n    └── pending_tasks/\n```\n\n파일 시스템이 원시 바이트를 정리된 데이터로 변환하듯, memU는 원시 상호작용을 **구조화되고, 검색 가능하며, 프로액티브한 인텔리전스**로 변환합니다.\n\n---\n\n## ⭐️ 리포지토리에 스타를\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/star.gif\" />\nMemU가 유용하거나 흥미롭다면, GitHub Star ⭐️를 눌러주시면 큰 힘이 됩니다.\n\n---\n\n\n## ✨ 핵심 기능\n\n| 기능 | 설명 |\n|------|------|\n| 🤖 **24/7 프로액티브 에이전트** | 백그라운드에서 지속적으로 작동하는 상시 가동 메모리 에이전트—절대 잠들지 않고, 절대 잊지 않습니다 |\n| 🎯 **사용자 의도 캡처** | 세션 간에 사용자의 목표, 선호도, 컨텍스트를 자동으로 이해하고 기억 |\n| 💰 **비용 효율적** | 인사이트를 캐싱하고 중복 LLM 호출을 방지하여 장기 실행 토큰 비용 절감 |\n---\n\n## 🔄 프로액티브 메모리 작동 방식\n\n```bash\n\ncd examples/proactive\npython proactive.py\n\n```\n\n---\n\n### Proactive Memory Lifecycle\n```\n┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\n│                                         USER QUERY                                               │\n└──────────────────────────────────────────────────────────────────────────────────────────────────┘\n                 │                                                           │\n                 ▼                                                           ▼\n┌────────────────────────────────────────┐         ┌────────────────────────────────────────────────┐\n│         🤖 MAIN AGENT                  │         │              🧠 MEMU BOT                       │\n│                                        │         │                                                │\n│  Handle user queries & execute tasks   │  ◄───►  │  Monitor, memorize & proactive intelligence   │\n├────────────────────────────────────────┤         ├────────────────────────────────────────────────┤\n│                                        │         │                                                │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  1. RECEIVE USER INPUT           │  │         │  │  1. MONITOR INPUT/OUTPUT                 │  │\n│  │     Parse query, understand      │  │   ───►  │  │     Observe agent interactions           │  │\n│  │     context and intent           │  │         │  │     Track conversation flow              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  2. PLAN & EXECUTE               │  │         │  │  2. MEMORIZE & EXTRACT                   │  │\n│  │     Break down tasks             │  │   ◄───  │  │     Store insights, facts, preferences   │  │\n│  │     Call tools, retrieve data    │  │  inject │  │     Extract skills & knowledge           │  │\n│  │     Generate responses           │  │  memory │  │     Update user profile                  │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  3. RESPOND TO USER              │  │         │  │  3. PREDICT USER INTENT                  │  │\n│  │     Deliver answer/result        │  │   ───►  │  │     Anticipate next steps                │  │\n│  │     Continue conversation        │  │         │  │     Identify upcoming needs              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  4. LOOP                         │  │         │  │  4. RUN PROACTIVE TASKS                  │  │\n│  │     Wait for next user input     │  │   ◄───  │  │     Pre-fetch relevant context           │  │\n│  │     or proactive suggestions     │  │  suggest│  │     Prepare recommendations              │  │\n│  └──────────────────────────────────┘  │         │  │     Update todolist autonomously         │  │\n│                                        │         │  └──────────────────────────────────────────┘  │\n└────────────────────────────────────────┘         └────────────────────────────────────────────────┘\n                 │                                                           │\n                 └───────────────────────────┬───────────────────────────────┘\n                                             ▼\n                              ┌──────────────────────────────┐\n                              │     CONTINUOUS SYNC LOOP     │\n                              │  Agent ◄──► MemU Bot ◄──► DB │\n                              └──────────────────────────────┘\n```\n\n---\n\n## 🎯 프로액티브 사용 사례\n\n### 1. **정보 추천**\n*에이전트가 관심사를 모니터링하고 관련 콘텐츠를 프로액티브하게 표시*\n```python\n# 사용자가 AI 주제를 연구하고 있음\nMemU 추적: 읽기 기록, 저장한 기사, 검색 쿼리\n\n# 새 콘텐츠가 도착했을 때:\n에이전트: \"검색 시스템에 대한 최근 연구와 일치하는 RAG 최적화에\n          대한 3개의 새 논문을 찾았습니다. 이전에 인용한 저자\n          (Dr. Chen)가 어제 발표했습니다.\"\n\n# 프로액티브 행동:\n- 브라우징 패턴에서 주제 선호도 학습\n- 저자/소스 신뢰도 선호도 추적\n- 참여 기록을 기반으로 노이즈 필터링\n- 최적의 주의를 끌 수 있는 타이밍에 추천\n```\n\n### 2. **이메일 관리**\n*에이전트가 커뮤니케이션 패턴을 학습하고 일상적인 통신 처리*\n```python\n# MemU가 시간에 따라 이메일 패턴을 관찰:\n- 일반적인 시나리오에 대한 응답 템플릿\n- 우선 연락처와 긴급 키워드\n- 일정 선호도와 가용 시간\n- 글쓰기 스타일과 톤 변화\n\n# 프로액티브 이메일 지원:\n에이전트: \"12개의 새 이메일이 있습니다. 3개의 일반 요청에 대한\n          답장을 작성했고, 우선 연락처로부터의 2개 긴급 항목에\n          플래그를 달았습니다. John이 언급한 충돌에 따라\n          내일 회의를 재조정할까요?\"\n\n# 자율적 작업:\n✓ 컨텍스트 인식 답장 초안 작성\n✓ 받은 편지함 분류 및 우선순위 지정\n✓ 일정 충돌 감지\n✓ 주요 결정이 포함된 긴 스레드 요약\n```\n\n### 3. **트레이딩 & 금융 모니터링**\n*에이전트가 시장 컨텍스트와 사용자 투자 행동 추적*\n```python\n# MemU가 트레이딩 선호도 학습:\n- 과거 결정에서의 위험 허용 범위\n- 선호 섹터 및 자산 클래스\n- 시장 이벤트에 대한 반응 패턴\n- 포트폴리오 리밸런싱 트리거\n\n# 프로액티브 알림:\n에이전트: \"NVDA가 시간 외 거래에서 5% 하락했습니다. 과거 행동에\n          기반하여, 일반적으로 3% 이상 기술주 하락 시 매수합니다.\n          현재 배분으로 70/30 주식-채권 목표를 유지하면서\n          $2,000 추가 노출이 가능합니다.\"\n\n# 지속적 모니터링:\n- 사용자 정의 임계값과 연결된 가격 알림 추적\n- 뉴스 이벤트와 포트폴리오 영향 연관\n- 실행된 추천과 무시된 추천에서 학습\n- 세금 손실 수확 기회 예측\n```\n\n\n...\n\n---\n\n## 🗂️ 계층적 메모리 아키텍처\n\nMemU의 3계층 시스템은 **반응적 쿼리**와 **프로액티브 컨텍스트 로딩**을 모두 지원합니다:\n\n<img width=\"100%\" alt=\"structure\" src=\"../assets/structure.png\" />\n\n| 계층 | 반응적 사용 | 프로액티브 사용 |\n|------|-----------|----------------|\n| **리소스** | 원본 데이터에 직접 액세스 | 새 패턴의 백그라운드 모니터링 |\n| **아이템** | 타겟팅된 팩트 검색 | 진행 중인 상호작용에서 실시간 추출 |\n| **카테고리** | 요약 수준 개요 | 예측을 위한 자동 컨텍스트 조합 |\n\n**프로액티브 이점:**\n- **자동 분류**: 새로운 메모리가 주제로 자체 조직화\n- **패턴 감지**: 시스템이 반복되는 테마를 식별\n- **컨텍스트 예측**: 다음에 필요한 정보를 예측\n\n---\n\n## 🚀 빠른 시작\n\n### 옵션 1: 클라우드 버전\n\n프로액티브 메모리를 즉시 경험하세요:\n\n👉 **[memu.so](https://memu.so)** - 7×24 지속 학습을 제공하는 호스팅 서비스\n\n사용자 정의 프로액티브 워크플로우를 포함한 엔터프라이즈 배포는 **info@nevamind.ai**로 문의하세요\n\n#### 클라우드 API (v3)\n\n| 기본 URL | `https://api.memu.so` |\n|---------|----------------------|\n| 인증 | `Authorization: Bearer YOUR_API_KEY` |\n\n| 메소드 | 엔드포인트 | 설명 |\n|-------|----------|------|\n| `POST` | `/api/v3/memory/memorize` | 지속 학습 작업 등록 |\n| `GET` | `/api/v3/memory/memorize/status/{task_id}` | 실시간 처리 상태 확인 |\n| `POST` | `/api/v3/memory/categories` | 자동 생성된 카테고리 목록 |\n| `POST` | `/api/v3/memory/retrieve` | 메모리 쿼리 (프로액티브 컨텍스트 로딩 지원) |\n\n📚 **[전체 API 문서](https://memu.pro/docs#cloud-version)**\n\n---\n\n### 옵션 2: 셀프 호스팅\n\n#### 설치\n```bash\npip install -e .\n```\n\n#### 기본 예제\n\n> **요구사항**: Python 3.13+ 및 OpenAI API 키\n\n**지속 학습 테스트** (인메모리):\n```bash\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_inmemory.py\n```\n\n**영구 저장소로 테스트** (PostgreSQL):\n```bash\n# pgvector가 포함된 PostgreSQL 시작\ndocker run -d \\\n  --name memu-postgres \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=memu \\\n  -p 5432:5432 \\\n  pgvector/pgvector:pg16\n\n# 지속 학습 테스트 실행\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_postgres.py\n```\n\n두 예제 모두 **프로액티브 메모리 워크플로우**를 보여줍니다:\n1. **지속적 수집**: 여러 파일을 순차적으로 처리\n2. **자동 추출**: 즉각적인 메모리 생성\n3. **프로액티브 검색**: 컨텍스트 인식 메모리 표시\n\n구현 세부사항은 [`tests/test_inmemory.py`](../tests/test_inmemory.py)와 [`tests/test_postgres.py`](../tests/test_postgres.py)를 참조하세요.\n\n---\n\n### 커스텀 LLM 및 임베딩 제공자\n\nMemU는 OpenAI 외에도 커스텀 LLM 및 임베딩 제공자를 지원합니다. `llm_profiles`로 구성:\n```python\nfrom memu import MemUService\n\nservice = MemUService(\n    llm_profiles={\n        # LLM 작업용 기본 프로필\n        \"default\": {\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n            \"api_key\": \"your_api_key\",\n            \"chat_model\": \"qwen3-max\",\n            \"client_backend\": \"sdk\"  # \"sdk\" 또는 \"http\"\n        },\n        # 임베딩용 별도 프로필\n        \"embedding\": {\n            \"base_url\": \"https://api.voyageai.com/v1\",\n            \"api_key\": \"your_voyage_api_key\",\n            \"embed_model\": \"voyage-3.5-lite\"\n        }\n    },\n    # ... 기타 구성\n)\n```\n\n---\n\n### OpenRouter 통합\n\nMemU는 [OpenRouter](https://openrouter.ai)를 모델 제공자로 지원하여 단일 API를 통해 여러 LLM 제공자에 액세스할 수 있습니다.\n\n#### 구성\n```python\nfrom memu import MemoryService\n\nservice = MemoryService(\n    llm_profiles={\n        \"default\": {\n            \"provider\": \"openrouter\",\n            \"client_backend\": \"httpx\",\n            \"base_url\": \"https://openrouter.ai\",\n            \"api_key\": \"your_openrouter_api_key\",\n            \"chat_model\": \"anthropic/claude-3.5-sonnet\",  # 모든 OpenRouter 모델\n            \"embed_model\": \"openai/text-embedding-3-small\",  # 임베딩 모델\n        },\n    },\n    database_config={\n        \"metadata_store\": {\"provider\": \"inmemory\"},\n    },\n)\n```\n\n#### 환경 변수\n\n| 변수 | 설명 |\n|------|------|\n| `OPENROUTER_API_KEY` | [openrouter.ai/keys](https://openrouter.ai/keys)에서 받은 OpenRouter API 키 |\n\n#### 지원 기능\n\n| 기능 | 상태 | 참고 |\n|------|------|------|\n| 채팅 완성 | 지원됨 | 모든 OpenRouter 채팅 모델과 작동 |\n| 임베딩 | 지원됨 | OpenRouter를 통해 OpenAI 임베딩 모델 사용 |\n| 비전 | 지원됨 | 비전 지원 모델 사용 (예: `openai/gpt-4o`) |\n\n#### OpenRouter 테스트 실행\n```bash\nexport OPENROUTER_API_KEY=your_api_key\n\n# 전체 워크플로우 테스트 (메모라이즈 + 검색)\npython tests/test_openrouter.py\n\n# 임베딩 특화 테스트\npython tests/test_openrouter_embedding.py\n\n# 비전 특화 테스트\npython tests/test_openrouter_vision.py\n```\n\n완전한 작동 예제는 [`examples/example_4_openrouter_memory.py`](../examples/example_4_openrouter_memory.py)를 참조하세요.\n\n---\n\n## 📖 핵심 API\n\n### `memorize()` - 지속 학습 파이프라인\n\n입력을 실시간으로 처리하고 메모리를 즉시 업데이트:\n\n<img width=\"100%\" alt=\"memorize\" src=\"../assets/memorize.png\" />\n\n```python\nresult = await service.memorize(\n    resource_url=\"path/to/file.json\",  # 파일 경로 또는 URL\n    modality=\"conversation\",            # conversation | document | image | video | audio\n    user={\"user_id\": \"123\"}             # 선택: 사용자로 범위 지정\n)\n\n# 추출된 메모리를 즉시 반환:\n{\n    \"resource\": {...},      # 저장된 리소스 메타데이터\n    \"items\": [...],         # 추출된 메모리 아이템 (즉시 사용 가능)\n    \"categories\": [...]     # 자동 업데이트된 카테고리 구조\n}\n```\n\n**프로액티브 기능:**\n- 지연 없는 처리—메모리 즉시 사용 가능\n- 수동 태그 지정 없는 자동 분류\n- 패턴 감지를 위한 기존 메모리와의 상호 참조\n\n### `retrieve()` - 이중 모드 인텔리전스\n\nMemU는 **프로액티브 컨텍스트 로딩**과 **반응적 쿼리**를 모두 지원:\n\n<img width=\"100%\" alt=\"retrieve\" src=\"../assets/retrieve.png\" />\n\n#### RAG 기반 검색 (`method=\"rag\"`)\n\n임베딩을 사용한 빠른 **프로액티브 컨텍스트 조합**:\n\n- ✅ **즉각적 컨텍스트**: 1초 미만의 메모리 표시\n- ✅ **백그라운드 모니터링**: LLM 비용 없이 지속적으로 실행 가능\n- ✅ **유사도 점수**: 가장 관련성 높은 메모리를 자동으로 식별\n\n#### LLM 기반 검색 (`method=\"llm\"`)\n\n복잡한 컨텍스트를 위한 심층 **예측적 추론**:\n\n- ✅ **의도 예측**: LLM이 사용자가 묻기 전에 필요를 추론\n- ✅ **쿼리 진화**: 컨텍스트 발전에 따라 검색 자동 개선\n- ✅ **조기 종료**: 충분한 컨텍스트가 수집되면 중지\n\n#### 비교\n\n| 측면 | RAG (빠른 컨텍스트) | LLM (심층 추론) |\n|------|-------------------|----------------|\n| **속도** | ⚡ 밀리초 | 🐢 초 |\n| **비용** | 💰 임베딩만 | 💰💰 LLM 추론 |\n| **프로액티브 사용** | 지속적 모니터링 | 트리거된 컨텍스트 로딩 |\n| **최적 용도** | 실시간 제안 | 복잡한 예측 |\n\n#### 사용법\n```python\n# 컨텍스트 히스토리를 포함한 프로액티브 검색\nresult = await service.retrieve(\n    queries=[\n        {\"role\": \"user\", \"content\": {\"text\": \"그들의 선호도가 무엇입니까?\"}},\n        {\"role\": \"user\", \"content\": {\"text\": \"업무 습관에 대해 알려주세요\"}}\n    ],\n    where={\"user_id\": \"123\"},  # 선택: 범위 필터\n    method=\"rag\"  # 또는 \"llm\"으로 더 깊은 추론\n)\n\n# 컨텍스트 인식 결과 반환:\n{\n    \"categories\": [...],     # 관련 주제 영역 (자동 우선순위)\n    \"items\": [...],          # 구체적인 메모리 팩트\n    \"resources\": [...],      # 추적 가능한 원본 소스\n    \"next_step_query\": \"...\" # 예측된 후속 컨텍스트\n}\n```\n\n**프로액티브 필터링**: `where`를 사용하여 지속적 모니터링 범위 지정:\n- `where={\"user_id\": \"123\"}` - 사용자별 컨텍스트\n- `where={\"agent_id__in\": [\"1\", \"2\"]}` - 다중 에이전트 조정\n- `where` 생략으로 전역 컨텍스트 인식\n\n> 📚 **전체 API 문서**는 [SERVICE_API.md](../docs/SERVICE_API.md) 참조 - 프로액티브 워크플로우 패턴, 파이프라인 구성, 실시간 업데이트 처리 포함.\n\n---\n\n## 💡 프로액티브 시나리오\n\n### 예제 1: 항상 학습하는 어시스턴트\n\n명시적인 메모리 명령 없이 모든 상호작용에서 지속적으로 학습:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_1_conversation_memory.py\n```\n\n**프로액티브 행동:**\n- 일상적인 언급에서 선호도를 자동으로 추출\n- 상호작용 패턴에서 관계 모델 구축\n- 향후 대화에서 관련 컨텍스트 표시\n- 학습된 선호도에 따라 커뮤니케이션 스타일 적응\n\n**최적 용도:** 개인 AI 어시스턴트, 기억하는 고객 지원, 소셜 챗봇\n\n---\n\n### 예제 2: 자기 개선 에이전트\n\n실행 로그에서 학습하고 최적화를 프로액티브하게 제안:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_2_skill_extraction.py\n```\n\n**프로액티브 행동:**\n- 에이전트 행동과 결과를 지속적으로 모니터링\n- 성공과 실패의 패턴 식별\n- 경험에서 스킬 가이드 자동 생성\n- 유사한 미래 작업에 대한 전략을 프로액티브하게 제안\n\n**최적 용도:** DevOps 자동화, 에이전트 자기 개선, 지식 캡처\n\n---\n\n### 예제 3: 멀티모달 컨텍스트 빌더\n\n포괄적인 컨텍스트를 위해 다양한 입력 유형의 메모리 통합:\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_3_multimodal_memory.py\n```\n\n**프로액티브 행동:**\n- 텍스트, 이미지, 문서를 자동으로 상호 참조\n- 모달리티 전반에 걸쳐 통합된 이해 구축\n- 관련 주제 논의 시 시각적 컨텍스트 표시\n- 여러 소스를 결합하여 정보 필요 예측\n\n**최적 용도:** 문서 시스템, 학습 플랫폼, 연구 어시스턴트\n\n---\n\n## 📊 성능\n\nMemU는 모든 추론 작업에서 Locomo 벤치마크에서 **92.09% 평균 정확도**를 달성하여 신뢰할 수 있는 프로액티브 메모리 작업을 입증합니다.\n\n<img width=\"100%\" alt=\"benchmark\" src=\"https://github.com/user-attachments/assets/6fec4884-94e5-4058-ad5c-baac3d7e76d9\" />\n\n상세 실험 데이터 보기: [memU-experiment](https://github.com/NevaMind-AI/memU-experiment)\n\n---\n\n## 🧩 에코시스템\n\n| 리포지토리 | 설명 | 프로액티브 기능 |\n|-----------|------|----------------|\n| **[memU](https://github.com/NevaMind-AI/memU)** | 핵심 프로액티브 메모리 엔진 | 7×24 학습 파이프라인, 자동 분류 |\n| **[memU-server](https://github.com/NevaMind-AI/memU-server)** | 지속 동기화가 포함된 백엔드 | 실시간 메모리 업데이트, 웹훅 트리거 |\n| **[memU-ui](https://github.com/NevaMind-AI/memU-ui)** | 시각적 메모리 대시보드 | 라이브 메모리 진화 모니터링 |\n\n**빠른 링크:**\n- 🚀 [MemU Cloud 체험](https://app.memu.so/quick-start)\n- 📚 [API 문서](https://memu.pro/docs)\n- 💬 [Discord 커뮤니티](https://discord.gg/memu)\n\n---\n\n## 🤝 파트너\n\n<div align=\"center\">\n\n<a href=\"https://github.com/TEN-framework/ten-framework\"><img src=\"https://avatars.githubusercontent.com/u/113095513?s=200&v=4\" alt=\"Ten\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://openagents.org\"><img src=\"../assets/partners/openagents.png\" alt=\"OpenAgents\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/milvus-io/milvus\"><img src=\"https://miro.medium.com/v2/resize:fit:2400/1*-VEGyAgcIBD62XtZWavy8w.png\" alt=\"Milvus\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://xroute.ai/\"><img src=\"../assets/partners/xroute.png\" alt=\"xRoute\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://jaaz.app/\"><img src=\"../assets/partners/jazz.png\" alt=\"Jazz\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/Buddie-AI/Buddie\"><img src=\"../assets/partners/buddie.png\" alt=\"Buddie\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/bytebase/bytebase\"><img src=\"../assets/partners/bytebase.png\" alt=\"Bytebase\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/LazyAGI/LazyLLM\"><img src=\"../assets/partners/LazyLLM.png\" alt=\"LazyLLM\" height=\"40\" style=\"margin: 10px;\"></a>\n\n</div>\n\n---\n\n## 🤝 기여 방법\n\n커뮤니티의 기여를 환영합니다! 버그 수정, 기능 추가, 문서 개선 등 어떤 도움이든 감사합니다.\n\n### 시작하기\n\nMemU에 기여하려면 개발 환경을 설정해야 합니다:\n\n#### 사전 요구사항\n- Python 3.13+\n- [uv](https://github.com/astral-sh/uv) (Python 패키지 관리자)\n- Git\n\n#### 개발 환경 설정\n```bash\n# 1. 리포지토리 포크 및 클론\ngit clone https://github.com/YOUR_USERNAME/memU.git\ncd memU\n\n# 2. 개발 의존성 설치\nmake install\n```\n\n`make install` 명령은:\n- `uv`를 사용하여 가상 환경 생성\n- 모든 프로젝트 의존성 설치\n- 코드 품질 검사를 위한 pre-commit 훅 설정\n\n#### 품질 검사 실행\n\n기여를 제출하기 전에 코드가 모든 품질 검사를 통과하는지 확인하세요:\n```bash\nmake check\n```\n\n`make check` 명령은:\n- **잠금 파일 검증**: `pyproject.toml` 일관성 확인\n- **Pre-commit 훅**: Ruff로 코드 린트, Black으로 포맷\n- **타입 검사**: 정적 타입 분석을 위한 `mypy` 실행\n- **의존성 분석**: `deptry`로 오래된 의존성 찾기\n\n### 기여 가이드라인\n\n상세한 기여 가이드라인, 코드 표준 및 개발 관행은 [CONTRIBUTING.md](../CONTRIBUTING.md)를 참조하세요.\n\n**빠른 팁:**\n- 각 기능 또는 버그 수정을 위한 새 브랜치 생성\n- 명확한 커밋 메시지 작성\n- 새 기능에 대한 테스트 추가\n- 필요에 따라 문서 업데이트\n- 푸시 전 `make check` 실행\n\n---\n\n## 📄 라이선스\n\n[Apache License 2.0](../LICENSE.txt)\n\n---\n\n## 🌍 커뮤니티\n\n- **GitHub Issues**: [버그 보고 및 기능 요청](https://github.com/NevaMind-AI/memU/issues)\n- **Discord**: [커뮤니티 참여](https://discord.com/invite/hQZntfGsbJ)\n- **X (Twitter)**: [@memU_ai 팔로우](https://x.com/memU_ai)\n- **연락처**: info@nevamind.ai\n\n---\n\n<div align=\"center\">\n\n⭐ **GitHub에서 스타를 눌러** 새 릴리스 알림을 받으세요!\n\n</div>\n"
  },
  {
    "path": "readme/README_zh.md",
    "content": "![MemU Banner](../assets/banner.png)\n\n<div align=\"center\">\n\n# memU\n\n### 面向 AI 智能体的全天候主动记忆系统\n\n[![PyPI version](https://badge.fury.io/py/memu-py.svg)](https://badge.fury.io/py/memu-py)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/memu)\n[![Twitter](https://img.shields.io/badge/Twitter-Follow-1DA1F2?logo=x&logoColor=white)](https://x.com/memU_ai)\n\n<a href=\"https://trendshift.io/repositories/17374\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/17374\" alt=\"NevaMind-AI%2FmemU | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n\n**[English](README_en.md) | [中文](README_zh.md) | [日本語](README_ja.md) | [한국어](README_ko.md) | [Español](README_es.md) | [Français](README_fr.md)**\n\n</div>\n\n---\n\nmemU 是一个专为 **24/7 主动智能体**打造的记忆框架。\n它专为长时间运行设计，能够大幅**降低保持智能体始终在线的 LLM token 成本**，使持续运行、不断进化的智能体在生产系统中变得切实可行。\nmemU **持续捕获并理解用户意图**。即使没有明确指令，智能体也能判断你即将要做什么并主动执行。\n\n---\n\n## 🤖 [OpenClaw (Moltbot, Clawdbot) Alternative](https://memu.bot)\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/memUbot.png\" />\n\n- **Download-and-use and simple** to get started.\n- Builds long-term memory to **understand user intent** and act proactively.\n- **Cuts LLM token cost** with smaller context.\n\nTry now: [memU bot](https://memu.bot)\n\n---\n\n## 🗃️ 记忆即文件系统，文件系统即记忆\n\nmemU 将**记忆视为文件系统**——结构化、层次化、即时可访问。\n\n| 文件系统 | memU 记忆 |\n|---------|----------|\n| 📁 文件夹 | 🏷️ 类别（自动组织的主题） |\n| 📄 文件 | 🧠 记忆条目（提取的事实、偏好、技能） |\n| 🔗 符号链接 | 🔄 交叉引用（关联的记忆相互链接） |\n| 📂 挂载点 | 📥 资源（对话、文档、图像） |\n\n**为何重要：**\n- **像浏览目录一样导航记忆**——从宽泛的类别深入到具体的事实\n- **即时挂载新知识**——对话和文档成为可查询的记忆\n- **万物互联**——记忆相互引用，构建连接的知识图谱\n- **持久化且可迁移**——像文件一样导出、备份和迁移记忆\n\n```\nmemory/\n├── preferences/\n│   ├── communication_style.md\n│   └── topic_interests.md\n├── relationships/\n│   ├── contacts/\n│   └── interaction_history/\n├── knowledge/\n│   ├── domain_expertise/\n│   └── learned_skills/\n└── context/\n    ├── recent_conversations/\n    └── pending_tasks/\n```\n\n正如文件系统将原始字节转化为有组织的数据，memU 将原始交互转化为**结构化、可搜索、主动式的智能**。\n\n---\n\n## ⭐️ 给项目点个星\n\n<img width=\"100%\" src=\"https://github.com/NevaMind-AI/memU/blob/main/assets/star.gif\" />\n如果你觉得 memU 有用或有趣，请给项目点个星 ⭐️，这将是对我们最大的支持！\n\n---\n\n\n## ✨ 核心能力\n\n| 能力 | 描述 |\n|------|------|\n| 🤖 **24/7 主动智能体** | 始终在线的记忆智能体，在后台持续工作——永不休眠，永不遗忘 |\n| 🎯 **用户意图捕获** | 自动理解并记住用户在各个会话中的目标、偏好和上下文 |\n| 💰 **成本高效** | 通过缓存洞察和避免冗余 LLM 调用来降低长期运行的 token 成本 |\n---\n\n## 🔄 主动记忆工作原理\n\n```bash\n\ncd examples/proactive\npython proactive.py\n\n```\n\n---\n\n### Proactive Memory Lifecycle\n```\n┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\n│                                         USER QUERY                                               │\n└──────────────────────────────────────────────────────────────────────────────────────────────────┘\n                 │                                                           │\n                 ▼                                                           ▼\n┌────────────────────────────────────────┐         ┌────────────────────────────────────────────────┐\n│         🤖 MAIN AGENT                  │         │              🧠 MEMU BOT                       │\n│                                        │         │                                                │\n│  Handle user queries & execute tasks   │  ◄───►  │  Monitor, memorize & proactive intelligence   │\n├────────────────────────────────────────┤         ├────────────────────────────────────────────────┤\n│                                        │         │                                                │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  1. RECEIVE USER INPUT           │  │         │  │  1. MONITOR INPUT/OUTPUT                 │  │\n│  │     Parse query, understand      │  │   ───►  │  │     Observe agent interactions           │  │\n│  │     context and intent           │  │         │  │     Track conversation flow              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  2. PLAN & EXECUTE               │  │         │  │  2. MEMORIZE & EXTRACT                   │  │\n│  │     Break down tasks             │  │   ◄───  │  │     Store insights, facts, preferences   │  │\n│  │     Call tools, retrieve data    │  │  inject │  │     Extract skills & knowledge           │  │\n│  │     Generate responses           │  │  memory │  │     Update user profile                  │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  3. RESPOND TO USER              │  │         │  │  3. PREDICT USER INTENT                  │  │\n│  │     Deliver answer/result        │  │   ───►  │  │     Anticipate next steps                │  │\n│  │     Continue conversation        │  │         │  │     Identify upcoming needs              │  │\n│  └──────────────────────────────────┘  │         │  └──────────────────────────────────────────┘  │\n│                 │                      │         │                    │                           │\n│                 ▼                      │         │                    ▼                           │\n│  ┌──────────────────────────────────┐  │         │  ┌──────────────────────────────────────────┐  │\n│  │  4. LOOP                         │  │         │  │  4. RUN PROACTIVE TASKS                  │  │\n│  │     Wait for next user input     │  │   ◄───  │  │     Pre-fetch relevant context           │  │\n│  │     or proactive suggestions     │  │  suggest│  │     Prepare recommendations              │  │\n│  └──────────────────────────────────┘  │         │  │     Update todolist autonomously         │  │\n│                                        │         │  └──────────────────────────────────────────┘  │\n└────────────────────────────────────────┘         └────────────────────────────────────────────────┘\n                 │                                                           │\n                 └───────────────────────────┬───────────────────────────────┘\n                                             ▼\n                              ┌──────────────────────────────┐\n                              │     CONTINUOUS SYNC LOOP     │\n                              │  Agent ◄──► MemU Bot ◄──► DB │\n                              └──────────────────────────────┘\n```\n\n---\n\n## 🎯 主动应用场景\n\n### 1. **信息推荐**\n*智能体监控用户兴趣，主动呈现相关内容*\n```python\n# 用户一直在研究 AI 话题\nMemU 追踪：阅读历史、收藏文章、搜索查询\n\n# 当新内容到达时：\n智能体：\"我发现了 3 篇关于 RAG 优化的新论文，与你最近关于检索系统的\n        研究方向一致。其中一位作者（陈博士）你之前引用过，昨天发表了新作。\"\n\n# 主动行为：\n- 从浏览模式学习话题偏好\n- 追踪作者/来源可信度偏好\n- 基于参与历史过滤噪音\n- 选择最佳时机进行推荐\n```\n\n### 2. **邮件管理**\n*智能体学习沟通模式，处理日常通信*\n```python\n# MemU 随时间观察邮件模式：\n- 常见场景的回复模板\n- 优先联系人和紧急关键词\n- 日程偏好和可用时间\n- 写作风格和语气变化\n\n# 主动邮件助理：\n智能体：\"你有 12 封新邮件。我已为 3 个常规请求起草了回复，\n        并标记了来自优先联系人的 2 个紧急事项。\n        需要我根据约翰提到的冲突重新安排明天的会议吗？\"\n\n# 自主执行：\n✓ 起草上下文感知的回复\n✓ 分类并排序收件箱\n✓ 检测日程冲突\n✓ 总结长对话线程的关键决策\n```\n\n### 3. **交易与财务监控**\n*智能体追踪市场情况和用户投资行为*\n```python\n# MemU 学习交易偏好：\n- 从历史决策中了解风险承受能力\n- 偏好的行业和资产类别\n- 对市场事件的响应模式\n- 投资组合再平衡触发条件\n\n# 主动提醒：\n智能体：\"NVDA 盘后下跌 5%。根据你的历史行为，\n        你通常在科技股跌幅超过 3% 时买入。\n        你当前的配置允许增加 $2,000 的敞口，\n        同时保持你 70/30 的股债目标。\"\n\n# 持续监控：\n- 追踪与用户定义阈值相关的价格警报\n- 关联新闻事件与投资组合影响\n- 从已执行与忽略的建议中学习\n- 预判税损收割机会\n```\n\n\n...\n\n---\n\n## 🗂️ 分层记忆架构\n\nMemU 的三层系统同时支持**响应式查询**和**主动上下文加载**：\n\n<img width=\"100%\" alt=\"structure\" src=\"../assets/structure.png\" />\n\n| 层级 | 响应式使用 | 主动使用 |\n|------|-----------|----------|\n| **资源层** | 直接访问原始数据 | 后台监控新模式 |\n| **条目层** | 针对性事实检索 | 从进行中的交互实时提取 |\n| **类别层** | 摘要级概览 | 自动上下文组装以进行预测 |\n\n**主动优势：**\n- **自动分类**：新记忆自组织到主题中\n- **模式检测**：系统识别重复出现的主题\n- **上下文预测**：预测接下来需要什么信息\n\n---\n\n## 🚀 快速开始\n\n### 选项 1：云版本\n\n立即体验主动记忆：\n\n👉 **[memu.so](https://memu.so)** - 提供 7×24 持续学习的托管服务\n\n如需具有自定义主动工作流的企业部署，请联系 **info@nevamind.ai**\n\n#### 云 API (v3)\n\n| 基础 URL | `https://api.memu.so` |\n|----------|----------------------|\n| 认证 | `Authorization: Bearer YOUR_API_KEY` |\n\n| 方法 | 端点 | 描述 |\n|------|------|------|\n| `POST` | `/api/v3/memory/memorize` | 注册持续学习任务 |\n| `GET` | `/api/v3/memory/memorize/status/{task_id}` | 检查实时处理状态 |\n| `POST` | `/api/v3/memory/categories` | 列出自动生成的类别 |\n| `POST` | `/api/v3/memory/retrieve` | 查询记忆（支持主动上下文加载） |\n\n📚 **[完整 API 文档](https://memu.pro/docs#cloud-version)**\n\n---\n\n### 选项 2：自托管\n\n#### 安装\n```bash\npip install -e .\n```\n\n#### 基础示例\n\n> **要求**：Python 3.13+ 和 OpenAI API 密钥\n\n**测试持续学习**（内存模式）：\n```bash\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_inmemory.py\n```\n\n**测试持久化存储**（PostgreSQL）：\n```bash\n# 启动带 pgvector 的 PostgreSQL\ndocker run -d \\\n  --name memu-postgres \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=memu \\\n  -p 5432:5432 \\\n  pgvector/pgvector:pg16\n\n# 运行持续学习测试\nexport OPENAI_API_KEY=your_api_key\ncd tests\npython test_postgres.py\n```\n\n两个示例都演示了**主动记忆工作流**：\n1. **持续摄入**：顺序处理多个文件\n2. **自动提取**：即时创建记忆\n3. **主动检索**：上下文感知的记忆呈现\n\n查看 [`tests/test_inmemory.py`](../tests/test_inmemory.py) 和 [`tests/test_postgres.py`](../tests/test_postgres.py) 了解实现细节。\n\n---\n\n### 自定义 LLM 和嵌入提供者\n\nMemU 支持 OpenAI 以外的自定义 LLM 和嵌入提供者。通过 `llm_profiles` 配置：\n```python\nfrom memu import MemUService\n\nservice = MemUService(\n    llm_profiles={\n        # LLM 操作的默认配置\n        \"default\": {\n            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n            \"api_key\": \"your_api_key\",\n            \"chat_model\": \"qwen3-max\",\n            \"client_backend\": \"sdk\"  # \"sdk\" 或 \"http\"\n        },\n        # 嵌入的单独配置\n        \"embedding\": {\n            \"base_url\": \"https://api.voyageai.com/v1\",\n            \"api_key\": \"your_voyage_api_key\",\n            \"embed_model\": \"voyage-3.5-lite\"\n        }\n    },\n    # ... 其他配置\n)\n```\n\n---\n\n### OpenRouter 集成\n\nMemU 支持 [OpenRouter](https://openrouter.ai) 作为模型提供者，让您通过单个 API 访问多个 LLM 提供者。\n\n#### 配置\n```python\nfrom memu import MemoryService\n\nservice = MemoryService(\n    llm_profiles={\n        \"default\": {\n            \"provider\": \"openrouter\",\n            \"client_backend\": \"httpx\",\n            \"base_url\": \"https://openrouter.ai\",\n            \"api_key\": \"your_openrouter_api_key\",\n            \"chat_model\": \"anthropic/claude-3.5-sonnet\",  # 任何 OpenRouter 模型\n            \"embed_model\": \"openai/text-embedding-3-small\",  # 嵌入模型\n        },\n    },\n    database_config={\n        \"metadata_store\": {\"provider\": \"inmemory\"},\n    },\n)\n```\n\n#### 环境变量\n\n| 变量 | 描述 |\n|------|------|\n| `OPENROUTER_API_KEY` | 您的 OpenRouter API 密钥，来自 [openrouter.ai/keys](https://openrouter.ai/keys) |\n\n#### 支持的功能\n\n| 功能 | 状态 | 备注 |\n|------|------|------|\n| 聊天补全 | 支持 | 适用于任何 OpenRouter 聊天模型 |\n| 嵌入 | 支持 | 通过 OpenRouter 使用 OpenAI 嵌入模型 |\n| 视觉 | 支持 | 使用支持视觉的模型（如 `openai/gpt-4o`） |\n\n#### 运行 OpenRouter 测试\n```bash\nexport OPENROUTER_API_KEY=your_api_key\n\n# 完整工作流测试（记忆 + 检索）\npython tests/test_openrouter.py\n\n# 嵌入专项测试\npython tests/test_openrouter_embedding.py\n\n# 视觉专项测试\npython tests/test_openrouter_vision.py\n```\n\n查看 [`examples/example_4_openrouter_memory.py`](../examples/example_4_openrouter_memory.py) 获取完整示例。\n\n---\n\n## 📖 核心 API\n\n### `memorize()` - 持续学习管道\n\n实时处理输入并立即更新记忆：\n\n<img width=\"100%\" alt=\"memorize\" src=\"../assets/memorize.png\" />\n\n```python\nresult = await service.memorize(\n    resource_url=\"path/to/file.json\",  # 文件路径或 URL\n    modality=\"conversation\",            # conversation | document | image | video | audio\n    user={\"user_id\": \"123\"}             # 可选：限定到特定用户\n)\n\n# 立即返回提取的记忆:\n{\n    \"resource\": {...},      # 存储的资源元数据\n    \"items\": [...],         # 提取的记忆条目（即时可用）\n    \"categories\": [...]     # 自动更新的类别结构\n}\n```\n\n**主动功能：**\n- 零延迟处理——记忆即时可用\n- 无需手动标记的自动分类\n- 与现有记忆交叉引用以检测模式\n\n### `retrieve()` - 双模式智能\n\nMemU 同时支持**主动上下文加载**和**响应式查询**：\n\n<img width=\"100%\" alt=\"retrieve\" src=\"../assets/retrieve.png\" />\n\n#### 基于 RAG 的检索 (`method=\"rag\"`)\n\n使用嵌入的快速**主动上下文组装**：\n\n- ✅ **即时上下文**：亚秒级记忆呈现\n- ✅ **后台监控**：可持续运行而无 LLM 成本\n- ✅ **相似度评分**：自动识别最相关的记忆\n\n#### 基于 LLM 的检索 (`method=\"llm\"`)\n\n针对复杂上下文的深度**预期性推理**：\n\n- ✅ **意图预测**：LLM 在用户询问之前推断需求\n- ✅ **查询演化**：随着上下文发展自动优化搜索\n- ✅ **提前终止**：收集到足够上下文时停止\n\n#### 对比\n\n| 方面 | RAG（快速上下文） | LLM（深度推理） |\n|------|------------------|----------------|\n| **速度** | ⚡ 毫秒级 | 🐢 秒级 |\n| **成本** | 💰 仅嵌入 | 💰💰 LLM 推理 |\n| **主动使用** | 持续监控 | 触发式上下文加载 |\n| **最适合** | 实时建议 | 复杂预测 |\n\n#### 使用\n```python\n# 带上下文历史的主动检索\nresult = await service.retrieve(\n    queries=[\n        {\"role\": \"user\", \"content\": {\"text\": \"他们的偏好是什么？\"}},\n        {\"role\": \"user\", \"content\": {\"text\": \"告诉我工作习惯\"}}\n    ],\n    where={\"user_id\": \"123\"},  # 可选：范围过滤\n    method=\"rag\"  # 或 \"llm\" 用于更深入的推理\n)\n\n# 返回上下文感知的结果:\n{\n    \"categories\": [...],     # 相关主题领域（自动优先排序）\n    \"items\": [...],          # 具体记忆事实\n    \"resources\": [...],      # 原始来源以供追溯\n    \"next_step_query\": \"...\" # 预测的后续上下文\n}\n```\n\n**主动过滤**：使用 `where` 限定持续监控范围：\n- `where={\"user_id\": \"123\"}` - 用户特定上下文\n- `where={\"agent_id__in\": [\"1\", \"2\"]}` - 多智能体协调\n- 省略 `where` 以获取全局上下文感知\n\n> 📚 **完整 API 文档**，请参阅 [SERVICE_API.md](../docs/SERVICE_API.md) - 包含主动工作流模式、管道配置和实时更新处理。\n\n---\n\n## 💡 主动场景\n\n### 示例 1：始终学习的助手\n\n无需显式记忆命令，从每次交互中持续学习：\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_1_conversation_memory.py\n```\n\n**主动行为：**\n- 从随意提及中自动提取偏好\n- 从交互模式构建关系模型\n- 在未来对话中呈现相关上下文\n- 根据学习的偏好调整沟通风格\n\n**最适合：** 个人 AI 助手、记住用户的客户支持、社交聊天机器人\n\n---\n\n### 示例 2：自我改进的智能体\n\n从执行日志中学习并主动建议优化：\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_2_skill_extraction.py\n```\n\n**主动行为：**\n- 持续监控智能体操作和结果\n- 识别成功和失败中的模式\n- 从经验中自动生成技能指南\n- 主动为类似的未来任务建议策略\n\n**最适合：** DevOps 自动化、智能体自我改进、知识捕获\n\n---\n\n### 示例 3：多模态上下文构建器\n\n将不同输入类型的记忆统一为全面的上下文：\n```bash\nexport OPENAI_API_KEY=your_api_key\npython examples/example_3_multimodal_memory.py\n```\n\n**主动行为：**\n- 自动交叉引用文本、图像和文档\n- 跨模态构建统一理解\n- 讨论相关话题时呈现视觉上下文\n- 通过组合多个来源预测信息需求\n\n**最适合：** 文档系统、学习平台、研究助手\n\n---\n\n## 📊 性能表现\n\nMemU 在 Locomo 基准测试中，在所有推理任务上实现了 **92.09% 的平均准确率**，展示了可靠的主动记忆操作。\n\n<img width=\"100%\" alt=\"benchmark\" src=\"https://github.com/user-attachments/assets/6fec4884-94e5-4058-ad5c-baac3d7e76d9\" />\n\n查看详细实验数据：[memU-experiment](https://github.com/NevaMind-AI/memU-experiment)\n\n---\n\n## 🧩 生态系统\n\n| 仓库 | 描述 | 主动功能 |\n|------|------|----------|\n| **[memU](https://github.com/NevaMind-AI/memU)** | 核心主动记忆引擎 | 7×24 学习管道、自动分类 |\n| **[memU-server](https://github.com/NevaMind-AI/memU-server)** | 带持续同步的后端 | 实时记忆更新、webhook 触发 |\n| **[memU-ui](https://github.com/NevaMind-AI/memU-ui)** | 可视化记忆仪表板 | 实时记忆演化监控 |\n\n**快速链接：**\n- 🚀 [试用 MemU 云服务](https://app.memu.so/quick-start)\n- 📚 [API 文档](https://memu.pro/docs)\n- 💬 [Discord 社区](https://discord.gg/memu)\n\n---\n\n## 🤝 合作伙伴\n\n<div align=\"center\">\n\n<a href=\"https://github.com/TEN-framework/ten-framework\"><img src=\"https://avatars.githubusercontent.com/u/113095513?s=200&v=4\" alt=\"Ten\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://openagents.org\"><img src=\"../assets/partners/openagents.png\" alt=\"OpenAgents\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/milvus-io/milvus\"><img src=\"https://miro.medium.com/v2/resize:fit:2400/1*-VEGyAgcIBD62XtZWavy8w.png\" alt=\"Milvus\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://xroute.ai/\"><img src=\"../assets/partners/xroute.png\" alt=\"xRoute\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://jaaz.app/\"><img src=\"../assets/partners/jazz.png\" alt=\"Jazz\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/Buddie-AI/Buddie\"><img src=\"../assets/partners/buddie.png\" alt=\"Buddie\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/bytebase/bytebase\"><img src=\"../assets/partners/bytebase.png\" alt=\"Bytebase\" height=\"40\" style=\"margin: 10px;\"></a>\n<a href=\"https://github.com/LazyAGI/LazyLLM\"><img src=\"../assets/partners/LazyLLM.png\" alt=\"LazyLLM\" height=\"40\" style=\"margin: 10px;\"></a>\n\n</div>\n\n---\n\n## 🤝 如何贡献\n\n我们欢迎社区的各种贡献！无论是修复错误、添加功能还是改进文档，您的帮助都将受到赞赏。\n\n### 开始贡献\n\n要开始为 MemU 做贡献，您需要设置开发环境：\n\n#### 先决条件\n- Python 3.13+\n- [uv](https://github.com/astral-sh/uv)（Python 包管理器）\n- Git\n\n#### 设置开发环境\n```bash\n# 1. Fork 并克隆仓库\ngit clone https://github.com/YOUR_USERNAME/memU.git\ncd memU\n\n# 2. 安装开发依赖\nmake install\n```\n\n`make install` 命令将：\n- 使用 `uv` 创建虚拟环境\n- 安装所有项目依赖\n- 设置代码质量检查的 pre-commit hooks\n\n#### 运行质量检查\n\n在提交贡献之前，请确保您的代码通过所有质量检查：\n```bash\nmake check\n```\n\n`make check` 命令运行：\n- **锁文件验证**：确保 `pyproject.toml` 一致性\n- **Pre-commit hooks**：使用 Ruff 检查代码，使用 Black 格式化\n- **类型检查**：运行 `mypy` 进行静态类型分析\n- **依赖分析**：使用 `deptry` 查找过时的依赖项\n\n### 贡献指南\n\n有关详细的贡献指南、代码标准和开发实践，请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md)。\n\n**快速提示：**\n- 为每个功能或错误修复创建新分支\n- 编写清晰的提交信息\n- 为新功能添加测试\n- 根据需要更新文档\n- 推送前运行 `make check`\n\n---\n\n## 📄 许可证\n\n[Apache License 2.0](../LICENSE.txt)\n\n---\n\n## 🌍 社区\n\n- **GitHub Issues**：[报告错误和请求功能](https://github.com/NevaMind-AI/memU/issues)\n- **Discord**：[加入社区](https://discord.com/invite/hQZntfGsbJ)\n- **X (Twitter)**：[关注 @memU_ai](https://x.com/memU_ai)\n- **联系方式**：info@nevamind.ai\n\n---\n\n<div align=\"center\">\n\n⭐ **在 GitHub 上给我们点星**，获取新版本通知！\n\n</div>\n"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\nmax-line-length = 120\nextend-ignore = E203,W503,E501\nexclude =\n    .git,\n    __pycache__,\n    .venv,\n    venv,\n    build,\n    dist,\n    *.egg-info,\n    .pytest_cache,\n    .mypy_cache\nper-file-ignores =\n    */test*.py:E402\n    **/test_*.py:E402\n    **/tests.py:E402\n    **/quick_memory_test.py:E402\n"
  },
  {
    "path": "src/lib.rs",
    "content": "use pyo3::prelude::*;\n\n#[pyfunction]\nfn hello_from_bin() -> String {\n    \"Hello from memu!\".to_string()\n}\n\n/// A Python module implemented in Rust. The name of this function must match\n/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to\n/// import the module.\n#[pymodule]\nfn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {\n    m.add_function(wrap_pyfunction!(hello_from_bin, m)?)?;\n    Ok(())\n}\n"
  },
  {
    "path": "src/memu/__init__.py",
    "content": "from memu._core import hello_from_bin\nfrom memu.app.service import MemoryService\n\n# Public alias used in documentation examples\nMemUService = MemoryService\n\n\ndef _rust_entry() -> str:\n    return hello_from_bin()\n"
  },
  {
    "path": "src/memu/_core.pyi",
    "content": "def hello_from_bin() -> str: ...\n"
  },
  {
    "path": "src/memu/app/__init__.py",
    "content": "from memu.app.service import MemoryService\nfrom memu.app.settings import (\n    BlobConfig,\n    DatabaseConfig,\n    DefaultUserModel,\n    LLMConfig,\n    LLMProfilesConfig,\n    MemorizeConfig,\n    RetrieveConfig,\n    UserConfig,\n)\nfrom memu.workflow.runner import (\n    LocalWorkflowRunner,\n    WorkflowRunner,\n    register_workflow_runner,\n    resolve_workflow_runner,\n)\n\n__all__ = [\n    \"BlobConfig\",\n    \"DatabaseConfig\",\n    \"DefaultUserModel\",\n    \"LLMConfig\",\n    \"LLMProfilesConfig\",\n    \"LocalWorkflowRunner\",\n    \"MemorizeConfig\",\n    \"MemoryService\",\n    \"RetrieveConfig\",\n    \"UserConfig\",\n    \"WorkflowRunner\",\n    \"register_workflow_runner\",\n    \"resolve_workflow_runner\",\n]\n"
  },
  {
    "path": "src/memu/app/crud.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nfrom collections.abc import Awaitable, Callable, Mapping\nfrom typing import TYPE_CHECKING, Any, cast, get_args\n\nfrom pydantic import BaseModel\n\nfrom memu.database.models import MemoryCategory, MemoryType\nfrom memu.prompts.category_patch import CATEGORY_PATCH_PROMPT\nfrom memu.workflow.step import WorkflowState, WorkflowStep\n\nlogger = logging.getLogger(__name__)\n\nif TYPE_CHECKING:\n    from memu.app.service import Context\n    from memu.app.settings import PatchConfig\n    from memu.database.interfaces import Database\n\n\nclass CRUDMixin:\n    if TYPE_CHECKING:\n        _run_workflow: Callable[..., Awaitable[WorkflowState]]\n        _get_context: Callable[[], Context]\n        _get_database: Callable[[], Database]\n        _get_step_llm_client: Callable[[Mapping[str, Any] | None], Any]\n        _get_step_embedding_client: Callable[[Mapping[str, Any] | None], Any]\n        _get_llm_client: Callable[..., Any]\n        _model_dump_without_embeddings: Callable[[BaseModel], dict[str, Any]]\n        _extract_json_blob: Callable[[str], str]\n        _escape_prompt_value: Callable[[str], str]\n        user_model: type[BaseModel]\n        patch_config: PatchConfig\n        _ensure_categories_ready: Callable[[Context, Database, Mapping[str, Any] | None], Awaitable[None]]\n\n    async def list_memory_items(\n        self,\n        where: dict[str, Any] | None = None,\n    ) -> dict[str, Any]:\n        ctx = self._get_context()\n        store = self._get_database()\n        where_filters = self._normalize_where(where)\n\n        state: WorkflowState = {\n            \"ctx\": ctx,\n            \"store\": store,\n            \"where\": where_filters,\n        }\n\n        result = await self._run_workflow(\"crud_list_memory_items\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"List memory items workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    async def list_memory_categories(\n        self,\n        where: dict[str, Any] | None = None,\n    ) -> dict[str, Any]:\n        ctx = self._get_context()\n        store = self._get_database()\n        where_filters = self._normalize_where(where)\n\n        state: WorkflowState = {\n            \"ctx\": ctx,\n            \"store\": store,\n            \"where\": where_filters,\n        }\n        result = await self._run_workflow(\"crud_list_memory_categories\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"List memory categories workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    async def clear_memory(\n        self,\n        where: dict[str, Any] | None = None,\n    ) -> dict[str, Any]:\n        ctx = self._get_context()\n        store = self._get_database()\n        where_filters = self._normalize_where(where)\n\n        state: WorkflowState = {\n            \"ctx\": ctx,\n            \"store\": store,\n            \"where\": where_filters,\n        }\n\n        result = await self._run_workflow(\"crud_clear_memory\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Clear memory workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    def _build_list_memory_items_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"list_memory_items\",\n                role=\"read_memories\",\n                handler=self._crud_list_memory_items,\n                requires={\"ctx\", \"store\", \"where\"},\n                produces={\"items\"},\n                capabilities={\"db\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._crud_build_list_items_response,\n                requires={\"items\", \"ctx\", \"store\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_list_memories_initial_keys() -> set[str]:\n        return {\n            \"ctx\",\n            \"store\",\n            \"where\",\n        }\n\n    def _build_list_memory_categories_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"list_memory_categories\",\n                role=\"read_categories\",\n                handler=self._crud_list_memory_categories,\n                requires={\"ctx\", \"store\", \"where\"},\n                produces={\"categories\"},\n                capabilities={\"db\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._crud_build_list_categories_response,\n                requires={\"categories\", \"ctx\", \"store\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    def _build_clear_memory_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"clear_memory_categories\",\n                role=\"delete_memories\",\n                handler=self._crud_clear_memory_categories,\n                requires={\"ctx\", \"store\", \"where\"},\n                produces={\"deleted_categories\"},\n                capabilities={\"db\"},\n            ),\n            WorkflowStep(\n                step_id=\"clear_memory_items\",\n                role=\"delete_memories\",\n                handler=self._crud_clear_memory_items,\n                requires={\"ctx\", \"store\", \"where\"},\n                produces={\"deleted_items\"},\n                capabilities={\"db\"},\n            ),\n            WorkflowStep(\n                step_id=\"clear_memory_resources\",\n                role=\"delete_memories\",\n                handler=self._crud_clear_memory_resources,\n                requires={\"ctx\", \"store\", \"where\"},\n                produces={\"deleted_resources\"},\n                capabilities={\"db\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._crud_build_clear_memory_response,\n                requires={\"ctx\", \"store\", \"deleted_categories\", \"deleted_items\", \"deleted_resources\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_clear_memories_initial_keys() -> set[str]:\n        return {\n            \"ctx\",\n            \"store\",\n            \"where\",\n        }\n\n    def _normalize_where(self, where: Mapping[str, Any] | None) -> dict[str, Any]:\n        \"\"\"Validate and clean the `where` scope filters against the configured user model.\"\"\"\n        if not where:\n            return {}\n\n        valid_fields = set(getattr(self.user_model, \"model_fields\", {}).keys())\n        cleaned: dict[str, Any] = {}\n\n        for raw_key, value in where.items():\n            if value is None:\n                continue\n            field = raw_key.split(\"__\", 1)[0]\n            if field not in valid_fields:\n                msg = f\"Unknown filter field '{field}' for current user scope\"\n                raise ValueError(msg)\n            cleaned[raw_key] = value\n\n        return cleaned\n\n    def _crud_list_memory_items(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        where_filters = state.get(\"where\") or {}\n        store = state[\"store\"]\n        items = store.memory_item_repo.list_items(where_filters)\n        state[\"items\"] = items\n        return state\n\n    def _crud_list_memory_categories(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        where_filters = state.get(\"where\") or {}\n        store = state[\"store\"]\n        categories = store.memory_category_repo.list_categories(where_filters)\n        state[\"categories\"] = categories\n        return state\n\n    def _crud_build_list_items_response(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        items = state[\"items\"]\n        items_list = [self._model_dump_without_embeddings(item) for item in items.values()]\n        response = {\n            \"items\": items_list,\n        }\n        state[\"response\"] = response\n        return state\n\n    def _crud_build_list_categories_response(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        categories = state[\"categories\"]\n        categories_list = [self._model_dump_without_embeddings(category) for category in categories.values()]\n        response = {\n            \"categories\": categories_list,\n        }\n        state[\"response\"] = response\n        return state\n\n    def _crud_clear_memory_categories(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        where_filters = state.get(\"where\") or {}\n        store = state[\"store\"]\n        deleted = store.memory_category_repo.clear_categories(where_filters)\n        state[\"deleted_categories\"] = deleted\n        return state\n\n    def _crud_clear_memory_items(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        where_filters = state.get(\"where\") or {}\n        store = state[\"store\"]\n        deleted = store.memory_item_repo.clear_items(where_filters)\n        state[\"deleted_items\"] = deleted\n        return state\n\n    def _crud_clear_memory_resources(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        where_filters = state.get(\"where\") or {}\n        store = state[\"store\"]\n        deleted = store.resource_repo.clear_resources(where_filters)\n        state[\"deleted_resources\"] = deleted\n        return state\n\n    def _crud_build_clear_memory_response(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        deleted_categories = state.get(\"deleted_categories\", {})\n        deleted_items = state.get(\"deleted_items\", {})\n        deleted_resources = state.get(\"deleted_resources\", {})\n        response = {\n            \"deleted_categories\": [self._model_dump_without_embeddings(cat) for cat in deleted_categories.values()],\n            \"deleted_items\": [self._model_dump_without_embeddings(item) for item in deleted_items.values()],\n            \"deleted_resources\": [self._model_dump_without_embeddings(res) for res in deleted_resources.values()],\n        }\n        state[\"response\"] = response\n        return state\n\n    async def create_memory_item(\n        self,\n        *,\n        memory_type: MemoryType,\n        memory_content: str,\n        memory_categories: list[str],\n        user: dict[str, Any] | None = None,\n        propagate: bool = True,\n    ) -> dict[str, Any]:\n        if memory_type not in get_args(MemoryType):\n            msg = f\"Invalid memory type: '{memory_type}', must be one of {get_args(MemoryType)}\"\n            raise ValueError(msg)\n\n        ctx = self._get_context()\n        store = self._get_database()\n        user_scope = self.user_model(**user).model_dump() if user is not None else None\n        await self._ensure_categories_ready(ctx, store, user_scope)\n\n        state: WorkflowState = {\n            \"memory_payload\": {\n                \"type\": memory_type,\n                \"content\": memory_content,\n                \"categories\": memory_categories,\n            },\n            \"ctx\": ctx,\n            \"store\": store,\n            \"category_ids\": list(ctx.category_ids),\n            \"user\": user_scope,\n            \"propagate\": propagate,\n        }\n\n        result = await self._run_workflow(\"patch_create\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Create memory item workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    async def update_memory_item(\n        self,\n        *,\n        memory_id: str,\n        memory_type: MemoryType | None = None,\n        memory_content: str | None = None,\n        memory_categories: list[str] | None = None,\n        user: dict[str, Any] | None = None,\n        propagate: bool = True,\n    ) -> dict[str, Any]:\n        if all((memory_type is None, memory_content is None, memory_categories is None)):\n            msg = \"At least one of memory type, memory content, or memory categories is required for UPDATE operation\"\n            raise ValueError(msg)\n        if memory_type and memory_type not in get_args(MemoryType):\n            msg = f\"Invalid memory type: '{memory_type}', must be one of {get_args(MemoryType)}\"\n            raise ValueError(msg)\n\n        ctx = self._get_context()\n        store = self._get_database()\n        user_scope = self.user_model(**user).model_dump() if user is not None else None\n        await self._ensure_categories_ready(ctx, store, user_scope)\n\n        state: WorkflowState = {\n            \"memory_id\": memory_id,\n            \"memory_payload\": {\n                \"type\": memory_type,\n                \"content\": memory_content,\n                \"categories\": memory_categories,\n            },\n            \"ctx\": ctx,\n            \"store\": store,\n            \"category_ids\": list(ctx.category_ids),\n            \"user\": user_scope,\n            \"propagate\": propagate,\n        }\n\n        result = await self._run_workflow(\"patch_update\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Update memory item workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    async def delete_memory_item(\n        self,\n        *,\n        memory_id: str,\n        user: dict[str, Any] | None = None,\n        propagate: bool = True,\n    ) -> dict[str, Any]:\n        ctx = self._get_context()\n        store = self._get_database()\n        user_scope = self.user_model(**user).model_dump() if user is not None else None\n        await self._ensure_categories_ready(ctx, store, user_scope)\n\n        state: WorkflowState = {\n            \"memory_id\": memory_id,\n            \"ctx\": ctx,\n            \"store\": store,\n            \"category_ids\": list(ctx.category_ids),\n            \"user\": user_scope,\n            \"propagate\": propagate,\n        }\n\n        result = await self._run_workflow(\"patch_delete\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Delete memory item workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    def _build_create_memory_item_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"create_memory_item\",\n                role=\"patch\",\n                handler=self._patch_create_memory_item,\n                requires={\"memory_payload\", \"ctx\", \"store\", \"user\"},\n                produces={\"memory_item\", \"category_updates\"},\n                capabilities={\"db\", \"llm\"},\n                config={\"embed_llm_profile\": \"embedding\"},\n            ),\n            WorkflowStep(\n                step_id=\"persist_index\",\n                role=\"persist\",\n                handler=self._patch_persist_and_index,\n                requires={\"category_updates\", \"ctx\", \"store\"},\n                produces={\"categories\"},\n                capabilities={\"db\", \"llm\"},\n                config={\"chat_llm_profile\": \"default\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._patch_build_response,\n                requires={\"memory_item\", \"category_updates\", \"ctx\", \"store\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_create_memory_item_initial_keys() -> set[str]:\n        return {\n            \"memory_payload\",\n            \"ctx\",\n            \"store\",\n            \"user\",\n        }\n\n    def _build_update_memory_item_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"update_memory_item\",\n                role=\"patch\",\n                handler=self._patch_update_memory_item,\n                requires={\"memory_id\", \"memory_payload\", \"ctx\", \"store\", \"user\"},\n                produces={\"memory_item\", \"category_updates\"},\n                capabilities={\"db\", \"llm\"},\n                config={\"embed_llm_profile\": \"embedding\"},\n            ),\n            WorkflowStep(\n                step_id=\"persist_index\",\n                role=\"persist\",\n                handler=self._patch_persist_and_index,\n                requires={\"category_updates\", \"ctx\", \"store\"},\n                produces={\"categories\"},\n                capabilities={\"db\", \"llm\"},\n                config={\"chat_llm_profile\": \"default\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._patch_build_response,\n                requires={\"memory_item\", \"category_updates\", \"ctx\", \"store\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_update_memory_item_initial_keys() -> set[str]:\n        return {\n            \"memory_id\",\n            \"memory_payload\",\n            \"ctx\",\n            \"store\",\n            \"user\",\n        }\n\n    def _build_delete_memory_item_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"delete_memory_item\",\n                role=\"patch\",\n                handler=self._patch_delete_memory_item,\n                requires={\"memory_id\", \"ctx\", \"store\", \"user\"},\n                produces={\"memory_item\", \"category_updates\"},\n                capabilities={\"db\"},\n            ),\n            WorkflowStep(\n                step_id=\"persist_index\",\n                role=\"persist\",\n                handler=self._patch_persist_and_index,\n                requires={\"category_updates\", \"ctx\", \"store\"},\n                produces={\"categories\"},\n                capabilities={\"db\", \"llm\"},\n                config={\"chat_llm_profile\": \"default\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._patch_build_response,\n                requires={\"memory_item\", \"category_updates\", \"ctx\", \"store\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_delete_memory_item_initial_keys() -> set[str]:\n        return {\n            \"memory_id\",\n            \"ctx\",\n            \"store\",\n            \"user\",\n        }\n\n    async def _patch_create_memory_item(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        memory_payload = state[\"memory_payload\"]\n        ctx = state[\"ctx\"]\n        store = state[\"store\"]\n        user = state[\"user\"]\n        propagate = state[\"propagate\"]\n        category_memory_updates: dict[str, tuple[Any, Any]] = {}\n\n        embed_payload = [memory_payload[\"content\"]]\n        content_embedding = (await self._get_step_embedding_client(step_context).embed(embed_payload))[0]\n\n        item = store.memory_item_repo.create_item(\n            memory_type=memory_payload[\"type\"],\n            summary=memory_payload[\"content\"],\n            embedding=content_embedding,\n            user_data=dict(user or {}),\n        )\n        cat_names = memory_payload[\"categories\"]\n        mapped_cat_ids = self._map_category_names_to_ids(cat_names, ctx)\n        for cid in mapped_cat_ids:\n            store.category_item_repo.link_item_category(item.id, cid, user_data=dict(user or {}))\n            if propagate:\n                category_memory_updates[cid] = (None, memory_payload[\"content\"])\n\n        state.update({\n            \"memory_item\": item,\n            \"category_updates\": category_memory_updates,\n        })\n        return state\n\n    async def _patch_update_memory_item(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        memory_id = state[\"memory_id\"]\n        memory_payload = state[\"memory_payload\"]\n        ctx = state[\"ctx\"]\n        store = state[\"store\"]\n        user = state[\"user\"]\n        propagate = state[\"propagate\"]\n        category_memory_updates: dict[str, tuple[Any, Any]] = {}\n\n        item = store.memory_item_repo.get_item(memory_id)\n        if not item:\n            msg = f\"Memory item with id {memory_id} not found\"\n            raise ValueError(msg)\n        old_content = item.summary\n        old_item_categories = store.category_item_repo.get_item_categories(memory_id)\n        mapped_old_cat_ids = [cat.category_id for cat in old_item_categories]\n\n        if memory_payload[\"content\"]:\n            embed_payload = [memory_payload[\"content\"]]\n            content_embedding = (await self._get_step_embedding_client(step_context).embed(embed_payload))[0]\n        else:\n            content_embedding = None\n\n        if memory_payload[\"type\"] or memory_payload[\"content\"]:\n            item = store.memory_item_repo.update_item(\n                item_id=memory_id,\n                memory_type=memory_payload[\"type\"],\n                summary=memory_payload[\"content\"],\n                embedding=content_embedding,\n            )\n        new_cat_names = memory_payload[\"categories\"]\n        mapped_new_cat_ids = self._map_category_names_to_ids(new_cat_names, ctx)\n\n        cats_to_remove = set(mapped_old_cat_ids) - set(mapped_new_cat_ids)\n        cats_to_add = set(mapped_new_cat_ids) - set(mapped_old_cat_ids)\n        for cid in cats_to_remove:\n            store.category_item_repo.unlink_item_category(memory_id, cid)\n            if propagate:\n                category_memory_updates[cid] = (old_content, None)\n        for cid in cats_to_add:\n            store.category_item_repo.link_item_category(memory_id, cid, user_data=dict(user or {}))\n            if propagate:\n                category_memory_updates[cid] = (None, item.summary)\n\n        if propagate and memory_payload[\"content\"]:\n            for cid in set(mapped_old_cat_ids) & set(mapped_new_cat_ids):\n                category_memory_updates[cid] = (old_content, item.summary)\n\n        state.update({\n            \"memory_item\": item,\n            \"category_updates\": category_memory_updates,\n        })\n        return state\n\n    async def _patch_delete_memory_item(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        memory_id = state[\"memory_id\"]\n        store = state[\"store\"]\n        propagate = state[\"propagate\"]\n        category_memory_updates: dict[str, tuple[Any, Any]] = {}\n\n        item = store.memory_item_repo.get_item(memory_id)\n        if not item:\n            msg = f\"Memory item with id {memory_id} not found\"\n            raise ValueError(msg)\n        item_categories = store.category_item_repo.get_item_categories(memory_id)\n        if propagate:\n            for cat in item_categories:\n                category_memory_updates[cat.category_id] = (item.summary, None)\n        store.memory_item_repo.delete_item(memory_id)\n\n        state.update({\n            \"memory_item\": item,\n            \"category_updates\": category_memory_updates,\n        })\n        return state\n\n    async def _patch_persist_and_index(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        llm_client = self._get_step_llm_client(step_context)\n        await self._patch_category_summaries(\n            state.get(\"category_updates\", {}),\n            ctx=state[\"ctx\"],\n            store=state[\"store\"],\n            llm_client=llm_client,\n        )\n        return state\n\n    def _patch_build_response(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        store = state[\"store\"]\n        item = self._model_dump_without_embeddings(state[\"memory_item\"])\n        category_updates_ids = list(state.get(\"category_updates\", {}).keys())\n        category_updates = [\n            self._model_dump_without_embeddings(store.memory_category_repo.categories[c]) for c in category_updates_ids\n        ]\n        response = {\n            \"memory_item\": item,\n            \"category_updates\": category_updates,\n        }\n        state[\"response\"] = response\n        return state\n\n    def _map_category_names_to_ids(self, names: list[str], ctx: Context) -> list[str]:\n        if not names:\n            return []\n        mapped: list[str] = []\n        seen: set[str] = set()\n        for name in names:\n            key = name.strip().lower()\n            cid = ctx.category_name_to_id.get(key)\n            if cid and cid not in seen:\n                mapped.append(cid)\n                seen.add(cid)\n        return mapped\n\n    async def _patch_category_summaries(\n        self,\n        updates: dict[str, list[str]],\n        ctx: Context,\n        store: Database,\n        llm_client: Any | None = None,\n    ) -> None:\n        if not updates:\n            return\n        tasks = []\n        target_ids: list[str] = []\n        client = llm_client or self._get_llm_client()\n        for cid, (content_before, content_after) in updates.items():\n            cat = store.memory_category_repo.categories.get(cid)\n            if not cat or (not content_before and not content_after):\n                continue\n            prompt = self._build_category_patch_prompt(\n                category=cat, content_before=content_before, content_after=content_after\n            )\n            tasks.append(client.chat(prompt))\n            target_ids.append(cid)\n        if not tasks:\n            return\n        patches = await asyncio.gather(*tasks)\n        for cid, patch in zip(target_ids, patches, strict=True):\n            need_update, summary = self._parse_category_patch_response(patch)\n            if not need_update:\n                continue\n            cat = store.memory_category_repo.categories.get(cid)\n            store.memory_category_repo.update_category(\n                category_id=cid,\n                summary=summary.strip(),\n            )\n\n    def _build_category_patch_prompt(\n        self, *, category: MemoryCategory, content_before: str | None, content_after: str | None\n    ) -> str:\n        if content_before and content_after:\n            update_content = \"\\n\".join([\n                \"The memory content before:\",\n                content_before,\n                \"The memory content after:\",\n                content_after,\n            ])\n        elif content_before:\n            update_content = \"\\n\".join([\n                \"This memory content is discarded:\",\n                content_before,\n            ])\n        elif content_after:\n            update_content = \"\\n\".join([\n                \"This memory content is newly added:\",\n                content_after,\n            ])\n        original_content = category.summary or \"\"\n        prompt = CATEGORY_PATCH_PROMPT\n        return prompt.format(\n            category=self._escape_prompt_value(category.name),\n            original_content=self._escape_prompt_value(original_content or \"\"),\n            update_content=self._escape_prompt_value(update_content or \"\"),\n        )\n\n    def _parse_category_patch_response(self, response: str) -> tuple[bool, str]:\n        try:\n            data = json.loads(response)\n        except (json.JSONDecodeError, TypeError):\n            return False, \"\"\n        if not isinstance(data, dict):\n            return False, \"\"\n        if not data.get(\"updated_content\"):\n            return False, \"\"\n        need_update = data.get(\"need_update\", False)\n        updated_content = data[\"updated_content\"].strip()\n        if updated_content == \"empty\":\n            updated_content = \"\"\n        return need_update, updated_content\n"
  },
  {
    "path": "src/memu/app/memorize.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport pathlib\nimport re\nfrom collections.abc import Awaitable, Callable, Mapping, Sequence\nfrom typing import TYPE_CHECKING, Any, cast\nfrom xml.etree.ElementTree import Element\n\nimport defusedxml.ElementTree as ET\nfrom pydantic import BaseModel\n\nfrom memu.app.settings import CategoryConfig, CustomPrompt\nfrom memu.database.models import CategoryItem, MemoryCategory, MemoryItem, MemoryType, Resource\nfrom memu.prompts.category_summary import (\n    CUSTOM_PROMPT as CATEGORY_SUMMARY_CUSTOM_PROMPT,\n)\nfrom memu.prompts.category_summary import (\n    PROMPT as CATEGORY_SUMMARY_PROMPT,\n)\nfrom memu.prompts.memory_type import (\n    CUSTOM_PROMPTS as MEMORY_TYPE_CUSTOM_PROMPTS,\n)\nfrom memu.prompts.memory_type import (\n    CUSTOM_TYPE_CUSTOM_PROMPTS,\n    DEFAULT_MEMORY_TYPES,\n)\nfrom memu.prompts.memory_type import (\n    PROMPTS as MEMORY_TYPE_PROMPTS,\n)\nfrom memu.prompts.preprocess import PROMPTS as PREPROCESS_PROMPTS\nfrom memu.utils.conversation import format_conversation_for_preprocess\nfrom memu.utils.video import VideoFrameExtractor\nfrom memu.workflow.step import WorkflowState, WorkflowStep\n\nlogger = logging.getLogger(__name__)\n\nif TYPE_CHECKING:\n    from memu.app.service import Context\n    from memu.app.settings import MemorizeConfig\n    from memu.blob.local_fs import LocalFS\n    from memu.database.interfaces import Database\n\n\nclass MemorizeMixin:\n    if TYPE_CHECKING:\n        memorize_config: MemorizeConfig\n        category_configs: list[CategoryConfig]\n        category_config_map: dict[str, CategoryConfig]\n        _category_prompt_str: str\n        fs: LocalFS\n        _run_workflow: Callable[..., Awaitable[WorkflowState]]\n        _get_context: Callable[[], Context]\n        _get_database: Callable[[], Database]\n        _get_step_llm_client: Callable[[Mapping[str, Any] | None], Any]\n        _get_step_embedding_client: Callable[[Mapping[str, Any] | None], Any]\n        _get_llm_client: Callable[..., Any]\n        _model_dump_without_embeddings: Callable[[BaseModel], dict[str, Any]]\n        _extract_json_blob: Callable[[str], str]\n        _escape_prompt_value: Callable[[str], str]\n        user_model: type[BaseModel]\n\n    async def memorize(\n        self,\n        *,\n        resource_url: str,\n        modality: str,\n        user: dict[str, Any] | None = None,\n    ) -> dict[str, Any]:\n        ctx = self._get_context()\n        store = self._get_database()\n        user_scope = self.user_model(**user).model_dump() if user is not None else None\n        await self._ensure_categories_ready(ctx, store, user_scope)\n\n        memory_types = self._resolve_memory_types()\n\n        state: WorkflowState = {\n            \"resource_url\": resource_url,\n            \"modality\": modality,\n            \"memory_types\": memory_types,\n            \"categories_prompt_str\": self._category_prompt_str,\n            \"ctx\": ctx,\n            \"store\": store,\n            \"category_ids\": list(ctx.category_ids),\n            \"user\": user_scope,\n        }\n\n        result = await self._run_workflow(\"memorize\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Memorize workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    def _build_memorize_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"ingest_resource\",\n                role=\"ingest\",\n                handler=self._memorize_ingest_resource,\n                requires={\"resource_url\", \"modality\"},\n                produces={\"local_path\", \"raw_text\"},\n                capabilities={\"io\"},\n            ),\n            WorkflowStep(\n                step_id=\"preprocess_multimodal\",\n                role=\"preprocess\",\n                handler=self._memorize_preprocess_multimodal,\n                requires={\"local_path\", \"modality\", \"raw_text\"},\n                produces={\"preprocessed_resources\"},\n                capabilities={\"llm\"},\n                config={\"chat_llm_profile\": self.memorize_config.preprocess_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"extract_items\",\n                role=\"extract\",\n                handler=self._memorize_extract_items,\n                requires={\n                    \"preprocessed_resources\",\n                    \"memory_types\",\n                    \"categories_prompt_str\",\n                    \"modality\",\n                    \"resource_url\",\n                },\n                produces={\"resource_plans\"},\n                capabilities={\"llm\"},\n                config={\"chat_llm_profile\": self.memorize_config.memory_extract_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"dedupe_merge\",\n                role=\"dedupe_merge\",\n                handler=self._memorize_dedupe_merge,\n                requires={\"resource_plans\"},\n                produces={\"resource_plans\"},\n                capabilities=set(),\n            ),\n            WorkflowStep(\n                step_id=\"categorize_items\",\n                role=\"categorize\",\n                handler=self._memorize_categorize_items,\n                requires={\"resource_plans\", \"ctx\", \"store\", \"local_path\", \"modality\", \"user\"},\n                produces={\"resources\", \"items\", \"relations\", \"category_updates\"},\n                capabilities={\"db\", \"vector\"},\n                config={\"embed_llm_profile\": \"embedding\"},\n            ),\n            WorkflowStep(\n                step_id=\"persist_index\",\n                role=\"persist\",\n                handler=self._memorize_persist_and_index,\n                requires={\"category_updates\", \"ctx\", \"store\"},\n                produces={\"categories\"},\n                capabilities={\"db\", \"llm\"},\n                config={\"chat_llm_profile\": self.memorize_config.category_update_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._memorize_build_response,\n                requires={\"resources\", \"items\", \"relations\", \"ctx\", \"store\", \"category_ids\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_memorize_initial_keys() -> set[str]:\n        return {\n            \"resource_url\",\n            \"modality\",\n            \"memory_types\",\n            \"categories_prompt_str\",\n            \"ctx\",\n            \"store\",\n            \"category_ids\",\n            \"user\",\n        }\n\n    async def _memorize_ingest_resource(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        local_path, raw_text = await self.fs.fetch(state[\"resource_url\"], state[\"modality\"])\n        state.update({\"local_path\": local_path, \"raw_text\": raw_text})\n        return state\n\n    async def _memorize_preprocess_multimodal(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        llm_client = self._get_step_llm_client(step_context)\n        preprocessed = await self._preprocess_resource_url(\n            local_path=state[\"local_path\"],\n            text=state.get(\"raw_text\"),\n            modality=state[\"modality\"],\n            llm_client=llm_client,\n        )\n        if not preprocessed:\n            preprocessed = [{\"text\": state.get(\"raw_text\"), \"caption\": None}]\n        state[\"preprocessed_resources\"] = preprocessed\n        return state\n\n    async def _memorize_extract_items(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        llm_client = self._get_step_llm_client(step_context)\n        preprocessed_resources = state.get(\"preprocessed_resources\", [])\n        resource_plans: list[dict[str, Any]] = []\n        total_segments = len(preprocessed_resources) or 1\n\n        for idx, prep in enumerate(preprocessed_resources):\n            res_url = self._segment_resource_url(state[\"resource_url\"], idx, total_segments)\n            text = prep.get(\"text\")\n            caption = prep.get(\"caption\")\n\n            structured_entries = await self._generate_structured_entries(\n                resource_url=res_url,\n                modality=state[\"modality\"],\n                memory_types=state[\"memory_types\"],\n                text=text,\n                categories_prompt_str=state[\"categories_prompt_str\"],\n                llm_client=llm_client,\n            )\n\n            resource_plans.append({\n                \"resource_url\": res_url,\n                \"text\": text,\n                \"caption\": caption,\n                \"entries\": structured_entries,\n            })\n\n        state[\"resource_plans\"] = resource_plans\n        return state\n\n    def _memorize_dedupe_merge(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        # Placeholder for future dedup/merge logic\n        state[\"resource_plans\"] = state.get(\"resource_plans\", [])\n        return state\n\n    async def _memorize_categorize_items(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        embed_client = self._get_step_embedding_client(step_context)\n        ctx = state[\"ctx\"]\n        store = state[\"store\"]\n        modality = state[\"modality\"]\n        local_path = state[\"local_path\"]\n        resources: list[Resource] = []\n        items: list[MemoryItem] = []\n        relations: list[CategoryItem] = []\n        category_updates: dict[str, list[tuple[str, str]]] = {}\n        user_scope = state.get(\"user\", {})\n\n        for plan in state.get(\"resource_plans\", []):\n            res = await self._create_resource_with_caption(\n                resource_url=plan[\"resource_url\"],\n                modality=modality,\n                local_path=local_path,\n                caption=plan.get(\"caption\"),\n                store=store,\n                embed_client=embed_client,\n                user=user_scope,\n            )\n            resources.append(res)\n\n            entries = plan.get(\"entries\") or []\n            if not entries:\n                continue\n\n            mem_items, rels, cat_updates = await self._persist_memory_items(\n                resource_id=res.id,\n                structured_entries=entries,\n                ctx=ctx,\n                store=store,\n                embed_client=embed_client,\n                user=user_scope,\n            )\n            items.extend(mem_items)\n            relations.extend(rels)\n            for cat_id, mems in cat_updates.items():\n                category_updates.setdefault(cat_id, []).extend(mems)\n\n        state.update({\n            \"resources\": resources,\n            \"items\": items,\n            \"relations\": relations,\n            \"category_updates\": category_updates,\n        })\n        return state\n\n    async def _memorize_persist_and_index(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        llm_client = self._get_step_llm_client(step_context)\n        updated_summaries = await self._update_category_summaries(\n            state.get(\"category_updates\", {}),\n            ctx=state[\"ctx\"],\n            store=state[\"store\"],\n            llm_client=llm_client,\n        )\n        if self.memorize_config.enable_item_references:\n            await self._persist_item_references(\n                updated_summaries=updated_summaries,\n                category_updates=state.get(\"category_updates\", {}),\n                store=state[\"store\"],\n            )\n        return state\n\n    def _memorize_build_response(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        ctx = state[\"ctx\"]\n        store = state[\"store\"]\n        resources = [self._model_dump_without_embeddings(r) for r in state.get(\"resources\", [])]\n        items = [self._model_dump_without_embeddings(item) for item in state.get(\"items\", [])]\n        relations = [rel.model_dump() for rel in state.get(\"relations\", [])]\n        category_ids = state.get(\"category_ids\") or list(ctx.category_ids)\n        categories = [\n            self._model_dump_without_embeddings(store.memory_category_repo.categories[c]) for c in category_ids\n        ]\n\n        if len(resources) == 1:\n            response = {\n                \"resource\": resources[0],\n                \"items\": items,\n                \"categories\": categories,\n                \"relations\": relations,\n            }\n        else:\n            response = {\n                \"resources\": resources,\n                \"items\": items,\n                \"categories\": categories,\n                \"relations\": relations,\n            }\n        state[\"response\"] = response\n        return state\n\n    def _segment_resource_url(self, base_url: str, idx: int, total_segments: int) -> str:\n        if total_segments <= 1:\n            return base_url\n        path = pathlib.Path(base_url)\n        return f\"{path.stem}_#segment_{idx}{path.suffix}\"\n\n    async def _fetch_and_preprocess_resource(\n        self, resource_url: str, modality: str, llm_client: Any | None = None\n    ) -> tuple[str, list[dict[str, str | None]]]:\n        \"\"\"\n        Fetch and preprocess a resource.\n\n        Returns:\n            Tuple of (local_path, preprocessed_resources)\n            where preprocessed_resources is a list of dicts with 'text' and 'caption'\n        \"\"\"\n        local_path, text = await self.fs.fetch(resource_url, modality)\n        preprocessed_resources = await self._preprocess_resource_url(\n            local_path=local_path,\n            text=text,\n            modality=modality,\n            llm_client=llm_client,\n        )\n        return local_path, preprocessed_resources\n\n    async def _create_resource_with_caption(\n        self,\n        *,\n        resource_url: str,\n        modality: str,\n        local_path: str,\n        caption: str | None,\n        store: Database,\n        embed_client: Any | None = None,\n        user: Mapping[str, Any] | None = None,\n    ) -> Resource:\n        caption_text = caption.strip() if caption else None\n        if caption_text:\n            client = embed_client or self._get_llm_client()\n            caption_embedding = (await client.embed([caption_text]))[0]\n        else:\n            caption_embedding = None\n\n        res = store.resource_repo.create_resource(\n            url=resource_url,\n            modality=modality,\n            local_path=local_path,\n            caption=caption_text,\n            embedding=caption_embedding,\n            user_data=dict(user or {}),\n        )\n        # if caption:\n        #     caption_text = caption.strip()\n        #     if caption_text:\n        #         res.caption = caption_text\n        #         client = embed_client or self._get_llm_client()\n        #         res.embedding = (await client.embed([caption_text]))[0]\n        #         res.updated_at = pendulum.now()\n        return res\n\n    def _resolve_memory_types(self) -> list[MemoryType]:\n        configured_types = self.memorize_config.memory_types or DEFAULT_MEMORY_TYPES\n        return [cast(MemoryType, mtype) for mtype in configured_types]\n\n    def _resolve_summary_prompt(self, modality: str, override: str | None) -> str | None:\n        memo_settings = self.memorize_config\n        result = memo_settings.multimodal_preprocess_prompts.get(modality)\n        if override:\n            return override\n        if result is None:\n            return (\n                memo_settings.default_category_summary_prompt\n                if isinstance(memo_settings.default_category_summary_prompt, str)\n                else None\n            )\n        return result if isinstance(result, str) else None\n\n    def _resolve_multimodal_preprocess_prompt(self, modality: str) -> str | None:\n        memo_settings = self.memorize_config\n        result = memo_settings.multimodal_preprocess_prompts.get(modality)\n        return result if isinstance(result, str) else None\n\n    @staticmethod\n    def _resolve_custom_prompt(prompt: str | CustomPrompt, templates: Mapping[str, str]) -> str:\n        if isinstance(prompt, str):\n            return prompt\n        valid_blocks = [\n            (block.ordinal, name, block.prompt or templates.get(name))\n            for name, block in prompt.items()\n            if (block.ordinal >= 0 and (block.prompt or templates.get(name)))\n        ]\n        if not valid_blocks:\n            # raise ValueError(f\"No valid blocks contained in custom prompt: {prompt}\")\n            return \"\"\n        sorted_blocks = sorted(valid_blocks)\n        return \"\\n\\n\".join(block for (_, _, block) in sorted_blocks if block is not None)\n\n    async def _generate_structured_entries(\n        self,\n        *,\n        resource_url: str,\n        modality: str,\n        memory_types: list[MemoryType],\n        text: str | None,\n        categories_prompt_str: str,\n        segments: list[dict[str, int | str]] | None = None,\n        llm_client: Any | None = None,\n    ) -> list[tuple[MemoryType, str, list[str]]]:\n        if not memory_types:\n            return []\n\n        client = llm_client or self._get_llm_client()\n        if text:\n            entries = await self._generate_text_entries(\n                resource_text=text,\n                modality=modality,\n                memory_types=memory_types,\n                categories_prompt_str=categories_prompt_str,\n                segments=segments,\n                llm_client=client,\n            )\n            return entries\n            # if entries:\n            #     return entries\n            # no_result_entry = self._build_no_result_fallback(memory_types[0], resource_url, modality)\n            # return [no_result_entry]\n\n        return []\n        # return self._build_no_text_fallback(memory_types, resource_url, modality)\n\n    async def _generate_text_entries(\n        self,\n        *,\n        resource_text: str,\n        modality: str,\n        memory_types: list[MemoryType],\n        categories_prompt_str: str,\n        segments: list[dict[str, int | str]] | None,\n        llm_client: Any | None = None,\n    ) -> list[tuple[MemoryType, str, list[str]]]:\n        if modality == \"conversation\" and segments:\n            segment_entries = await self._generate_entries_for_segments(\n                resource_text=resource_text,\n                segments=segments,\n                memory_types=memory_types,\n                categories_prompt_str=categories_prompt_str,\n                llm_client=llm_client,\n            )\n            if segment_entries:\n                return segment_entries\n        return await self._generate_entries_from_text(\n            resource_text=resource_text,\n            memory_types=memory_types,\n            categories_prompt_str=categories_prompt_str,\n            llm_client=llm_client,\n        )\n\n    async def _generate_entries_for_segments(\n        self,\n        *,\n        resource_text: str,\n        segments: list[dict[str, int | str]],\n        memory_types: list[MemoryType],\n        categories_prompt_str: str,\n        llm_client: Any | None = None,\n    ) -> list[tuple[MemoryType, str, list[str]]]:\n        entries: list[tuple[MemoryType, str, list[str]]] = []\n        lines = resource_text.split(\"\\n\")\n        max_idx = len(lines) - 1\n        for segment in segments:\n            start_idx = int(segment.get(\"start\", 0))\n            end_idx = int(segment.get(\"end\", max_idx))\n            segment_text = self._extract_segment_text(lines, start_idx, end_idx)\n            if not segment_text:\n                continue\n            segment_entries = await self._generate_entries_from_text(\n                resource_text=segment_text,\n                memory_types=memory_types,\n                categories_prompt_str=categories_prompt_str,\n                llm_client=llm_client,\n            )\n            entries.extend(segment_entries)\n        return entries\n\n    async def _generate_entries_from_text(\n        self,\n        *,\n        resource_text: str,\n        memory_types: list[MemoryType],\n        categories_prompt_str: str,\n        llm_client: Any | None = None,\n    ) -> list[tuple[MemoryType, str, list[str]]]:\n        if not memory_types:\n            return []\n        client = llm_client or self._get_llm_client()\n        prompts = [\n            self._build_memory_type_prompt(\n                memory_type=mtype,\n                resource_text=resource_text,\n                categories_str=categories_prompt_str,\n            )\n            for mtype in memory_types\n        ]\n        valid_prompts = [prompt for prompt in prompts if prompt.strip()]\n        # These prompts are instructions that request structured output, not text summaries.\n        tasks = [client.chat(prompt_text) for prompt_text in valid_prompts]\n        responses = await asyncio.gather(*tasks)\n        return self._parse_structured_entries(memory_types, responses)\n\n    def _parse_structured_entries(\n        self, memory_types: list[MemoryType], responses: Sequence[str]\n    ) -> list[tuple[MemoryType, str, list[str]]]:\n        entries: list[tuple[MemoryType, str, list[str]]] = []\n        for mtype, response in zip(memory_types, responses, strict=True):\n            parsed = self._parse_memory_type_response_xml(response)\n            # if not parsed:\n            #     fallback_entry = response.strip()\n            #     if fallback_entry:\n            #         entries.append((mtype, fallback_entry, []))\n            #     continue\n            for entry in parsed:\n                content = (entry.get(\"content\") or \"\").strip()\n                if not content:\n                    continue\n                cat_names = [c.strip() for c in entry.get(\"categories\", []) if isinstance(c, str) and c.strip()]\n                entries.append((mtype, content, cat_names))\n        return entries\n\n    def _extract_segment_text(self, lines: list[str], start_idx: int, end_idx: int) -> str | None:\n        segment_lines = []\n        for line in lines:\n            match = re.match(r\"\\[(\\d+)\\]\", line)\n            if not match:\n                continue\n            idx = int(match.group(1))\n            if start_idx <= idx <= end_idx:\n                segment_lines.append(line)\n        return \"\\n\".join(segment_lines) if segment_lines else None\n\n    def _build_no_text_fallback(\n        self, memory_types: list[MemoryType], resource_url: str, modality: str\n    ) -> list[tuple[MemoryType, str, list[str]]]:\n        fallback = f\"Resource {resource_url} ({modality}) stored. No text summary in v0.\"\n        return [(mtype, f\"{fallback} (memory type: {mtype}).\", []) for mtype in memory_types]\n\n    def _build_no_result_fallback(\n        self, memory_type: MemoryType, resource_url: str, modality: str\n    ) -> tuple[MemoryType, str, list[str]]:\n        fallback = f\"Resource {resource_url} ({modality}) stored. No structured memories generated.\"\n        return memory_type, fallback, []\n\n    async def _persist_memory_items(\n        self,\n        *,\n        resource_id: str,\n        structured_entries: list[tuple[MemoryType, str, list[str]]],\n        ctx: Context,\n        store: Database,\n        embed_client: Any | None = None,\n        user: Mapping[str, Any] | None = None,\n    ) -> tuple[list[MemoryItem], list[CategoryItem], dict[str, list[tuple[str, str]]]]:\n        \"\"\"\n        Persist memory items and track category updates.\n\n        Returns:\n            Tuple of (items, relations, category_updates)\n            where category_updates maps category_id -> list of (item_id, summary) tuples\n        \"\"\"\n        summary_payloads = [content for _, content, _ in structured_entries]\n        client = embed_client or self._get_llm_client()\n        item_embeddings = await client.embed(summary_payloads) if summary_payloads else []\n        items: list[MemoryItem] = []\n        rels: list[CategoryItem] = []\n        # Changed: now stores (item_id, summary) tuples for reference support\n        category_memory_updates: dict[str, list[tuple[str, str]]] = {}\n\n        reinforce = self.memorize_config.enable_item_reinforcement\n        for (memory_type, summary_text, cat_names), emb in zip(structured_entries, item_embeddings, strict=True):\n            item = store.memory_item_repo.create_item(\n                resource_id=resource_id,\n                memory_type=memory_type,\n                summary=summary_text,\n                embedding=emb,\n                user_data=dict(user or {}),\n                reinforce=reinforce,\n            )\n            items.append(item)\n            if reinforce and item.extra.get(\"reinforcement_count\", 1) > 1:\n                # existing item\n                continue\n            mapped_cat_ids = self._map_category_names_to_ids(cat_names, ctx)\n            for cid in mapped_cat_ids:\n                rels.append(store.category_item_repo.link_item_category(item.id, cid, user_data=dict(user or {})))\n                # Store (item_id, summary) tuple for reference support\n                category_memory_updates.setdefault(cid, []).append((item.id, summary_text))\n\n        return items, rels, category_memory_updates\n\n    def _start_category_initialization(self, ctx: Context, store: Database) -> None:\n        if ctx.categories_ready:\n            return\n        try:\n            loop = asyncio.get_running_loop()\n        except RuntimeError:\n            loop = None\n        if loop:\n            ctx.category_init_task = loop.create_task(self._initialize_categories(ctx, store))\n        else:\n            asyncio.run(self._initialize_categories(ctx, store))\n\n    async def _ensure_categories_ready(\n        self, ctx: Context, store: Database, user_scope: Mapping[str, Any] | None = None\n    ) -> None:\n        if ctx.categories_ready:\n            return\n        if ctx.category_init_task:\n            await ctx.category_init_task\n            ctx.category_init_task = None\n            return\n        await self._initialize_categories(ctx, store, user_scope)\n\n    async def _initialize_categories(\n        self, ctx: Context, store: Database, user: Mapping[str, Any] | None = None\n    ) -> None:\n        if ctx.categories_ready:\n            return\n        if not self.category_configs:\n            ctx.categories_ready = True\n            return\n        cat_texts = [self._category_embedding_text(cfg) for cfg in self.category_configs]\n        cat_vecs = await self._get_llm_client(\"embedding\").embed(cat_texts)\n        ctx.category_ids = []\n        ctx.category_name_to_id = {}\n        for cfg, vec in zip(self.category_configs, cat_vecs, strict=True):\n            name = cfg.name.strip() or \"Untitled\"\n            description = cfg.description.strip()\n            cat = store.memory_category_repo.get_or_create_category(\n                name=name, description=description, embedding=vec, user_data=dict(user or {})\n            )\n            ctx.category_ids.append(cat.id)\n            ctx.category_name_to_id[name.lower()] = cat.id\n        ctx.categories_ready = True\n\n    @staticmethod\n    def _category_embedding_text(cat: CategoryConfig) -> str:\n        name = cat.name.strip() or \"Untitled\"\n        desc = cat.description.strip()\n        return f\"{name}: {desc}\" if desc else name\n\n    def _map_category_names_to_ids(self, names: list[str], ctx: Context) -> list[str]:\n        if not names:\n            return []\n        mapped: list[str] = []\n        seen: set[str] = set()\n        for name in names:\n            key = name.strip().lower()\n            cid = ctx.category_name_to_id.get(key)\n            if cid and cid not in seen:\n                mapped.append(cid)\n                seen.add(cid)\n        return mapped\n\n    async def _preprocess_resource_url(\n        self, *, local_path: str, text: str | None, modality: str, llm_client: Any | None = None\n    ) -> list[dict[str, str | None]]:\n        \"\"\"\n        Preprocess resource based on modality.\n\n        General preprocessing dispatcher for all modalities:\n        - Text-based modalities (conversation, document): require text content\n        - Audio modality: transcribe audio file first, then process as text\n        - Media modalities (video, image): process media files directly\n\n        Args:\n            local_path: Local file path to the resource\n            text: Text content if available (for text-based modalities)\n            modality: Resource modality type\n\n        Returns:\n            List of preprocessed resources, each with 'text' and 'caption'\n        \"\"\"\n        configured_prompt = self.memorize_config.multimodal_preprocess_prompts.get(modality)\n        if configured_prompt is None:\n            template = PREPROCESS_PROMPTS.get(modality)\n        elif isinstance(configured_prompt, str):\n            template = configured_prompt\n        else:\n            # No custom prompts configured for preprocssing for now,\n            # If the user decide to use their custom prompt, they must provide ALL prompt blocks.\n            template = self._resolve_custom_prompt(configured_prompt, {})\n\n        if not template:\n            return [{\"text\": text, \"caption\": None}]\n\n        if modality == \"audio\":\n            text = await self._prepare_audio_text(local_path, text, llm_client=llm_client)\n            if text is None:\n                return [{\"text\": None, \"caption\": None}]\n\n        if self._modality_requires_text(modality) and not text:\n            return [{\"text\": text, \"caption\": None}]\n\n        return await self._dispatch_preprocessor(\n            modality=modality,\n            local_path=local_path,\n            text=text,\n            template=template,\n            llm_client=llm_client,\n        )\n\n    async def _prepare_audio_text(self, local_path: str, text: str | None, llm_client: Any | None = None) -> str | None:\n        \"\"\"Ensure audio resources provide text either via transcription or file read.\"\"\"\n        if text:\n            return text\n\n        audio_extensions = {\".mp3\", \".mp4\", \".mpeg\", \".mpga\", \".m4a\", \".wav\", \".webm\"}\n        text_extensions = {\".txt\", \".text\"}\n        file_ext = pathlib.Path(local_path).suffix.lower()\n\n        if file_ext in audio_extensions:\n            try:\n                logger.info(f\"Transcribing audio file: {local_path}\")\n                client = llm_client or self._get_llm_client()\n                transcribed = cast(str, await client.transcribe(local_path))\n                logger.info(f\"Audio transcription completed: {len(transcribed)} characters\")\n            except Exception:\n                logger.exception(\"Audio transcription failed for %s\", local_path)\n                return None\n            else:\n                return transcribed\n\n        if file_ext in text_extensions:\n            path_obj = pathlib.Path(local_path)\n            try:\n                text_content = path_obj.read_text(encoding=\"utf-8\")\n                logger.info(f\"Read pre-transcribed text file: {len(text_content)} characters\")\n            except Exception:\n                logger.exception(\"Failed to read text file %s\", local_path)\n                return None\n            else:\n                return text_content\n\n        logger.warning(f\"Unknown audio file type: {file_ext}, skipping transcription\")\n        return None\n\n    def _modality_requires_text(self, modality: str) -> bool:\n        return modality in (\"conversation\", \"document\")\n\n    async def _dispatch_preprocessor(\n        self,\n        *,\n        modality: str,\n        local_path: str,\n        text: str | None,\n        template: str,\n        llm_client: Any | None = None,\n    ) -> list[dict[str, str | None]]:\n        if modality == \"conversation\" and text is not None:\n            return await self._preprocess_conversation(text, template, llm_client=llm_client)\n        if modality == \"video\":\n            return await self._preprocess_video(local_path, template, llm_client=llm_client)\n        if modality == \"image\":\n            return await self._preprocess_image(local_path, template, llm_client=llm_client)\n        if modality == \"document\" and text is not None:\n            return await self._preprocess_document(text, template, llm_client=llm_client)\n        if modality == \"audio\" and text is not None:\n            return await self._preprocess_audio(text, template, llm_client=llm_client)\n        return [{\"text\": text, \"caption\": None}]\n\n    async def _preprocess_conversation(\n        self, text: str, template: str, llm_client: Any | None = None\n    ) -> list[dict[str, str | None]]:\n        \"\"\"Preprocess conversation data with segmentation, returns list of resources (one per segment).\"\"\"\n        preprocessed_text = format_conversation_for_preprocess(text)\n        prompt = template.format(conversation=self._escape_prompt_value(preprocessed_text))\n        client = llm_client or self._get_llm_client()\n        processed = await client.chat(prompt)\n        _conv, segments = self._parse_conversation_preprocess_with_segments(processed, preprocessed_text)\n\n        # Important: always use the original JSON-derived, indexed conversation text for downstream\n        # segmentation and memory extraction. The LLM may rewrite the conversation and drop fields\n        # like created_at, which would cause them to be lost.\n        conversation_text = preprocessed_text\n        # If no segments, return single resource\n        if not segments:\n            return [{\"text\": conversation_text, \"caption\": None}]\n\n        # Generate caption for each segment and return as separate resources\n        lines = conversation_text.split(\"\\n\")\n        max_idx = len(lines) - 1\n        resources: list[dict[str, str | None]] = []\n\n        for segment in segments:\n            start = int(segment.get(\"start\", 0))\n            end = int(segment.get(\"end\", max_idx))\n            start = max(0, min(start, max_idx))\n            end = max(0, min(end, max_idx))\n            segment_text = \"\\n\".join(lines[start : end + 1])\n\n            if segment_text.strip():\n                caption = await self._summarize_segment(segment_text, llm_client=client)\n                resources.append({\"text\": segment_text, \"caption\": caption})\n        return resources if resources else [{\"text\": conversation_text, \"caption\": None}]\n\n    async def _summarize_segment(self, segment_text: str, llm_client: Any | None = None) -> str | None:\n        \"\"\"Summarize a single conversation segment.\"\"\"\n        system_prompt = (\n            \"Summarize the given conversation segment in 1-2 concise sentences. \"\n            \"Focus on the main topic or theme discussed.\"\n        )\n        try:\n            client = llm_client or self._get_llm_client()\n            response = await client.chat(segment_text, system_prompt=system_prompt)\n            return response.strip() if response else None\n        except Exception:\n            logger.exception(\"Failed to summarize segment\")\n            return None\n\n    async def _preprocess_video(\n        self, local_path: str, template: str, llm_client: Any | None = None\n    ) -> list[dict[str, str | None]]:\n        \"\"\"\n        Preprocess video data - extract description and caption using Vision API.\n\n        Extracts the middle frame from the video and analyzes it using Vision API.\n\n        Args:\n            local_path: Path to the video file\n            template: Prompt template for video analysis\n\n        Returns:\n            List with single resource containing text (description) and caption\n        \"\"\"\n        try:\n            # Check if ffmpeg is available\n            if not VideoFrameExtractor.is_ffmpeg_available():\n                logger.warning(\"ffmpeg not available, cannot process video. Returning None.\")\n                return [{\"text\": None, \"caption\": None}]\n\n            # Extract middle frame from video\n            logger.info(f\"Extracting frame from video: {local_path}\")\n            frame_path = VideoFrameExtractor.extract_middle_frame(local_path)\n\n            try:\n                # Call Vision API with extracted frame\n                logger.info(f\"Analyzing video frame with Vision API: {frame_path}\")\n                client = llm_client or self._get_llm_client()\n                processed = await client.vision(prompt=template, image_path=frame_path, system_prompt=None)\n                description, caption = self._parse_multimodal_response(processed, \"detailed_description\", \"caption\")\n                return [{\"text\": description, \"caption\": caption}]\n            finally:\n                # Clean up temporary frame file\n                import pathlib\n\n                try:\n                    pathlib.Path(frame_path).unlink(missing_ok=True)\n                    logger.debug(f\"Cleaned up temporary frame: {frame_path}\")\n                except Exception as e:\n                    logger.warning(f\"Failed to clean up frame {frame_path}: {e}\")\n\n        except Exception as e:\n            logger.error(f\"Video preprocessing failed: {e}\", exc_info=True)\n            return [{\"text\": None, \"caption\": None}]\n\n    async def _preprocess_image(\n        self, local_path: str, template: str, llm_client: Any | None = None\n    ) -> list[dict[str, str | None]]:\n        \"\"\"\n        Preprocess image data - extract description and caption using Vision API.\n\n        Args:\n            local_path: Path to the image file\n            template: Prompt template for image analysis\n\n        Returns:\n            List with single resource containing text (description) and caption\n        \"\"\"\n        # Call Vision API with image\n        client = llm_client or self._get_llm_client()\n        processed = await client.vision(prompt=template, image_path=local_path, system_prompt=None)\n        description, caption = self._parse_multimodal_response(processed, \"detailed_description\", \"caption\")\n        return [{\"text\": description, \"caption\": caption}]\n\n    async def _preprocess_document(\n        self, text: str, template: str, llm_client: Any | None = None\n    ) -> list[dict[str, str | None]]:\n        \"\"\"Preprocess document data - condense and extract caption\"\"\"\n        prompt = template.format(document_text=self._escape_prompt_value(text))\n        client = llm_client or self._get_llm_client()\n        processed = await client.chat(prompt)\n        processed_content, caption = self._parse_multimodal_response(processed, \"processed_content\", \"caption\")\n        return [{\"text\": processed_content or text, \"caption\": caption}]\n\n    async def _preprocess_audio(\n        self, text: str, template: str, llm_client: Any | None = None\n    ) -> list[dict[str, str | None]]:\n        \"\"\"Preprocess audio data - format transcription and extract caption\"\"\"\n        prompt = template.format(transcription=self._escape_prompt_value(text))\n        client = llm_client or self._get_llm_client()\n        processed = await client.chat(prompt)\n        processed_content, caption = self._parse_multimodal_response(processed, \"processed_content\", \"caption\")\n        return [{\"text\": processed_content or text, \"caption\": caption}]\n\n    def _format_categories_for_prompt(self, categories: list[CategoryConfig]) -> str:\n        if not categories:\n            return \"No categories provided.\"\n        lines = []\n        for cat in categories:\n            name = cat.name.strip() or \"Untitled\"\n            desc = cat.description.strip()\n            lines.append(f\"- {name}: {desc}\" if desc else f\"- {name}\")\n        return \"\\n\".join(lines)\n\n    def _add_conversation_indices(self, conversation: str) -> str:\n        \"\"\"\n        Add [INDEX] markers to each line of the conversation.\n\n        Args:\n            conversation: Raw conversation text with lines\n\n        Returns:\n            Conversation with [INDEX] markers prepended to each non-empty line\n        \"\"\"\n        lines = conversation.split(\"\\n\")\n        indexed_lines = []\n        index = 0\n\n        for line in lines:\n            stripped = line.strip()\n            if stripped:  # Only index non-empty lines\n                indexed_lines.append(f\"[{index}] {line}\")\n                index += 1\n            else:\n                # Preserve empty lines without indexing\n                indexed_lines.append(line)\n\n        return \"\\n\".join(indexed_lines)\n\n    def _build_memory_type_prompt(self, *, memory_type: MemoryType, resource_text: str, categories_str: str) -> str:\n        configured_prompt = self.memorize_config.memory_type_prompts.get(memory_type)\n        if configured_prompt is None:\n            template = MEMORY_TYPE_PROMPTS.get(memory_type)\n        elif isinstance(configured_prompt, str):\n            template = configured_prompt\n        else:\n            template = self._resolve_custom_prompt(\n                configured_prompt, MEMORY_TYPE_CUSTOM_PROMPTS.get(memory_type, CUSTOM_TYPE_CUSTOM_PROMPTS)\n            )\n        if not template:\n            return resource_text\n        safe_resource = self._escape_prompt_value(resource_text)\n        safe_categories = self._escape_prompt_value(categories_str)\n        return template.format(resource=safe_resource, categories_str=safe_categories)\n\n    def _build_item_ref_id(self, item_id: str) -> str:\n        return item_id.replace(\"-\", \"\")[:6]\n\n    def _extract_refs_from_summaries(self, summaries: dict[str, str]) -> set[str]:\n        \"\"\"\n        Extract all [ref:xxx] references from summary texts.\n\n        Args:\n            summaries: dict mapping category_id -> summary text\n\n        Returns:\n            Set of all referenced short IDs (the xxx part from [ref:xxx])\n        \"\"\"\n        from memu.utils.references import extract_references\n\n        refs: set[str] = set()\n        for summary in summaries.values():\n            refs.update(extract_references(summary))\n        return refs\n\n    async def _persist_item_references(\n        self,\n        *,\n        updated_summaries: dict[str, str],\n        category_updates: dict[str, list[tuple[str, str]]],\n        store: Database,\n    ) -> None:\n        \"\"\"\n        Persist ref_id to items that are referenced in category summaries.\n\n        This function:\n        1. Extracts all [ref:xxx] patterns from updated summaries\n        2. Builds a mapping of short_id -> full item_id for all items in category_updates\n        3. For items whose short_id appears in the references, updates their extra column\n           with {\"ref_id\": short_id}\n        \"\"\"\n        # Extract all referenced short IDs from summaries\n        referenced_short_ids = self._extract_refs_from_summaries(updated_summaries)\n        if not referenced_short_ids:\n            return\n\n        # Build mapping of short_id -> full item_id for all items in category_updates\n        short_id_to_item_id: dict[str, str] = {}\n        for item_tuples in category_updates.values():\n            for item_id, _ in item_tuples:\n                short_id = self._build_item_ref_id(item_id)\n                short_id_to_item_id[short_id] = item_id\n\n        # Update extra column for referenced items\n        for short_id in referenced_short_ids:\n            matched_item_id = short_id_to_item_id.get(short_id)\n            if matched_item_id:\n                store.memory_item_repo.update_item(\n                    item_id=matched_item_id,\n                    extra={\"ref_id\": short_id},\n                )\n\n    def _build_category_summary_prompt(\n        self,\n        *,\n        category: MemoryCategory,\n        new_memories: list[str] | list[tuple[str, str]],\n    ) -> str:\n        \"\"\"\n        Build the prompt for updating a category summary.\n\n        Args:\n            category: The category to update\n            new_memories: Either list of summary strings (legacy) or list of (item_id, summary) tuples (with refs)\n        \"\"\"\n        # Check if references are enabled and we have (id, summary) tuples\n        enable_refs = getattr(self.memorize_config, \"enable_item_references\", False)\n\n        if enable_refs:\n            from memu.prompts.category_summary import (\n                CUSTOM_PROMPT_WITH_REFS as category_summary_custom_prompt,\n            )\n            from memu.prompts.category_summary import (\n                PROMPT_WITH_REFS as category_summary_prompt,\n            )\n\n            tuple_memories = cast(list[tuple[str, str]], new_memories)\n            new_items_text = \"\\n\".join(\n                f\"- [{self._build_item_ref_id(item_id)}] {summary}\"\n                for item_id, summary in tuple_memories\n                if summary.strip()\n            )\n        else:\n            category_summary_prompt = CATEGORY_SUMMARY_PROMPT\n            category_summary_custom_prompt = CATEGORY_SUMMARY_CUSTOM_PROMPT\n\n            if new_memories and isinstance(new_memories[0], tuple):\n                tuple_memories = cast(list[tuple[str, str]], new_memories)\n                new_items_text = \"\\n\".join(f\"- {summary}\" for item_id, summary in tuple_memories if summary.strip())\n            else:\n                str_memories = cast(list[str], new_memories)\n                new_items_text = \"\\n\".join(f\"- {m}\" for m in str_memories if m.strip())\n\n        original = category.summary or \"\"\n        category_config = self.category_config_map.get(category.name)\n        configured_prompt = (\n            category_config and category_config.summary_prompt\n        ) or self.memorize_config.default_category_summary_prompt\n        if configured_prompt is None:\n            prompt = category_summary_prompt\n        elif isinstance(configured_prompt, str):\n            prompt = configured_prompt\n        else:\n            prompt = self._resolve_custom_prompt(configured_prompt, category_summary_custom_prompt)\n        target_length = (\n            category_config and category_config.target_length\n        ) or self.memorize_config.default_category_summary_target_length\n        return prompt.format(\n            category=self._escape_prompt_value(category.name),\n            original_content=self._escape_prompt_value(original or \"\"),\n            new_memory_items_text=self._escape_prompt_value(new_items_text or \"No new memory items.\"),\n            target_length=target_length,\n        )\n\n    async def _update_category_summaries(\n        self,\n        updates: dict[str, list[tuple[str, str]]] | dict[str, list[str]],\n        ctx: Context,\n        store: Database,\n        llm_client: Any | None = None,\n    ) -> dict[str, str]:\n        \"\"\"\n        Update category summaries based on new memory items.\n\n        Returns:\n            dict mapping category_id -> updated summary text\n        \"\"\"\n        updated_summaries: dict[str, str] = {}\n        if not updates:\n            return updated_summaries\n        tasks = []\n        target_ids: list[str] = []\n        client = llm_client or self._get_llm_client()\n        for cid, memories in updates.items():\n            cat = store.memory_category_repo.categories.get(cid)\n            if not cat or not memories:\n                continue\n            prompt = self._build_category_summary_prompt(category=cat, new_memories=memories)\n            tasks.append(client.chat(prompt))\n            target_ids.append(cid)\n        if not tasks:\n            return updated_summaries\n        summaries = await asyncio.gather(*tasks)\n        for cid, summary in zip(target_ids, summaries, strict=True):\n            cat = store.memory_category_repo.categories.get(cid)\n            if not cat:\n                continue\n            cleaned_summary = summary.replace(\"```markdown\", \"\").replace(\"```\", \"\").strip()\n            store.memory_category_repo.update_category(\n                category_id=cid,\n                summary=cleaned_summary,\n            )\n            updated_summaries[cid] = cleaned_summary\n        return updated_summaries\n\n    def _parse_conversation_preprocess(self, raw: str) -> tuple[str | None, str | None]:\n        conversation = self._extract_tag_content(raw, \"conversation\")\n        summary = self._extract_tag_content(raw, \"summary\")\n        return conversation, summary\n\n    def _parse_multimodal_response(self, raw: str, content_tag: str, caption_tag: str) -> tuple[str | None, str | None]:\n        \"\"\"\n        Parse multimodal preprocessing response (video, image, document, audio).\n        Extracts content and caption from XML-like tags.\n\n        Args:\n            raw: Raw LLM response\n            content_tag: Tag name for main content (e.g., \"detailed_description\", \"processed_content\")\n            caption_tag: Tag name for caption (typically \"caption\")\n\n        Returns:\n            Tuple of (content, caption)\n        \"\"\"\n        content = self._extract_tag_content(raw, content_tag)\n        caption = self._extract_tag_content(raw, caption_tag)\n\n        # Fallback: if no tags found, try to use raw response as content\n        if not content:\n            content = raw.strip()\n\n        # Fallback for caption: use first sentence of content if no caption found\n        if not caption and content:\n            first_sentence = content.split(\".\")[0]\n            caption = first_sentence if len(first_sentence) <= 200 else first_sentence[:200]\n\n        return content, caption\n\n    def _parse_conversation_preprocess_with_segments(\n        self, raw: str, original_text: str\n    ) -> tuple[str | None, list[dict[str, int | str]] | None]:\n        \"\"\"\n        Parse conversation preprocess response and extract segments.\n        Returns: (conversation_text, segments)\n        \"\"\"\n        conversation = self._extract_tag_content(raw, \"conversation\")\n        segments = self._extract_segments_with_fallback(raw)\n        return conversation, segments\n\n    def _extract_segments_with_fallback(self, raw: str) -> list[dict[str, int | str]] | None:\n        segments = self._segments_from_json_payload(raw)\n        if segments is not None:\n            return segments\n        try:\n            blob = self._extract_json_blob(raw)\n        except Exception:\n            logging.exception(\"Failed to extract segments from conversation preprocess response\")\n            return None\n        return self._segments_from_json_payload(blob)\n\n    def _segments_from_json_payload(self, payload: str) -> list[dict[str, int | str]] | None:\n        try:\n            parsed = json.loads(payload)\n        except (json.JSONDecodeError, TypeError):\n            return None\n        return self._segments_from_parsed_data(parsed)\n\n    @staticmethod\n    def _segments_from_parsed_data(parsed: Any) -> list[dict[str, int | str]] | None:\n        if not isinstance(parsed, dict):\n            return None\n        segments_data = parsed.get(\"segments\")\n        if not isinstance(segments_data, list):\n            return None\n        segments: list[dict[str, int | str]] = []\n        for seg in segments_data:\n            if isinstance(seg, dict) and \"start\" in seg and \"end\" in seg:\n                try:\n                    segment: dict[str, int | str] = {\n                        \"start\": int(seg[\"start\"]),\n                        \"end\": int(seg[\"end\"]),\n                    }\n                    if \"caption\" in seg and isinstance(seg[\"caption\"], str):\n                        segment[\"caption\"] = seg[\"caption\"]\n                    segments.append(segment)\n                except (TypeError, ValueError):\n                    continue\n        return segments or None\n\n    @staticmethod\n    def _extract_tag_content(raw: str, tag: str) -> str | None:\n        pattern = re.compile(rf\"<{tag}>(.*?)</{tag}>\", re.IGNORECASE | re.DOTALL)\n        match = pattern.search(raw)\n        if not match:\n            return None\n        content = match.group(1).strip()\n        return content or None\n\n    def _parse_memory_type_response(self, raw: str) -> list[dict[str, Any]]:\n        if not raw:\n            return []\n        raw = raw.strip()\n        if not raw:\n            return []\n        payload = None\n        try:\n            payload = json.loads(raw)\n        except json.JSONDecodeError:\n            try:\n                blob = self._extract_json_blob(raw)\n                payload = json.loads(blob)\n            except Exception:\n                return []\n        if not isinstance(payload, dict):\n            return []\n        items = payload.get(\"memories_items\")\n        if not isinstance(items, list):\n            return []\n        normalized: list[dict[str, Any]] = []\n        for entry in items:\n            if not isinstance(entry, dict):\n                continue\n            normalized.append(entry)\n        return normalized\n\n    def _find_xml_boundaries(self, raw: str) -> tuple[int, int, str] | None:\n        \"\"\"Find the start index, end index, and closing tag for XML root element.\"\"\"\n        root_tags = [\"item\", \"profile\", \"behaviors\", \"events\", \"knowledge\", \"skills\"]\n        for tag in root_tags:\n            opening = f\"<{tag}>\"\n            closing = f\"</{tag}>\"\n            start_idx = raw.find(opening)\n            if start_idx != -1:\n                end_idx = raw.rfind(closing)\n                if end_idx != -1:\n                    return (start_idx, end_idx, closing)\n        return None\n\n    def _parse_memory_element(self, memory_elem: Element) -> dict[str, Any] | None:\n        \"\"\"Parse a single memory XML element into a dict.\"\"\"\n        memory_dict: dict[str, Any] = {}\n\n        content_elem = memory_elem.find(\"content\")\n        if content_elem is not None and content_elem.text:\n            memory_dict[\"content\"] = content_elem.text.strip()\n\n        categories_elem = memory_elem.find(\"categories\")\n        if categories_elem is not None:\n            categories = [cat_elem.text.strip() for cat_elem in categories_elem.findall(\"category\") if cat_elem.text]\n            memory_dict[\"categories\"] = categories\n\n        if memory_dict.get(\"content\") and memory_dict.get(\"categories\"):\n            return memory_dict\n        return None\n\n    def _parse_memory_type_response_xml(self, raw: str) -> list[dict[str, Any]]:\n        \"\"\"\n        Parse XML memory extraction output into a list of memory items.\n\n        Expected XML format (root tag varies by memory type):\n        <profile|behaviors|events|knowledge|skills>\n            <memory>\n                <content>...</content>\n                <categories>\n                    <category>...</category>\n                </categories>\n            </memory>\n        </...>\n        \"\"\"\n        if not raw or not raw.strip():\n            return []\n        raw = raw.strip()\n\n        try:\n            boundaries = self._find_xml_boundaries(raw)\n            if boundaries is None:\n                logger.warning(\"Could not find valid root tag in XML response\")\n                return []\n\n            start_idx, end_idx, end_tag = boundaries\n            xml_content = raw[start_idx : end_idx + len(end_tag)]\n            xml_content = xml_content.replace(\"&\", \"&amp;\")\n\n            root = ET.fromstring(xml_content)\n            result: list[dict[str, Any]] = []\n\n            for memory_elem in root.findall(\"memory\"):\n                parsed = self._parse_memory_element(memory_elem)\n                if parsed:\n                    result.append(parsed)\n\n        except ET.ParseError:\n            logger.exception(\"Failed to parse XML\")\n            return []\n        else:\n            return result\n"
  },
  {
    "path": "src/memu/app/patch.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nfrom collections.abc import Awaitable, Callable, Mapping\nfrom typing import TYPE_CHECKING, Any, cast, get_args\n\nfrom pydantic import BaseModel\n\nfrom memu.database.models import MemoryCategory, MemoryType\nfrom memu.prompts.category_patch import CATEGORY_PATCH_PROMPT\nfrom memu.workflow.step import WorkflowState, WorkflowStep\n\nlogger = logging.getLogger(__name__)\n\nif TYPE_CHECKING:\n    from memu.app.service import Context\n    from memu.app.settings import PatchConfig\n    from memu.database.interfaces import Database\n\n\nclass PatchMixin:\n    if TYPE_CHECKING:\n        patch_config: PatchConfig\n        _run_workflow: Callable[..., Awaitable[WorkflowState]]\n        _get_context: Callable[[], Context]\n        _get_database: Callable[[], Database]\n        _get_step_llm_client: Callable[[Mapping[str, Any] | None], Any]\n        _get_llm_client: Callable[..., Any]\n        _model_dump_without_embeddings: Callable[[BaseModel], dict[str, Any]]\n        _extract_json_blob: Callable[[str], str]\n        _escape_prompt_value: Callable[[str], str]\n        user_model: type[BaseModel]\n        _ensure_categories_ready: Callable[[Context, Database, Mapping[str, Any] | None], Awaitable[None]]\n\n    async def create_memory_item(\n        self,\n        *,\n        memory_type: MemoryType,\n        memory_content: str,\n        memory_categories: list[str],\n        user: dict[str, Any] | None = None,\n        propagate: bool = True,\n    ) -> dict[str, Any]:\n        if memory_type not in get_args(MemoryType):\n            msg = f\"Invalid memory type: '{memory_type}', must be one of {get_args(MemoryType)}\"\n            raise ValueError(msg)\n\n        ctx = self._get_context()\n        store = self._get_database()\n        user_scope = self.user_model(**user).model_dump() if user is not None else None\n        await self._ensure_categories_ready(ctx, store, user_scope)\n\n        state: WorkflowState = {\n            \"memory_payload\": {\n                \"type\": memory_type,\n                \"content\": memory_content,\n                \"categories\": memory_categories,\n            },\n            \"ctx\": ctx,\n            \"store\": store,\n            \"category_ids\": list(ctx.category_ids),\n            \"user\": user_scope,\n            \"propagate\": propagate,\n        }\n\n        result = await self._run_workflow(\"patch_create\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Create memory item workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    async def update_memory_item(\n        self,\n        *,\n        memory_id: str,\n        memory_type: MemoryType | None = None,\n        memory_content: str | None = None,\n        memory_categories: list[str] | None = None,\n        user: dict[str, Any] | None = None,\n        propagate: bool = True,\n    ) -> dict[str, Any]:\n        if all((memory_type is None, memory_content is None, memory_categories is None)):\n            msg = \"At least one of memory type, memory content, or memory categories is required for UPDATE operation\"\n            raise ValueError(msg)\n        if memory_type and memory_type not in get_args(MemoryType):\n            msg = f\"Invalid memory type: '{memory_type}', must be one of {get_args(MemoryType)}\"\n            raise ValueError(msg)\n\n        ctx = self._get_context()\n        store = self._get_database()\n        user_scope = self.user_model(**user).model_dump() if user is not None else None\n        await self._ensure_categories_ready(ctx, store, user_scope)\n\n        state: WorkflowState = {\n            \"memory_id\": memory_id,\n            \"memory_payload\": {\n                \"type\": memory_type,\n                \"content\": memory_content,\n                \"categories\": memory_categories,\n            },\n            \"ctx\": ctx,\n            \"store\": store,\n            \"category_ids\": list(ctx.category_ids),\n            \"user\": user_scope,\n            \"propagate\": propagate,\n        }\n\n        result = await self._run_workflow(\"patch_update\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Update memory item workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    async def delete_memory_item(\n        self,\n        *,\n        memory_id: str,\n        user: dict[str, Any] | None = None,\n        propagate: bool = True,\n    ) -> dict[str, Any]:\n        ctx = self._get_context()\n        store = self._get_database()\n        user_scope = self.user_model(**user).model_dump() if user is not None else None\n        await self._ensure_categories_ready(ctx, store, user_scope)\n\n        state: WorkflowState = {\n            \"memory_id\": memory_id,\n            \"ctx\": ctx,\n            \"store\": store,\n            \"category_ids\": list(ctx.category_ids),\n            \"user\": user_scope,\n            \"propagate\": propagate,\n        }\n\n        result = await self._run_workflow(\"patch_delete\", state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Delete memory item workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    def _build_create_memory_item_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"create_memory_item\",\n                role=\"patch\",\n                handler=self._patch_create_memory_item,\n                requires={\"memory_payload\", \"ctx\", \"store\", \"user\"},\n                produces={\"memory_item\", \"category_updates\"},\n                capabilities={\"db\", \"llm\"},\n            ),\n            WorkflowStep(\n                step_id=\"persist_index\",\n                role=\"persist\",\n                handler=self._patch_persist_and_index,\n                requires={\"category_updates\", \"ctx\", \"store\"},\n                produces={\"categories\"},\n                capabilities={\"db\", \"llm\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._patch_build_response,\n                requires={\"memory_item\", \"category_updates\", \"ctx\", \"store\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_create_memory_item_initial_keys() -> set[str]:\n        return {\n            \"memory_payload\",\n            \"ctx\",\n            \"store\",\n            \"user\",\n        }\n\n    def _build_update_memory_item_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"update_memory_item\",\n                role=\"patch\",\n                handler=self._patch_update_memory_item,\n                requires={\"memory_id\", \"memory_payload\", \"ctx\", \"store\", \"user\"},\n                produces={\"memory_item\", \"category_updates\"},\n                capabilities={\"db\", \"llm\"},\n            ),\n            WorkflowStep(\n                step_id=\"persist_index\",\n                role=\"persist\",\n                handler=self._patch_persist_and_index,\n                requires={\"category_updates\", \"ctx\", \"store\"},\n                produces={\"categories\"},\n                capabilities={\"db\", \"llm\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._patch_build_response,\n                requires={\"memory_item\", \"category_updates\", \"ctx\", \"store\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_update_memory_item_initial_keys() -> set[str]:\n        return {\n            \"memory_id\",\n            \"memory_payload\",\n            \"ctx\",\n            \"store\",\n            \"user\",\n        }\n\n    def _build_delete_memory_item_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"delete_memory_item\",\n                role=\"patch\",\n                handler=self._patch_delete_memory_item,\n                requires={\"memory_id\", \"ctx\", \"store\", \"user\"},\n                produces={\"memory_item\", \"category_updates\"},\n                capabilities={\"db\", \"llm\"},\n            ),\n            WorkflowStep(\n                step_id=\"persist_index\",\n                role=\"persist\",\n                handler=self._patch_persist_and_index,\n                requires={\"category_updates\", \"ctx\", \"store\"},\n                produces={\"categories\"},\n                capabilities={\"db\", \"llm\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_response\",\n                role=\"emit\",\n                handler=self._patch_build_response,\n                requires={\"memory_item\", \"category_updates\", \"ctx\", \"store\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    @staticmethod\n    def _list_delete_memory_item_initial_keys() -> set[str]:\n        return {\n            \"memory_id\",\n            \"ctx\",\n            \"store\",\n            \"user\",\n        }\n\n    async def _patch_create_memory_item(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        memory_payload = state[\"memory_payload\"]\n        ctx = state[\"ctx\"]\n        store = state[\"store\"]\n        user = state[\"user\"]\n        propagate = state[\"propagate\"]\n        category_memory_updates: dict[str, tuple[Any, Any]] = {}\n\n        embed_payload = [memory_payload[\"content\"]]\n        content_embedding = (await self._get_llm_client().embed(embed_payload))[0]\n\n        item = store.memory_item_repo.create_item(\n            memory_type=memory_payload[\"type\"],\n            summary=memory_payload[\"content\"],\n            embedding=content_embedding,\n            user_data=dict(user or {}),\n        )\n        cat_names = memory_payload[\"categories\"]\n        mapped_cat_ids = self._map_category_names_to_ids(cat_names, ctx)\n        for cid in mapped_cat_ids:\n            store.category_item_repo.link_item_category(item.id, cid, user_data=dict(user or {}))\n            if propagate:\n                category_memory_updates[cid] = (None, memory_payload[\"content\"])\n\n        state.update({\n            \"memory_item\": item,\n            \"category_updates\": category_memory_updates,\n        })\n        return state\n\n    async def _patch_update_memory_item(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        memory_id = state[\"memory_id\"]\n        memory_payload = state[\"memory_payload\"]\n        ctx = state[\"ctx\"]\n        store = state[\"store\"]\n        user = state[\"user\"]\n        propagate = state[\"propagate\"]\n        category_memory_updates: dict[str, tuple[Any, Any]] = {}\n\n        item = store.memory_item_repo.get_item(memory_id)\n        if not item:\n            msg = f\"Memory item with id {memory_id} not found\"\n            raise ValueError(msg)\n        old_content = item.summary\n        old_item_categories = store.category_item_repo.get_item_categories(memory_id)\n        mapped_old_cat_ids = [cat.category_id for cat in old_item_categories]\n\n        if memory_payload[\"content\"]:\n            embed_payload = [memory_payload[\"content\"]]\n            content_embedding = (await self._get_llm_client().embed(embed_payload))[0]\n        else:\n            content_embedding = None\n\n        if memory_payload[\"type\"] or memory_payload[\"content\"]:\n            item = store.memory_item_repo.update_item(\n                item_id=memory_id,\n                memory_type=memory_payload[\"type\"],\n                summary=memory_payload[\"content\"],\n                embedding=content_embedding,\n            )\n        new_cat_names = memory_payload[\"categories\"]\n        mapped_new_cat_ids = self._map_category_names_to_ids(new_cat_names, ctx)\n\n        cats_to_remove = set(mapped_old_cat_ids) - set(mapped_new_cat_ids)\n        cats_to_add = set(mapped_new_cat_ids) - set(mapped_old_cat_ids)\n        for cid in cats_to_remove:\n            store.category_item_repo.unlink_item_category(memory_id, cid)\n            if propagate:\n                category_memory_updates[cid] = (old_content, None)\n        for cid in cats_to_add:\n            store.category_item_repo.link_item_category(memory_id, cid, user_data=dict(user or {}))\n            if propagate:\n                category_memory_updates[cid] = (None, item.summary)\n\n        if propagate and memory_payload[\"content\"]:\n            for cid in set(mapped_old_cat_ids) & set(mapped_new_cat_ids):\n                category_memory_updates[cid] = (old_content, item.summary)\n\n        state.update({\n            \"memory_item\": item,\n            \"category_updates\": category_memory_updates,\n        })\n        return state\n\n    async def _patch_delete_memory_item(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        memory_id = state[\"memory_id\"]\n        store = state[\"store\"]\n        propagate = state[\"propagate\"]\n        category_memory_updates: dict[str, tuple[Any, Any]] = {}\n\n        item = store.memory_item_repo.get_item(memory_id)\n        if not item:\n            msg = f\"Memory item with id {memory_id} not found\"\n            raise ValueError(msg)\n        item_categories = store.category_item_repo.get_item_categories(memory_id)\n        if propagate:\n            for cat in item_categories:\n                category_memory_updates[cat.category_id] = (item.summary, None)\n        store.memory_item_repo.delete_item(memory_id)\n\n        state.update({\n            \"memory_item\": item,\n            \"category_updates\": category_memory_updates,\n        })\n        return state\n\n    async def _patch_persist_and_index(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        llm_client = self._get_step_llm_client(step_context)\n        await self._patch_category_summaries(\n            state.get(\"category_updates\", {}),\n            ctx=state[\"ctx\"],\n            store=state[\"store\"],\n            llm_client=llm_client,\n        )\n        return state\n\n    def _patch_build_response(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        store = state[\"store\"]\n        item = self._model_dump_without_embeddings(state[\"memory_item\"])\n        category_updates_ids = list(state.get(\"category_updates\", {}).keys())\n        category_updates = [\n            self._model_dump_without_embeddings(store.memory_category_repo.categories[c]) for c in category_updates_ids\n        ]\n        response = {\n            \"memory_item\": item,\n            \"category_updates\": category_updates,\n        }\n        state[\"response\"] = response\n        return state\n\n    def _map_category_names_to_ids(self, names: list[str], ctx: Context) -> list[str]:\n        if not names:\n            return []\n        mapped: list[str] = []\n        seen: set[str] = set()\n        for name in names:\n            key = name.strip().lower()\n            cid = ctx.category_name_to_id.get(key)\n            if cid and cid not in seen:\n                mapped.append(cid)\n                seen.add(cid)\n        return mapped\n\n    async def _patch_category_summaries(\n        self,\n        updates: dict[str, list[str]],\n        ctx: Context,\n        store: Database,\n        llm_client: Any | None = None,\n    ) -> None:\n        if not updates:\n            return\n        tasks = []\n        target_ids: list[str] = []\n        client = llm_client or self._get_llm_client()\n        for cid, (content_before, content_after) in updates.items():\n            cat = store.memory_category_repo.categories.get(cid)\n            if not cat or (not content_before and not content_after):\n                continue\n            prompt = self._build_category_patch_prompt(\n                category=cat, content_before=content_before, content_after=content_after\n            )\n            tasks.append(client.chat(prompt))\n            target_ids.append(cid)\n        if not tasks:\n            return\n        patches = await asyncio.gather(*tasks)\n        for cid, patch in zip(target_ids, patches, strict=True):\n            need_update, summary = self._parse_category_patch_response(patch)\n            if not need_update:\n                continue\n            cat = store.memory_category_repo.categories.get(cid)\n            store.memory_category_repo.update_category(\n                category_id=cid,\n                summary=summary.strip(),\n            )\n\n    def _build_category_patch_prompt(\n        self, *, category: MemoryCategory, content_before: str | None, content_after: str | None\n    ) -> str:\n        if content_before and content_after:\n            update_content = \"\\n\".join([\n                \"The memory content before:\",\n                content_before,\n                \"The memory content after:\",\n                content_after,\n            ])\n        elif content_before:\n            update_content = \"\\n\".join([\n                \"This memory content is discarded:\",\n                content_before,\n            ])\n        elif content_after:\n            update_content = \"\\n\".join([\n                \"This memory content is newly added:\",\n                content_after,\n            ])\n        original_content = category.summary or \"\"\n        prompt = CATEGORY_PATCH_PROMPT\n        return prompt.format(\n            category=self._escape_prompt_value(category.name),\n            original_content=self._escape_prompt_value(original_content or \"\"),\n            update_content=self._escape_prompt_value(update_content or \"\"),\n        )\n\n    def _parse_category_patch_response(self, response: str) -> tuple[bool, str]:\n        try:\n            data = json.loads(response)\n        except (json.JSONDecodeError, TypeError):\n            return False, \"\"\n        if not isinstance(data, dict):\n            return False, \"\"\n        if not data.get(\"updated_content\"):\n            return False, \"\"\n        need_update = data.get(\"need_update\", False)\n        updated_content = data[\"updated_content\"].strip()\n        if updated_content == \"empty\":\n            updated_content = \"\"\n        return need_update, updated_content\n"
  },
  {
    "path": "src/memu/app/retrieve.py",
    "content": "from __future__ import annotations\n\nimport json\nimport logging\nimport re\nfrom collections.abc import Awaitable, Callable, Mapping, Sequence\nfrom typing import TYPE_CHECKING, Any, cast\n\nfrom pydantic import BaseModel\n\nfrom memu.database.inmemory.vector import cosine_topk\nfrom memu.prompts.retrieve.llm_category_ranker import PROMPT as LLM_CATEGORY_RANKER_PROMPT\nfrom memu.prompts.retrieve.llm_item_ranker import PROMPT as LLM_ITEM_RANKER_PROMPT\nfrom memu.prompts.retrieve.llm_resource_ranker import PROMPT as LLM_RESOURCE_RANKER_PROMPT\nfrom memu.prompts.retrieve.pre_retrieval_decision import SYSTEM_PROMPT as PRE_RETRIEVAL_SYSTEM_PROMPT\nfrom memu.prompts.retrieve.pre_retrieval_decision import USER_PROMPT as PRE_RETRIEVAL_USER_PROMPT\nfrom memu.workflow.step import WorkflowState, WorkflowStep\n\nlogger = logging.getLogger(__name__)\n\nif TYPE_CHECKING:\n    from memu.app.service import Context\n    from memu.app.settings import RetrieveConfig\n    from memu.database.interfaces import Database\n\n\nclass RetrieveMixin:\n    if TYPE_CHECKING:\n        retrieve_config: RetrieveConfig\n        _run_workflow: Callable[..., Awaitable[WorkflowState]]\n        _get_context: Callable[[], Context]\n        _get_database: Callable[[], Database]\n        _ensure_categories_ready: Callable[[Context, Database], Awaitable[None]]\n        _get_step_llm_client: Callable[[Mapping[str, Any] | None], Any]\n        _get_step_embedding_client: Callable[[Mapping[str, Any] | None], Any]\n        _get_llm_client: Callable[..., Any]\n        _model_dump_without_embeddings: Callable[[BaseModel], dict[str, Any]]\n        _extract_json_blob: Callable[[str], str]\n        _escape_prompt_value: Callable[[str], str]\n        user_model: type[BaseModel]\n\n    async def retrieve(\n        self,\n        queries: list[dict[str, Any]],\n        where: dict[str, Any] | None = None,\n    ) -> dict[str, Any]:\n        if not queries:\n            raise ValueError(\"empty_queries\")\n        ctx = self._get_context()\n        store = self._get_database()\n        original_query = self._extract_query_text(queries[-1])\n        # await self._ensure_categories_ready(ctx, store)\n        where_filters = self._normalize_where(where)\n\n        context_queries_objs = queries[:-1] if len(queries) > 1 else []\n\n        route_intention = self.retrieve_config.route_intention\n        retrieve_category = self.retrieve_config.category.enabled\n        retrieve_item = self.retrieve_config.item.enabled\n        retrieve_resource = self.retrieve_config.resource.enabled\n        sufficiency_check = self.retrieve_config.sufficiency_check\n\n        workflow_name = \"retrieve_llm\" if self.retrieve_config.method == \"llm\" else \"retrieve_rag\"\n\n        state: WorkflowState = {\n            \"method\": self.retrieve_config.method,\n            \"original_query\": original_query,\n            \"context_queries\": context_queries_objs,\n            \"route_intention\": route_intention,\n            \"skip_rewrite\": len(queries) == 1,\n            \"retrieve_category\": retrieve_category,\n            \"retrieve_item\": retrieve_item,\n            \"retrieve_resource\": retrieve_resource,\n            \"sufficiency_check\": sufficiency_check,\n            \"ctx\": ctx,\n            \"store\": store,\n            \"where\": where_filters,\n        }\n\n        result = await self._run_workflow(workflow_name, state)\n        response = cast(dict[str, Any] | None, result.get(\"response\"))\n        if response is None:\n            msg = \"Retrieve workflow failed to produce a response\"\n            raise RuntimeError(msg)\n        return response\n\n    def _normalize_where(self, where: Mapping[str, Any] | None) -> dict[str, Any]:\n        \"\"\"Validate and clean the `where` scope filters against the configured user model.\"\"\"\n        if not where:\n            return {}\n\n        valid_fields = set(getattr(self.user_model, \"model_fields\", {}).keys())\n        cleaned: dict[str, Any] = {}\n\n        for raw_key, value in where.items():\n            if value is None:\n                continue\n            field = raw_key.split(\"__\", 1)[0]\n            if field not in valid_fields:\n                msg = f\"Unknown filter field '{field}' for current user scope\"\n                raise ValueError(msg)\n            cleaned[raw_key] = value\n\n        return cleaned\n\n    def _build_rag_retrieve_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"route_intention\",\n                role=\"route_intention\",\n                handler=self._rag_route_intention,\n                requires={\"route_intention\", \"original_query\", \"context_queries\", \"skip_rewrite\"},\n                produces={\"needs_retrieval\", \"rewritten_query\", \"active_query\", \"next_step_query\"},\n                capabilities={\"llm\"},\n                config={\"chat_llm_profile\": self.retrieve_config.sufficiency_check_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"route_category\",\n                role=\"route_category\",\n                handler=self._rag_route_category,\n                requires={\"retrieve_category\", \"needs_retrieval\", \"active_query\", \"ctx\", \"store\", \"where\"},\n                produces={\"category_hits\", \"category_summary_lookup\", \"query_vector\"},\n                capabilities={\"vector\"},\n                config={\"embed_llm_profile\": \"embedding\"},\n            ),\n            WorkflowStep(\n                step_id=\"sufficiency_after_category\",\n                role=\"sufficiency_check\",\n                handler=self._rag_category_sufficiency,\n                requires={\n                    \"retrieve_category\",\n                    \"needs_retrieval\",\n                    \"active_query\",\n                    \"context_queries\",\n                    \"category_hits\",\n                    \"ctx\",\n                    \"store\",\n                    \"where\",\n                },\n                produces={\"next_step_query\", \"proceed_to_items\", \"query_vector\"},\n                capabilities={\"llm\"},\n                config={\n                    \"chat_llm_profile\": self.retrieve_config.sufficiency_check_llm_profile,\n                    \"embed_llm_profile\": \"embedding\",\n                },\n            ),\n            WorkflowStep(\n                step_id=\"recall_items\",\n                role=\"recall_items\",\n                handler=self._rag_recall_items,\n                requires={\n                    \"needs_retrieval\",\n                    \"proceed_to_items\",\n                    \"ctx\",\n                    \"store\",\n                    \"where\",\n                    \"active_query\",\n                    \"query_vector\",\n                },\n                produces={\"item_hits\", \"query_vector\"},\n                capabilities={\"vector\"},\n                config={\"embed_llm_profile\": \"embedding\"},\n            ),\n            WorkflowStep(\n                step_id=\"sufficiency_after_items\",\n                role=\"sufficiency_check\",\n                handler=self._rag_item_sufficiency,\n                requires={\n                    \"needs_retrieval\",\n                    \"active_query\",\n                    \"context_queries\",\n                    \"item_hits\",\n                    \"ctx\",\n                    \"store\",\n                    \"where\",\n                },\n                produces={\"next_step_query\", \"proceed_to_resources\", \"query_vector\"},\n                capabilities={\"llm\"},\n                config={\n                    \"chat_llm_profile\": self.retrieve_config.sufficiency_check_llm_profile,\n                    \"embed_llm_profile\": \"embedding\",\n                },\n            ),\n            WorkflowStep(\n                step_id=\"recall_resources\",\n                role=\"recall_resources\",\n                handler=self._rag_recall_resources,\n                requires={\n                    \"needs_retrieval\",\n                    \"proceed_to_resources\",\n                    \"ctx\",\n                    \"store\",\n                    \"where\",\n                    \"active_query\",\n                    \"query_vector\",\n                },\n                produces={\"resource_hits\", \"query_vector\"},\n                capabilities={\"vector\"},\n                config={\"embed_llm_profile\": \"embedding\"},\n            ),\n            WorkflowStep(\n                step_id=\"build_context\",\n                role=\"build_context\",\n                handler=self._rag_build_context,\n                requires={\"needs_retrieval\", \"original_query\", \"rewritten_query\", \"ctx\", \"store\", \"where\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    def _list_retrieve_initial_keys(self) -> set[str]:\n        return {\n            \"method\",\n            \"original_query\",\n            \"context_queries\",\n            \"route_intention\",\n            \"skip_rewrite\",\n            \"retrieve_category\",\n            \"retrieve_item\",\n            \"retrieve_resource\",\n            \"sufficiency_check\",\n            \"ctx\",\n            \"store\",\n            \"where\",\n        }\n\n    async def _rag_route_intention(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"route_intention\"):\n            state.update({\n                \"needs_retrieval\": True,\n                \"rewritten_query\": state[\"original_query\"],\n                \"active_query\": state[\"original_query\"],\n                \"next_step_query\": None,\n                \"proceed_to_items\": False,\n                \"proceed_to_resources\": False,\n            })\n            return state\n\n        llm_client = self._get_step_llm_client(step_context)\n        needs_retrieval, rewritten_query = await self._decide_if_retrieval_needed(\n            state[\"original_query\"],\n            state[\"context_queries\"],\n            retrieved_content=None,\n            llm_client=llm_client,\n        )\n        if state.get(\"skip_rewrite\"):\n            rewritten_query = state[\"original_query\"]\n\n        state.update({\n            \"needs_retrieval\": needs_retrieval,\n            \"rewritten_query\": rewritten_query,\n            \"active_query\": rewritten_query,\n            \"next_step_query\": None,\n            \"proceed_to_items\": False,\n            \"proceed_to_resources\": False,\n        })\n        return state\n\n    async def _rag_route_category(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"retrieve_category\") or not state.get(\"needs_retrieval\"):\n            state[\"category_hits\"] = []\n            state[\"category_summary_lookup\"] = {}\n            state[\"query_vector\"] = None\n            return state\n\n        embed_client = self._get_step_embedding_client(step_context)\n        store = state[\"store\"]\n        where_filters = state.get(\"where\") or {}\n        category_pool = store.memory_category_repo.list_categories(where_filters)\n        qvec = (await embed_client.embed([state[\"active_query\"]]))[0]\n        hits, summary_lookup = await self._rank_categories_by_summary(\n            qvec,\n            self.retrieve_config.category.top_k,\n            state[\"ctx\"],\n            store,\n            embed_client=embed_client,\n            categories=category_pool,\n        )\n        state.update({\n            \"query_vector\": qvec,\n            \"category_hits\": hits,\n            \"category_summary_lookup\": summary_lookup,\n            \"category_pool\": category_pool,\n        })\n        return state\n\n    async def _rag_category_sufficiency(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"needs_retrieval\"):\n            state[\"proceed_to_items\"] = False\n            return state\n        if not state.get(\"retrieve_category\") or not state.get(\"sufficiency_check\"):\n            state[\"proceed_to_items\"] = True\n            return state\n\n        retrieved_content = \"\"\n        store = state[\"store\"]\n        where_filters = state.get(\"where\") or {}\n        category_pool = state.get(\"category_pool\") or store.memory_category_repo.list_categories(where_filters)\n        hits = state.get(\"category_hits\") or []\n        if hits:\n            retrieved_content = self._format_category_content(\n                hits,\n                state.get(\"category_summary_lookup\", {}),\n                store,\n                categories=category_pool,\n            )\n\n        llm_client = self._get_step_llm_client(step_context)\n        needs_more, rewritten_query = await self._decide_if_retrieval_needed(\n            state[\"active_query\"],\n            state[\"context_queries\"],\n            retrieved_content=retrieved_content or \"No content retrieved yet.\",\n            llm_client=llm_client,\n        )\n        state[\"next_step_query\"] = rewritten_query\n        state[\"active_query\"] = rewritten_query\n        state[\"proceed_to_items\"] = needs_more\n        if needs_more:\n            embed_client = self._get_step_embedding_client(step_context)\n            state[\"query_vector\"] = (await embed_client.embed([state[\"active_query\"]]))[0]\n        return state\n\n    def _extract_referenced_item_ids(self, state: WorkflowState) -> set[str]:\n        \"\"\"Extract item IDs from category summary references.\"\"\"\n        from memu.utils.references import extract_references\n\n        category_hits = state.get(\"category_hits\") or []\n        summary_lookup = state.get(\"category_summary_lookup\", {})\n        category_pool = state.get(\"category_pool\") or {}\n        referenced_item_ids: set[str] = set()\n\n        for cid, _score in category_hits:\n            # Get summary from lookup or category\n            summary = summary_lookup.get(cid)\n            if not summary:\n                cat = category_pool.get(cid)\n                if cat:\n                    summary = cat.summary\n            if summary:\n                refs = extract_references(summary)\n                referenced_item_ids.update(refs)\n\n        return referenced_item_ids\n\n    async def _rag_recall_items(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"retrieve_item\") or not state.get(\"needs_retrieval\") or not state.get(\"proceed_to_items\"):\n            state[\"item_hits\"] = []\n            return state\n\n        store = state[\"store\"]\n        where_filters = state.get(\"where\") or {}\n        items_pool = store.memory_item_repo.list_items(where_filters)\n        qvec = state.get(\"query_vector\")\n        if qvec is None:\n            embed_client = self._get_step_embedding_client(step_context)\n            qvec = (await embed_client.embed([state[\"active_query\"]]))[0]\n            state[\"query_vector\"] = qvec\n        state[\"item_hits\"] = store.memory_item_repo.vector_search_items(\n            qvec,\n            self.retrieve_config.item.top_k,\n            where=where_filters,\n            ranking=self.retrieve_config.item.ranking,\n            recency_decay_days=self.retrieve_config.item.recency_decay_days,\n        )\n        state[\"item_pool\"] = items_pool\n        return state\n\n    async def _rag_item_sufficiency(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"needs_retrieval\"):\n            state[\"proceed_to_resources\"] = False\n            return state\n        if not state.get(\"retrieve_item\") or not state.get(\"sufficiency_check\"):\n            state[\"proceed_to_resources\"] = True\n            return state\n\n        store = state[\"store\"]\n        where_filters = state.get(\"where\") or {}\n        items_pool = state.get(\"item_pool\") or store.memory_item_repo.list_items(where_filters)\n        retrieved_content = \"\"\n        hits = state.get(\"item_hits\") or []\n        if hits:\n            retrieved_content = self._format_item_content(hits, store, items=items_pool)\n\n        llm_client = self._get_step_llm_client(step_context)\n        needs_more, rewritten_query = await self._decide_if_retrieval_needed(\n            state[\"active_query\"],\n            state[\"context_queries\"],\n            retrieved_content=retrieved_content or \"No content retrieved yet.\",\n            llm_client=llm_client,\n        )\n        state[\"next_step_query\"] = rewritten_query\n        state[\"active_query\"] = rewritten_query\n        state[\"proceed_to_resources\"] = needs_more\n        if needs_more:\n            embed_client = self._get_step_embedding_client(step_context)\n            state[\"query_vector\"] = (await embed_client.embed([state[\"active_query\"]]))[0]\n        return state\n\n    async def _rag_recall_resources(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if (\n            not state.get(\"needs_retrieval\")\n            or not state.get(\"retrieve_resource\")\n            or not state.get(\"proceed_to_resources\")\n        ):\n            state[\"resource_hits\"] = []\n            return state\n\n        store = state[\"store\"]\n        where_filters = state.get(\"where\") or {}\n        resource_pool = store.resource_repo.list_resources(where_filters)\n        state[\"resource_pool\"] = resource_pool\n        corpus = self._resource_caption_corpus(store, resources=resource_pool)\n        if not corpus:\n            state[\"resource_hits\"] = []\n            return state\n\n        qvec = state.get(\"query_vector\")\n        if qvec is None:\n            embed_client = self._get_step_embedding_client(step_context)\n            qvec = (await embed_client.embed([state[\"active_query\"]]))[0]\n            state[\"query_vector\"] = qvec\n        state[\"resource_hits\"] = cosine_topk(qvec, corpus, k=self.retrieve_config.resource.top_k)\n        return state\n\n    def _rag_build_context(self, state: WorkflowState, _: Any) -> WorkflowState:\n        response = {\n            \"needs_retrieval\": bool(state.get(\"needs_retrieval\")),\n            \"original_query\": state[\"original_query\"],\n            \"rewritten_query\": state.get(\"rewritten_query\", state[\"original_query\"]),\n            \"next_step_query\": state.get(\"next_step_query\"),\n            \"categories\": [],\n            \"items\": [],\n            \"resources\": [],\n        }\n        if state.get(\"needs_retrieval\"):\n            store = state[\"store\"]\n            where_filters = state.get(\"where\") or {}\n            categories_pool = state.get(\"category_pool\") or store.memory_category_repo.list_categories(where_filters)\n            items_pool = state.get(\"item_pool\") or store.memory_item_repo.list_items(where_filters)\n            resources_pool = state.get(\"resource_pool\") or store.resource_repo.list_resources(where_filters)\n            response[\"categories\"] = self._materialize_hits(\n                state.get(\"category_hits\", []),\n                categories_pool,\n            )\n            response[\"items\"] = self._materialize_hits(state.get(\"item_hits\", []), items_pool)\n            response[\"resources\"] = self._materialize_hits(\n                state.get(\"resource_hits\", []),\n                resources_pool,\n            )\n        state[\"response\"] = response\n        return state\n\n    def _build_llm_retrieve_workflow(self) -> list[WorkflowStep]:\n        steps = [\n            WorkflowStep(\n                step_id=\"route_intention\",\n                role=\"route_intention\",\n                handler=self._llm_route_intention,\n                requires={\"original_query\", \"context_queries\", \"skip_rewrite\"},\n                produces={\"needs_retrieval\", \"rewritten_query\", \"active_query\", \"next_step_query\"},\n                capabilities={\"llm\"},\n                config={\"llm_profile\": self.retrieve_config.sufficiency_check_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"route_category\",\n                role=\"route_category\",\n                handler=self._llm_route_category,\n                requires={\"needs_retrieval\", \"active_query\", \"ctx\", \"store\", \"where\"},\n                produces={\"category_hits\"},\n                capabilities={\"llm\"},\n                config={\"llm_profile\": self.retrieve_config.llm_ranking_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"sufficiency_after_category\",\n                role=\"sufficiency_check\",\n                handler=self._llm_category_sufficiency,\n                requires={\"needs_retrieval\", \"active_query\", \"context_queries\", \"category_hits\"},\n                produces={\"next_step_query\", \"proceed_to_items\"},\n                capabilities={\"llm\"},\n                config={\"llm_profile\": self.retrieve_config.sufficiency_check_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"recall_items\",\n                role=\"recall_items\",\n                handler=self._llm_recall_items,\n                requires={\n                    \"needs_retrieval\",\n                    \"proceed_to_items\",\n                    \"ctx\",\n                    \"store\",\n                    \"where\",\n                    \"active_query\",\n                    \"category_hits\",\n                },\n                produces={\"item_hits\"},\n                capabilities={\"llm\"},\n                config={\"llm_profile\": self.retrieve_config.llm_ranking_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"sufficiency_after_items\",\n                role=\"sufficiency_check\",\n                handler=self._llm_item_sufficiency,\n                requires={\"needs_retrieval\", \"active_query\", \"context_queries\", \"item_hits\"},\n                produces={\"next_step_query\", \"proceed_to_resources\"},\n                capabilities={\"llm\"},\n                config={\"llm_profile\": self.retrieve_config.sufficiency_check_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"recall_resources\",\n                role=\"recall_resources\",\n                handler=self._llm_recall_resources,\n                requires={\n                    \"needs_retrieval\",\n                    \"proceed_to_resources\",\n                    \"active_query\",\n                    \"ctx\",\n                    \"store\",\n                    \"where\",\n                    \"item_hits\",\n                    \"category_hits\",\n                },\n                produces={\"resource_hits\"},\n                capabilities={\"llm\"},\n                config={\"llm_profile\": self.retrieve_config.llm_ranking_llm_profile},\n            ),\n            WorkflowStep(\n                step_id=\"build_context\",\n                role=\"build_context\",\n                handler=self._llm_build_context,\n                requires={\"needs_retrieval\", \"original_query\", \"rewritten_query\"},\n                produces={\"response\"},\n                capabilities=set(),\n            ),\n        ]\n        return steps\n\n    async def _llm_route_intention(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"route_intention\"):\n            state.update({\n                \"needs_retrieval\": True,\n                \"rewritten_query\": state[\"original_query\"],\n                \"active_query\": state[\"original_query\"],\n                \"next_step_query\": None,\n                \"proceed_to_items\": False,\n                \"proceed_to_resources\": False,\n            })\n            return state\n\n        llm_client = self._get_step_llm_client(step_context)\n        needs_retrieval, rewritten_query = await self._decide_if_retrieval_needed(\n            state[\"original_query\"],\n            state[\"context_queries\"],\n            retrieved_content=None,\n            llm_client=llm_client,\n        )\n        if state.get(\"skip_rewrite\"):\n            rewritten_query = state[\"original_query\"]\n\n        state.update({\n            \"needs_retrieval\": needs_retrieval,\n            \"rewritten_query\": rewritten_query,\n            \"active_query\": rewritten_query,\n            \"next_step_query\": None,\n            \"proceed_to_items\": False,\n            \"proceed_to_resources\": False,\n        })\n        return state\n\n    async def _llm_route_category(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"needs_retrieval\"):\n            state[\"category_hits\"] = []\n            return state\n        llm_client = self._get_step_llm_client(step_context)\n        store = state[\"store\"]\n        where_filters = state.get(\"where\") or {}\n        category_pool = store.memory_category_repo.list_categories(where_filters)\n        hits = await self._llm_rank_categories(\n            state[\"active_query\"],\n            self.retrieve_config.category.top_k,\n            state[\"ctx\"],\n            store,\n            llm_client=llm_client,\n            categories=category_pool,\n        )\n        state[\"category_hits\"] = hits\n        state[\"category_pool\"] = category_pool\n        return state\n\n    async def _llm_category_sufficiency(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"needs_retrieval\"):\n            state[\"proceed_to_items\"] = False\n            return state\n        if not state.get(\"retrieve_category\") or not state.get(\"sufficiency_check\"):\n            state[\"proceed_to_items\"] = True\n            return state\n\n        retrieved_content = \"\"\n        hits = state.get(\"category_hits\") or []\n        if hits:\n            retrieved_content = self._format_llm_category_content(hits)\n\n        llm_client = self._get_step_llm_client(step_context)\n        needs_more, rewritten_query = await self._decide_if_retrieval_needed(\n            state[\"active_query\"],\n            state[\"context_queries\"],\n            retrieved_content=retrieved_content or \"No content retrieved yet.\",\n            llm_client=llm_client,\n        )\n        state[\"next_step_query\"] = rewritten_query\n        state[\"active_query\"] = rewritten_query\n        state[\"proceed_to_items\"] = needs_more\n        return state\n\n    async def _llm_recall_items(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"needs_retrieval\") or not state.get(\"proceed_to_items\"):\n            state[\"item_hits\"] = []\n            return state\n\n        where_filters = state.get(\"where\") or {}\n        category_hits = state.get(\"category_hits\", [])\n        category_ids = [cat[\"id\"] for cat in category_hits]\n        llm_client = self._get_step_llm_client(step_context)\n        store = state[\"store\"]\n\n        use_refs = getattr(self.retrieve_config.item, \"use_category_references\", False)\n        ref_ids: list[str] = []\n        if use_refs and category_hits:\n            # Extract all ref_ids from category summaries\n            from memu.utils.references import extract_references\n\n            for cat in category_hits:\n                summary = cat.get(\"summary\") or \"\"\n                ref_ids.extend(extract_references(summary))\n        if ref_ids:\n            # Query items by ref_ids\n            items_pool = store.memory_item_repo.list_items_by_ref_ids(ref_ids, where_filters)\n        else:\n            items_pool = store.memory_item_repo.list_items(where_filters)\n\n        relations = store.category_item_repo.list_relations(where_filters)\n        category_pool = state.get(\"category_pool\") or store.memory_category_repo.list_categories(where_filters)\n        state[\"item_hits\"] = await self._llm_rank_items(\n            state[\"active_query\"],\n            self.retrieve_config.item.top_k,\n            category_ids,\n            state.get(\"category_hits\", []),\n            state[\"ctx\"],\n            store,\n            llm_client=llm_client,\n            categories=category_pool,\n            items=items_pool,\n            relations=relations,\n        )\n        state[\"item_pool\"] = items_pool\n        state[\"relation_pool\"] = relations\n        return state\n\n    async def _llm_item_sufficiency(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"needs_retrieval\"):\n            state[\"proceed_to_resources\"] = False\n            return state\n        if not state.get(\"retrieve_item\") or not state.get(\"sufficiency_check\"):\n            state[\"proceed_to_resources\"] = True\n            return state\n\n        retrieved_content = \"\"\n        hits = state.get(\"item_hits\") or []\n        if hits:\n            retrieved_content = self._format_llm_item_content(hits)\n\n        llm_client = self._get_step_llm_client(step_context)\n        needs_more, rewritten_query = await self._decide_if_retrieval_needed(\n            state[\"active_query\"],\n            state[\"context_queries\"],\n            retrieved_content=retrieved_content or \"No content retrieved yet.\",\n            llm_client=llm_client,\n        )\n        state[\"next_step_query\"] = rewritten_query\n        state[\"active_query\"] = rewritten_query\n        state[\"proceed_to_resources\"] = needs_more\n        return state\n\n    async def _llm_recall_resources(self, state: WorkflowState, step_context: Any) -> WorkflowState:\n        if not state.get(\"needs_retrieval\") or not state.get(\"proceed_to_resources\"):\n            state[\"resource_hits\"] = []\n            return state\n\n        llm_client = self._get_step_llm_client(step_context)\n        store = state[\"store\"]\n        where_filters = state.get(\"where\") or {}\n        resource_pool = store.resource_repo.list_resources(where_filters)\n        items_pool = state.get(\"item_pool\") or store.memory_item_repo.list_items(where_filters)\n        state[\"resource_hits\"] = await self._llm_rank_resources(\n            state[\"active_query\"],\n            self.retrieve_config.resource.top_k,\n            state.get(\"category_hits\", []),\n            state.get(\"item_hits\", []),\n            state[\"ctx\"],\n            store,\n            llm_client=llm_client,\n            items=items_pool,\n            resources=resource_pool,\n        )\n        state[\"resource_pool\"] = resource_pool\n        return state\n\n    def _llm_build_context(self, state: WorkflowState, _: Any) -> WorkflowState:\n        response = {\n            \"needs_retrieval\": bool(state.get(\"needs_retrieval\")),\n            \"original_query\": state[\"original_query\"],\n            \"rewritten_query\": state.get(\"rewritten_query\", state[\"original_query\"]),\n            \"next_step_query\": state.get(\"next_step_query\"),\n            \"categories\": [],\n            \"items\": [],\n            \"resources\": [],\n        }\n        if state.get(\"needs_retrieval\"):\n            response[\"categories\"] = list(state.get(\"category_hits\") or [])\n            response[\"items\"] = list(state.get(\"item_hits\") or [])\n            response[\"resources\"] = list(state.get(\"resource_hits\") or [])\n        state[\"response\"] = response\n        return state\n\n    async def _rank_categories_by_summary(\n        self,\n        query_vec: list[float],\n        top_k: int,\n        ctx: Context,\n        store: Database,\n        embed_client: Any | None = None,\n        categories: Mapping[str, Any] | None = None,\n    ) -> tuple[list[tuple[str, float]], dict[str, str]]:\n        category_pool = categories if categories is not None else store.memory_category_repo.categories\n        entries = [(cid, cat.summary) for cid, cat in category_pool.items() if cat.summary]\n        if not entries:\n            return [], {}\n        summary_texts = [summary for _, summary in entries]\n        client = embed_client or self._get_llm_client()\n        summary_embeddings = await client.embed(summary_texts)\n        corpus = [(cid, emb) for (cid, _), emb in zip(entries, summary_embeddings, strict=True)]\n        hits = cosine_topk(query_vec, corpus, k=top_k)\n        summary_lookup = dict(entries)\n        return hits, summary_lookup\n\n    async def _decide_if_retrieval_needed(\n        self,\n        query: str,\n        context_queries: list[dict[str, Any]] | None,\n        retrieved_content: str | None = None,\n        system_prompt: str | None = None,\n        llm_client: Any | None = None,\n    ) -> tuple[bool, str]:\n        \"\"\"\n        Decide if the query requires memory retrieval (or MORE retrieval) and rewrite it with context.\n\n        Args:\n            query: The current query string\n            context_queries: List of previous query objects with role and content\n            retrieved_content: Content retrieved so far (if checking for sufficiency)\n            system_prompt: Optional system prompt override\n\n        Returns:\n            Tuple of (needs_retrieval: bool, rewritten_query: str)\n            - needs_retrieval: True if retrieval/more retrieval is needed\n            - rewritten_query: The rewritten query for the next step\n        \"\"\"\n        history_text = self._format_query_context(context_queries)\n        content_text = retrieved_content or \"No content retrieved yet.\"\n\n        prompt = self.retrieve_config.sufficiency_check_prompt or PRE_RETRIEVAL_USER_PROMPT\n        user_prompt = prompt.format(\n            query=self._escape_prompt_value(query),\n            conversation_history=self._escape_prompt_value(history_text),\n            retrieved_content=self._escape_prompt_value(content_text),\n        )\n\n        sys_prompt = system_prompt or PRE_RETRIEVAL_SYSTEM_PROMPT\n        client = llm_client or self._get_llm_client()\n        response = await client.chat(user_prompt, system_prompt=sys_prompt)\n        decision = self._extract_decision(response)\n        rewritten = self._extract_rewritten_query(response) or query\n\n        return decision == \"RETRIEVE\", rewritten\n\n    def _format_query_context(self, queries: list[dict[str, Any]] | None) -> str:\n        \"\"\"Format query context for prompts, including role information\"\"\"\n        if not queries:\n            return \"No query context.\"\n\n        lines = []\n        for q in queries:\n            if isinstance(q, str):\n                # Backward compatibility\n                lines.append(f\"- {q}\")\n            elif isinstance(q, dict):\n                role = q.get(\"role\", \"user\")\n                content = q.get(\"content\")\n                if isinstance(content, dict):\n                    text = content.get(\"text\", \"\")\n                elif isinstance(content, str):\n                    text = content\n                else:\n                    text = str(content)\n                lines.append(f\"- [{role}]: {text}\")\n            else:\n                lines.append(f\"- {q!s}\")\n\n        return \"\\n\".join(lines)\n\n    @staticmethod\n    def _extract_query_text(query: dict[str, Any]) -> str:\n        \"\"\"\n        Extract text content from query message structure.\n\n        Args:\n            query: Query in format {\"role\": \"user\", \"content\": {\"text\": \"...\"}}\n\n        Returns:\n            The extracted text string\n        \"\"\"\n        if isinstance(query, str):\n            # Backward compatibility: if it's already a string, return it\n            return query\n\n        if not isinstance(query, dict):\n            raise TypeError(\"INVALID\")\n\n        content = query.get(\"content\")\n        if isinstance(content, dict):\n            text = content.get(\"text\", \"\")\n            if not text:\n                raise ValueError(\"EMPTY\")\n            return str(text)\n        elif isinstance(content, str):\n            # Also support {\"role\": \"user\", \"content\": \"text\"} format\n            return content\n        else:\n            raise TypeError(\"INVALID\")\n\n    def _extract_decision(self, raw: str) -> str:\n        \"\"\"Extract RETRIEVE or NO_RETRIEVE decision from LLM response\"\"\"\n        if not raw:\n            return \"RETRIEVE\"  # Default to retrieve if uncertain\n\n        match = re.search(r\"<decision>(.*?)</decision>\", raw, re.IGNORECASE | re.DOTALL)\n        if match:\n            decision = match.group(1).strip().upper()\n            if \"NO_RETRIEVE\" in decision or \"NO RETRIEVE\" in decision:\n                return \"NO_RETRIEVE\"\n            if \"RETRIEVE\" in decision:\n                return \"RETRIEVE\"\n\n        upper = raw.strip().upper()\n        if \"NO_RETRIEVE\" in upper or \"NO RETRIEVE\" in upper:\n            return \"NO_RETRIEVE\"\n\n        return \"RETRIEVE\"  # Default to retrieve\n\n    def _extract_rewritten_query(self, raw: str) -> str | None:\n        \"\"\"Extract rewritten query from LLM response\"\"\"\n        match = re.search(r\"<rewritten_query>(.*?)</rewritten_query>\", raw, re.IGNORECASE | re.DOTALL)\n        if match:\n            return match.group(1).strip()\n        return None\n\n    async def _embedding_based_retrieve(\n        self,\n        query: str,\n        top_k: int,\n        context_queries: list[dict[str, Any]] | None,\n        ctx: Context,\n        store: Database,\n        llm_client: Any | None = None,\n        where: Mapping[str, Any] | None = None,\n    ) -> dict[str, Any]:\n        \"\"\"Embedding-based retrieval with query rewriting and judging at each tier\"\"\"\n        where_filters = self._normalize_where(where)\n        category_pool = store.memory_category_repo.list_categories(where_filters)\n        items_pool = store.memory_item_repo.list_items(where_filters)\n        resource_pool = store.resource_repo.list_resources(where_filters)\n        client = llm_client or self._get_llm_client()\n        current_query = query\n        qvec = (await client.embed([current_query]))[0]\n        response: dict[str, Any] = {\"resources\": [], \"items\": [], \"categories\": [], \"next_step_query\": None}\n        content_sections: list[str] = []\n\n        # Tier 1: Categories\n        cat_hits, summary_lookup = await self._rank_categories_by_summary(\n            qvec,\n            top_k,\n            ctx,\n            store,\n            embed_client=client,\n            categories=category_pool,\n        )\n        if cat_hits:\n            response[\"categories\"] = self._materialize_hits(cat_hits, category_pool)\n            content_sections.append(\n                self._format_category_content(cat_hits, summary_lookup, store, categories=category_pool)\n            )\n\n            needs_more, current_query = await self._decide_if_retrieval_needed(\n                current_query,\n                context_queries,\n                retrieved_content=\"\\n\\n\".join(content_sections),\n                llm_client=client,\n            )\n            response[\"next_step_query\"] = current_query\n            if not needs_more:\n                return response\n            # Re-embed with rewritten query\n            qvec = (await client.embed([current_query]))[0]\n\n        # Tier 2: Items\n        item_hits = store.memory_item_repo.vector_search_items(qvec, top_k, where=where_filters)\n        if item_hits:\n            response[\"items\"] = self._materialize_hits(item_hits, items_pool)\n            content_sections.append(self._format_item_content(item_hits, store, items=items_pool))\n\n            needs_more, current_query = await self._decide_if_retrieval_needed(\n                current_query,\n                context_queries,\n                retrieved_content=\"\\n\\n\".join(content_sections),\n                llm_client=client,\n            )\n            response[\"next_step_query\"] = current_query\n            if not needs_more:\n                return response\n            # Re-embed with rewritten query\n            qvec = (await client.embed([current_query]))[0]\n\n        # Tier 3: Resources\n        resource_corpus = self._resource_caption_corpus(store, resources=resource_pool)\n        if resource_corpus:\n            res_hits = cosine_topk(qvec, resource_corpus, k=top_k)\n            if res_hits:\n                response[\"resources\"] = self._materialize_hits(res_hits, resource_pool)\n                content_sections.append(self._format_resource_content(res_hits, store, resources=resource_pool))\n\n        return response\n\n    def _materialize_hits(self, hits: Sequence[tuple[str, float]], pool: dict[str, Any]) -> list[dict[str, Any]]:\n        out = []\n        for _id, score in hits:\n            obj = pool.get(_id)\n            if not obj:\n                continue\n            data = self._model_dump_without_embeddings(obj)\n            data[\"score\"] = float(score)\n            out.append(data)\n        return out\n\n    def _format_category_content(\n        self,\n        hits: list[tuple[str, float]],\n        summaries: dict[str, str],\n        store: Database,\n        categories: Mapping[str, Any] | None = None,\n    ) -> str:\n        category_pool = categories if categories is not None else store.memory_category_repo.categories\n        lines = []\n        for cid, score in hits:\n            cat = category_pool.get(cid)\n            if not cat:\n                continue\n            summary = summaries.get(cid) or cat.summary or \"\"\n            lines.append(f\"Category: {cat.name}\\nSummary: {summary}\\nScore: {score:.3f}\")\n        return \"\\n\\n\".join(lines).strip()\n\n    def _format_item_content(\n        self, hits: list[tuple[str, float]], store: Database, items: Mapping[str, Any] | None = None\n    ) -> str:\n        item_pool = items if items is not None else store.memory_item_repo.items\n        lines = []\n        for iid, score in hits:\n            item = item_pool.get(iid)\n            if not item:\n                continue\n            lines.append(f\"Memory Item ({item.memory_type}): {item.summary}\\nScore: {score:.3f}\")\n        return \"\\n\\n\".join(lines).strip()\n\n    def _format_resource_content(\n        self, hits: list[tuple[str, float]], store: Database, resources: Mapping[str, Any] | None = None\n    ) -> str:\n        resource_pool = resources if resources is not None else store.resource_repo.resources\n        lines = []\n        for rid, score in hits:\n            res = resource_pool.get(rid)\n            if not res:\n                continue\n            caption = res.caption or f\"Resource {res.url}\"\n            lines.append(f\"Resource: {caption}\\nScore: {score:.3f}\")\n        return \"\\n\\n\".join(lines).strip()\n\n    def _resource_caption_corpus(\n        self, store: Database, resources: Mapping[str, Any] | None = None\n    ) -> list[tuple[str, list[float]]]:\n        resource_pool = resources if resources is not None else store.resource_repo.resources\n        corpus: list[tuple[str, list[float]]] = []\n        for rid, res in resource_pool.items():\n            if res.embedding:\n                corpus.append((rid, res.embedding))\n        return corpus\n\n    def _extract_judgement(self, raw: str) -> str:\n        if not raw:\n            return \"MORE\"\n        match = re.search(r\"<judgement>(.*?)</judgement>\", raw, re.IGNORECASE | re.DOTALL)\n        if match:\n            token = match.group(1).strip().upper()\n            if \"ENOUGH\" in token:\n                return \"ENOUGH\"\n            if \"MORE\" in token:\n                return \"MORE\"\n        upper = raw.strip().upper()\n        if \"ENOUGH\" in upper:\n            return \"ENOUGH\"\n        return \"MORE\"\n\n    async def _llm_based_retrieve(\n        self,\n        query: str,\n        top_k: int,\n        context_queries: list[dict[str, Any]] | None,\n        ctx: Context,\n        store: Database,\n        llm_client: Any | None = None,\n        where: Mapping[str, Any] | None = None,\n    ) -> dict[str, Any]:\n        \"\"\"\n        LLM-based retrieval that uses language model to search and rank results\n        in a hierarchical manner, with query rewriting and judging at each tier.\n\n        Flow:\n        1. Search categories with LLM, judge + rewrite query\n        2. If needs more, search items from relevant categories, judge + rewrite\n        3. If needs more, search resources related to context\n        \"\"\"\n        where_filters = self._normalize_where(where)\n        category_pool = store.memory_category_repo.list_categories(where_filters)\n        items_pool = store.memory_item_repo.list_items(where_filters)\n        relations = store.category_item_repo.list_relations(where_filters)\n        resource_pool = store.resource_repo.list_resources(where_filters)\n        current_query = query\n        client = llm_client or self._get_llm_client()\n        response: dict[str, Any] = {\"resources\": [], \"items\": [], \"categories\": [], \"next_step_query\": None}\n        content_sections: list[str] = []\n\n        # Tier 1: Search and rank categories\n        category_hits = await self._llm_rank_categories(\n            current_query,\n            top_k,\n            ctx,\n            store,\n            llm_client=client,\n            categories=category_pool,\n        )\n        if category_hits:\n            response[\"categories\"] = category_hits\n            content_sections.append(self._format_llm_category_content(category_hits))\n\n            needs_more, current_query = await self._decide_if_retrieval_needed(\n                current_query,\n                context_queries,\n                retrieved_content=\"\\n\\n\".join(content_sections),\n                llm_client=client,\n            )\n            response[\"next_step_query\"] = current_query\n            if not needs_more:\n                return response\n\n        # Tier 2: Search memory items from relevant categories\n        relevant_category_ids = [cat[\"id\"] for cat in category_hits]\n        item_hits = await self._llm_rank_items(\n            current_query,\n            top_k,\n            relevant_category_ids,\n            category_hits,\n            ctx,\n            store,\n            llm_client=client,\n            categories=category_pool,\n            items=items_pool,\n            relations=relations,\n        )\n        if item_hits:\n            response[\"items\"] = item_hits\n            content_sections.append(self._format_llm_item_content(item_hits))\n\n            needs_more, current_query = await self._decide_if_retrieval_needed(\n                current_query,\n                context_queries,\n                retrieved_content=\"\\n\\n\".join(content_sections),\n                llm_client=client,\n            )\n            response[\"next_step_query\"] = current_query\n            if not needs_more:\n                return response\n\n        # Tier 3: Search resources related to the context\n        resource_hits = await self._llm_rank_resources(\n            current_query,\n            top_k,\n            category_hits,\n            item_hits,\n            ctx,\n            store,\n            llm_client=client,\n            items=items_pool,\n            resources=resource_pool,\n        )\n        if resource_hits:\n            response[\"resources\"] = resource_hits\n            content_sections.append(self._format_llm_resource_content(resource_hits))\n\n        return response\n\n    def _format_categories_for_llm(\n        self,\n        store: Database,\n        category_ids: list[str] | None = None,\n        categories: Mapping[str, Any] | None = None,\n    ) -> str:\n        \"\"\"Format categories for LLM consumption\"\"\"\n        categories_to_format = categories if categories is not None else store.memory_category_repo.categories\n        if category_ids:\n            categories_to_format = {cid: cat for cid, cat in categories_to_format.items() if cid in category_ids}\n\n        if not categories_to_format:\n            return \"No categories available.\"\n\n        lines = []\n        for cid, cat in categories_to_format.items():\n            lines.append(f\"ID: {cid}\")\n            lines.append(f\"Name: {cat.name}\")\n            if cat.description:\n                lines.append(f\"Description: {cat.description}\")\n            if cat.summary:\n                lines.append(f\"Summary: {cat.summary}\")\n            lines.append(\"---\")\n\n        return \"\\n\".join(lines)\n\n    def _format_items_for_llm(\n        self,\n        store: Database,\n        category_ids: list[str] | None = None,\n        items: Mapping[str, Any] | None = None,\n        relations: Sequence[Any] | None = None,\n    ) -> str:\n        \"\"\"Format memory items for LLM consumption, optionally filtered by category\"\"\"\n        item_pool = items if items is not None else store.memory_item_repo.items\n        relation_pool = relations if relations is not None else store.category_item_repo.relations\n        items_to_format = []\n        seen_item_ids = set()\n\n        if category_ids:\n            # Get items that belong to the specified categories\n            for rel in relation_pool:\n                if rel.category_id in category_ids:\n                    item = item_pool.get(rel.item_id)\n                    if item and item.id not in seen_item_ids:\n                        items_to_format.append(item)\n                        seen_item_ids.add(item.id)\n        else:\n            items_to_format = list(item_pool.values())\n\n        if not items_to_format:\n            return \"No memory items available.\"\n\n        lines = []\n        for item in items_to_format:\n            lines.append(f\"ID: {item.id}\")\n            lines.append(f\"Type: {item.memory_type}\")\n            lines.append(f\"Summary: {item.summary}\")\n            lines.append(\"---\")\n\n        return \"\\n\".join(lines)\n\n    def _format_resources_for_llm(\n        self,\n        store: Database,\n        item_ids: list[str] | None = None,\n        items: Mapping[str, Any] | None = None,\n        resources: Mapping[str, Any] | None = None,\n    ) -> str:\n        \"\"\"Format resources for LLM consumption, optionally filtered by related items\"\"\"\n        resource_pool = resources if resources is not None else store.resource_repo.resources\n        item_pool = items if items is not None else store.memory_item_repo.items\n        resources_to_format = []\n\n        if item_ids:\n            # Get resources that are related to the specified items\n            resource_ids = {item_pool[iid].resource_id for iid in item_ids if iid in item_pool and iid is not None}\n            resources_to_format = [\n                resource_pool[rid] for rid in resource_ids if rid in resource_pool and rid is not None\n            ]\n        else:\n            resources_to_format = list(resource_pool.values())\n\n        if not resources_to_format:\n            return \"No resources available.\"\n\n        lines = []\n        for res in resources_to_format:\n            lines.append(f\"ID: {res.id}\")\n            lines.append(f\"URL: {res.url}\")\n            lines.append(f\"Modality: {res.modality}\")\n            if res.caption:\n                lines.append(f\"Caption: {res.caption}\")\n            lines.append(\"---\")\n\n        return \"\\n\".join(lines)\n\n    async def _llm_rank_categories(\n        self,\n        query: str,\n        top_k: int,\n        ctx: Context,\n        store: Database,\n        llm_client: Any | None = None,\n        categories: Mapping[str, Any] | None = None,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Use LLM to rank categories based on query relevance\"\"\"\n        category_pool = categories if categories is not None else store.memory_category_repo.categories\n        if not category_pool:\n            return []\n\n        categories_data = self._format_categories_for_llm(store, categories=category_pool)\n        prompt = LLM_CATEGORY_RANKER_PROMPT.format(\n            query=self._escape_prompt_value(query),\n            top_k=top_k,\n            categories_data=self._escape_prompt_value(categories_data),\n        )\n\n        client = llm_client or self._get_llm_client()\n        llm_response = await client.chat(prompt)\n        return self._parse_llm_category_response(llm_response, store, categories=category_pool)\n\n    async def _llm_rank_items(\n        self,\n        query: str,\n        top_k: int,\n        category_ids: list[str],\n        category_hits: list[dict[str, Any]],\n        ctx: Context,\n        store: Database,\n        llm_client: Any | None = None,\n        categories: Mapping[str, Any] | None = None,\n        items: Mapping[str, Any] | None = None,\n        relations: Sequence[Any] | None = None,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Use LLM to rank memory items from relevant categories\"\"\"\n        if not category_ids:\n            print(\"[LLM Rank Items] No category_ids provided\")\n            return []\n\n        item_pool = items if items is not None else store.memory_item_repo.items\n        items_data = self._format_items_for_llm(store, category_ids, items=item_pool, relations=relations)\n        if items_data == \"No memory items available.\":\n            return []\n\n        # Format relevant categories for context\n        relevant_categories_info = \"\\n\".join([\n            f\"- {cat['name']}: {cat.get('summary', cat.get('description', ''))}\" for cat in category_hits\n        ])\n\n        prompt = LLM_ITEM_RANKER_PROMPT.format(\n            query=self._escape_prompt_value(query),\n            top_k=top_k,\n            relevant_categories=self._escape_prompt_value(relevant_categories_info),\n            items_data=self._escape_prompt_value(items_data),\n        )\n\n        client = llm_client or self._get_llm_client()\n        llm_response = await client.chat(prompt)\n        return self._parse_llm_item_response(llm_response, store, items=item_pool)\n\n    async def _llm_rank_resources(\n        self,\n        query: str,\n        top_k: int,\n        category_hits: list[dict[str, Any]],\n        item_hits: list[dict[str, Any]],\n        ctx: Context,\n        store: Database,\n        llm_client: Any | None = None,\n        items: Mapping[str, Any] | None = None,\n        resources: Mapping[str, Any] | None = None,\n    ) -> list[dict[str, Any]]:\n        \"\"\"Use LLM to rank resources related to the context\"\"\"\n        # Get item IDs to filter resources\n        item_ids = [item[\"id\"] for item in item_hits]\n        if not item_ids:\n            return []\n\n        item_pool = items if items is not None else store.memory_item_repo.items\n        resource_pool = resources if resources is not None else store.resource_repo.resources\n        resources_data = self._format_resources_for_llm(store, item_ids, items=item_pool, resources=resource_pool)\n        if resources_data == \"No resources available.\":\n            return []\n\n        # Build context info\n        context_parts = []\n        if category_hits:\n            context_parts.append(\"Relevant Categories:\")\n            context_parts.extend([f\"- {cat['name']}\" for cat in category_hits])\n        if item_hits:\n            context_parts.append(\"\\nRelevant Memory Items:\")\n            context_parts.extend([f\"- {item.get('summary', '')[:100]}...\" for item in item_hits[:3]])\n\n        context_info = \"\\n\".join(context_parts)\n        prompt = LLM_RESOURCE_RANKER_PROMPT.format(\n            query=self._escape_prompt_value(query),\n            top_k=top_k,\n            context_info=self._escape_prompt_value(context_info),\n            resources_data=self._escape_prompt_value(resources_data),\n        )\n\n        client = llm_client or self._get_llm_client()\n        llm_response = await client.chat(prompt)\n        return self._parse_llm_resource_response(llm_response, store, resources=resource_pool)\n\n    def _parse_llm_category_response(\n        self, raw_response: str, store: Database, categories: Mapping[str, Any] | None = None\n    ) -> list[dict[str, Any]]:\n        \"\"\"Parse LLM category ranking response\"\"\"\n        category_pool = categories if categories is not None else store.memory_category_repo.categories\n        results = []\n        try:\n            json_blob = self._extract_json_blob(raw_response)\n            parsed = json.loads(json_blob)\n\n            if \"categories\" in parsed and isinstance(parsed[\"categories\"], list):\n                category_ids = parsed[\"categories\"]\n                # Return categories in the order provided by LLM (already sorted by relevance)\n                for cat_id in category_ids:\n                    if isinstance(cat_id, str):\n                        cat = category_pool.get(cat_id)\n                        if cat:\n                            cat_data = self._model_dump_without_embeddings(cat)\n                            results.append(cat_data)\n        except Exception as e:\n            logger.warning(f\"Failed to parse LLM category ranking response: {e}\")\n\n        return results\n\n    def _parse_llm_item_response(\n        self, raw_response: str, store: Database, items: Mapping[str, Any] | None = None\n    ) -> list[dict[str, Any]]:\n        \"\"\"Parse LLM item ranking response\"\"\"\n        item_pool = items if items is not None else store.memory_item_repo.items\n        results = []\n        try:\n            json_blob = self._extract_json_blob(raw_response)\n            parsed = json.loads(json_blob)\n\n            if \"items\" in parsed and isinstance(parsed[\"items\"], list):\n                item_ids = parsed[\"items\"]\n                # Return items in the order provided by LLM (already sorted by relevance)\n                for item_id in item_ids:\n                    if isinstance(item_id, str):\n                        mem_item = item_pool.get(item_id)\n                        if mem_item:\n                            item_data = self._model_dump_without_embeddings(mem_item)\n                            results.append(item_data)\n        except Exception as e:\n            logger.warning(f\"Failed to parse LLM item ranking response: {e}\")\n\n        return results\n\n    def _parse_llm_resource_response(\n        self, raw_response: str, store: Database, resources: Mapping[str, Any] | None = None\n    ) -> list[dict[str, Any]]:\n        \"\"\"Parse LLM resource ranking response\"\"\"\n        resource_pool = resources if resources is not None else store.resource_repo.resources\n        results = []\n        try:\n            json_blob = self._extract_json_blob(raw_response)\n            parsed = json.loads(json_blob)\n\n            if \"resources\" in parsed and isinstance(parsed[\"resources\"], list):\n                resource_ids = parsed[\"resources\"]\n                # Return resources in the order provided by LLM (already sorted by relevance)\n                for res_id in resource_ids:\n                    if isinstance(res_id, str):\n                        res = resource_pool.get(res_id)\n                        if res:\n                            res_data = self._model_dump_without_embeddings(res)\n                            results.append(res_data)\n        except Exception as e:\n            logger.warning(f\"Failed to parse LLM resource ranking response: {e}\")\n\n        return results\n\n    def _format_llm_category_content(self, hits: list[dict[str, Any]]) -> str:\n        \"\"\"Format LLM-ranked category content for judger\"\"\"\n        lines = []\n        for cat in hits:\n            summary = cat.get(\"summary\", \"\") or cat.get(\"description\", \"\")\n            lines.append(f\"Category: {cat['name']}\\nSummary: {summary}\")\n        return \"\\n\\n\".join(lines).strip()\n\n    def _format_llm_item_content(self, hits: list[dict[str, Any]]) -> str:\n        \"\"\"Format LLM-ranked item content for judger\"\"\"\n        lines = []\n        for item in hits:\n            lines.append(f\"Memory Item ({item['memory_type']}): {item['summary']}\")\n        return \"\\n\\n\".join(lines).strip()\n\n    def _format_llm_resource_content(self, hits: list[dict[str, Any]]) -> str:\n        \"\"\"Format LLM-ranked resource content for judger\"\"\"\n        lines = []\n        for res in hits:\n            caption = res.get(\"caption\", \"\") or f\"Resource {res['url']}\"\n            lines.append(f\"Resource: {caption}\")\n        return \"\\n\\n\".join(lines).strip()\n"
  },
  {
    "path": "src/memu/app/service.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Callable, Mapping\nfrom dataclasses import dataclass, field\nfrom typing import Any, Literal, TypeVar\n\nfrom pydantic import BaseModel\n\nfrom memu.app.crud import CRUDMixin\nfrom memu.app.memorize import MemorizeMixin\nfrom memu.app.retrieve import RetrieveMixin\nfrom memu.app.settings import (\n    BlobConfig,\n    CategoryConfig,\n    DatabaseConfig,\n    LLMConfig,\n    LLMProfilesConfig,\n    MemorizeConfig,\n    RetrieveConfig,\n    UserConfig,\n)\nfrom memu.blob.local_fs import LocalFS\nfrom memu.database.factory import build_database\nfrom memu.database.interfaces import Database\nfrom memu.llm.http_client import HTTPLLMClient\nfrom memu.llm.wrapper import (\n    LLMCallMetadata,\n    LLMClientWrapper,\n    LLMInterceptorHandle,\n    LLMInterceptorRegistry,\n)\nfrom memu.workflow.interceptor import WorkflowInterceptorHandle, WorkflowInterceptorRegistry\nfrom memu.workflow.pipeline import PipelineManager\nfrom memu.workflow.runner import WorkflowRunner, resolve_workflow_runner\nfrom memu.workflow.step import WorkflowState, WorkflowStep\n\nTConfigModel = TypeVar(\"TConfigModel\", bound=BaseModel)\n\n\n@dataclass\nclass Context:\n    categories_ready: bool = False\n    category_ids: list[str] = field(default_factory=list)\n    category_name_to_id: dict[str, str] = field(default_factory=dict)\n    category_init_task: asyncio.Task | None = None\n\n\nclass MemoryService(MemorizeMixin, RetrieveMixin, CRUDMixin):\n    def __init__(\n        self,\n        *,\n        llm_profiles: LLMProfilesConfig | dict[str, Any] | None = None,\n        blob_config: BlobConfig | dict[str, Any] | None = None,\n        database_config: DatabaseConfig | dict[str, Any] | None = None,\n        memorize_config: MemorizeConfig | dict[str, Any] | None = None,\n        retrieve_config: RetrieveConfig | dict[str, Any] | None = None,\n        workflow_runner: WorkflowRunner | str | None = None,\n        user_config: UserConfig | dict[str, Any] | None = None,\n    ):\n        self.llm_profiles = self._validate_config(llm_profiles, LLMProfilesConfig)\n        self.user_config = self._validate_config(user_config, UserConfig)\n        self.user_model = self.user_config.model\n        self.llm_config = self._validate_config(self.llm_profiles.default, LLMConfig)\n        self.blob_config = self._validate_config(blob_config, BlobConfig)\n        self.database_config = self._validate_config(database_config, DatabaseConfig)\n        self.memorize_config = self._validate_config(memorize_config, MemorizeConfig)\n        self.retrieve_config = self._validate_config(retrieve_config, RetrieveConfig)\n\n        self.fs = LocalFS(self.blob_config.resources_dir)\n        self.category_configs: list[CategoryConfig] = list(self.memorize_config.memory_categories or [])\n        self.category_config_map: dict[str, CategoryConfig] = {cfg.name: cfg for cfg in self.category_configs}\n        self._category_prompt_str = self._format_categories_for_prompt(self.category_configs)\n\n        self._context = Context(categories_ready=not bool(self.category_configs))\n\n        self.database: Database = build_database(\n            config=self.database_config,\n            user_model=self.user_model,\n        )\n        # We need the concrete user scope (user_id: xxx) to initialize the categories\n        # self._start_category_initialization(self._context, self.database)\n\n        # Initialize client caches (lazy creation on first use)\n        self._llm_clients: dict[str, Any] = {}\n        self._llm_interceptors = LLMInterceptorRegistry()\n        self._workflow_interceptors = WorkflowInterceptorRegistry()\n\n        self._workflow_runner = resolve_workflow_runner(workflow_runner)\n\n        self._pipelines = PipelineManager(\n            available_capabilities={\"llm\", \"vector\", \"db\", \"io\", \"vision\"},\n            llm_profiles=set(self.llm_profiles.profiles.keys()),\n        )\n        self._register_pipelines()\n\n    def _init_llm_client(self, config: LLMConfig | None = None) -> Any:\n        \"\"\"Initialize LLM client based on configuration.\"\"\"\n        cfg = config or self.llm_config\n        backend = cfg.client_backend\n        if backend == \"sdk\":\n            from memu.llm.openai_sdk import OpenAISDKClient\n\n            return OpenAISDKClient(\n                base_url=cfg.base_url,\n                api_key=cfg.api_key,\n                chat_model=cfg.chat_model,\n                embed_model=cfg.embed_model,\n                embed_batch_size=cfg.embed_batch_size,\n            )\n        elif backend == \"httpx\":\n            return HTTPLLMClient(\n                base_url=cfg.base_url,\n                api_key=cfg.api_key,\n                chat_model=cfg.chat_model,\n                provider=cfg.provider,\n                endpoint_overrides=cfg.endpoint_overrides,\n                embed_model=cfg.embed_model,\n            )\n        elif backend == \"lazyllm_backend\":\n            from memu.llm.lazyllm_client import LazyLLMClient\n\n            return LazyLLMClient(\n                llm_source=cfg.lazyllm_source.llm_source or cfg.lazyllm_source.source,\n                vlm_source=cfg.lazyllm_source.vlm_source or cfg.lazyllm_source.source,\n                embed_source=cfg.lazyllm_source.embed_source or cfg.lazyllm_source.source,\n                stt_source=cfg.lazyllm_source.stt_source or cfg.lazyllm_source.source,\n                chat_model=cfg.chat_model,\n                embed_model=cfg.embed_model,\n                vlm_model=cfg.lazyllm_source.vlm_model,\n                stt_model=cfg.lazyllm_source.stt_model,\n            )\n        else:\n            msg = f\"Unknown llm_client_backend '{cfg.client_backend}'\"\n            raise ValueError(msg)\n\n    def _get_llm_base_client(self, profile: str | None = None) -> Any:\n        \"\"\"\n        Lazily initialize and cache LLM clients per profile to avoid eager network setup.\n        \"\"\"\n        name = profile or \"default\"\n        client = self._llm_clients.get(name)\n        if client is not None:\n            return client\n        cfg: LLMConfig | None = self.llm_profiles.profiles.get(name)\n        if cfg is None:\n            msg = f\"Unknown llm profile '{name}'\"\n            raise KeyError(msg)\n        client = self._init_llm_client(cfg)\n        self._llm_clients[name] = client\n        return client\n\n    @staticmethod\n    def _llm_call_metadata(profile: str, step_context: Mapping[str, Any] | None) -> LLMCallMetadata:\n        if not isinstance(step_context, Mapping):\n            return LLMCallMetadata(profile)\n        operation = None\n        for key in (\"operation\", \"workflow_name\"):\n            value = step_context.get(key)\n            if isinstance(value, str) and value.strip():\n                operation = value.strip()\n                break\n        step_id = step_context.get(\"step_id\") if isinstance(step_context.get(\"step_id\"), str) else None\n        trace_id = step_context.get(\"trace_id\") if isinstance(step_context.get(\"trace_id\"), str) else None\n        tags = step_context.get(\"tags\") if isinstance(step_context.get(\"tags\"), Mapping) else None\n        return LLMCallMetadata(profile=profile, operation=operation, step_id=step_id, trace_id=trace_id, tags=tags)\n\n    def _wrap_llm_client(\n        self,\n        client: Any,\n        *,\n        profile: str | None = None,\n        step_context: Mapping[str, Any] | None = None,\n    ) -> Any:\n        cfg: LLMConfig | None = self.llm_profiles.profiles.get(profile or \"default\")\n        provider = cfg.provider if cfg is not None else None\n        metadata = self._llm_call_metadata(profile or \"default\", step_context)\n        return LLMClientWrapper(\n            client,\n            registry=self._llm_interceptors,\n            metadata=metadata,\n            provider=provider,\n            chat_model=getattr(client, \"chat_model\", None),\n            embed_model=getattr(client, \"embed_model\", None),\n        )\n\n    def _get_llm_client(self, profile: str | None = None, step_context: Mapping[str, Any] | None = None) -> Any:\n        base_client = self._get_llm_base_client(profile)\n        return self._wrap_llm_client(base_client, profile=profile, step_context=step_context)\n\n    @property\n    def llm_client(self) -> Any:\n        \"\"\"Default LLM client (lazy).\"\"\"\n        return self._get_llm_client()\n\n    @property\n    def workflow_runner(self) -> WorkflowRunner:\n        \"\"\"Current workflow runner backend.\"\"\"\n        return self._workflow_runner\n\n    @staticmethod\n    def _llm_profile_from_context(\n        step_context: Mapping[str, Any] | None, task: Literal[\"chat\", \"embedding\"] = \"chat\"\n    ) -> str | None:\n        if not isinstance(step_context, Mapping):\n            return None\n        step_cfg = step_context.get(\"step_config\")\n        if not isinstance(step_cfg, Mapping):\n            return None\n        if task == \"chat\":\n            profile = step_cfg.get(\"chat_llm_profile\", step_cfg.get(\"llm_profile\"))\n        elif task == \"embedding\":\n            profile = step_cfg.get(\"embed_llm_profile\", step_cfg.get(\"llm_profile\"))\n        else:\n            raise ValueError(task)\n        if isinstance(profile, str) and profile.strip():\n            return profile.strip()\n        return None\n\n    def _get_step_llm_client(self, step_context: Mapping[str, Any] | None) -> Any:\n        profile = self._llm_profile_from_context(step_context, task=\"chat\") or \"default\"\n        return self._get_llm_client(profile, step_context=step_context)\n\n    def _get_step_embedding_client(self, step_context: Mapping[str, Any] | None) -> Any:\n        profile = self._llm_profile_from_context(step_context, task=\"embedding\") or \"embedding\"\n        return self._get_llm_client(profile, step_context=step_context)\n\n    def intercept_before_llm_call(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        priority: int = 0,\n        where: Mapping[str, Any] | Callable[..., Any] | None = None,\n    ) -> LLMInterceptorHandle:\n        return self._llm_interceptors.register_before(fn, name=name, priority=priority, where=where)\n\n    def intercept_after_llm_call(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        priority: int = 0,\n        where: Mapping[str, Any] | Callable[..., Any] | None = None,\n    ) -> LLMInterceptorHandle:\n        return self._llm_interceptors.register_after(fn, name=name, priority=priority, where=where)\n\n    def intercept_on_error_llm_call(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        priority: int = 0,\n        where: Mapping[str, Any] | Callable[..., Any] | None = None,\n    ) -> LLMInterceptorHandle:\n        return self._llm_interceptors.register_on_error(fn, name=name, priority=priority, where=where)\n\n    def intercept_before_workflow_step(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n    ) -> WorkflowInterceptorHandle:\n        \"\"\"\n        Register an interceptor to be called before each workflow step.\n\n        The interceptor receives (step_context: WorkflowStepContext, state: WorkflowState).\n        \"\"\"\n        return self._workflow_interceptors.register_before(fn, name=name)\n\n    def intercept_after_workflow_step(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n    ) -> WorkflowInterceptorHandle:\n        \"\"\"\n        Register an interceptor to be called after each workflow step.\n\n        The interceptor receives (step_context: WorkflowStepContext, state: WorkflowState).\n        \"\"\"\n        return self._workflow_interceptors.register_after(fn, name=name)\n\n    def intercept_on_error_workflow_step(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n    ) -> WorkflowInterceptorHandle:\n        \"\"\"\n        Register an interceptor to be called when a workflow step raises an exception.\n\n        The interceptor receives (step_context: WorkflowStepContext, state: WorkflowState, error: Exception).\n        \"\"\"\n        return self._workflow_interceptors.register_on_error(fn, name=name)\n\n    def _get_context(self) -> Context:\n        return self._context\n\n    def _get_database(self) -> Database:\n        return self.database\n\n    def _provider_summary(self) -> dict[str, Any]:\n        vector_provider = None\n        if self.database_config.vector_index:\n            vector_provider = self.database_config.vector_index.provider\n        return {\n            \"llm_profiles\": list(self.llm_profiles.profiles.keys()),\n            \"storage\": {\n                \"metadata_store\": self.database_config.metadata_store.provider,\n                \"vector_index\": vector_provider,\n            },\n        }\n\n    def _register_pipelines(self) -> None:\n        memo_workflow = self._build_memorize_workflow()\n        memo_initial_keys = self._list_memorize_initial_keys()\n        self._pipelines.register(\"memorize\", memo_workflow, initial_state_keys=memo_initial_keys)\n        rag_workflow = self._build_rag_retrieve_workflow()\n        retrieve_initial_keys = self._list_retrieve_initial_keys()\n        self._pipelines.register(\"retrieve_rag\", rag_workflow, initial_state_keys=retrieve_initial_keys)\n        llm_workflow = self._build_llm_retrieve_workflow()\n        self._pipelines.register(\"retrieve_llm\", llm_workflow, initial_state_keys=retrieve_initial_keys)\n        patch_create_workflow = self._build_create_memory_item_workflow()\n        patch_create_initial_keys = CRUDMixin._list_create_memory_item_initial_keys()\n        self._pipelines.register(\"patch_create\", patch_create_workflow, initial_state_keys=patch_create_initial_keys)\n        patch_update_workflow = self._build_update_memory_item_workflow()\n        patch_update_initial_keys = CRUDMixin._list_update_memory_item_initial_keys()\n        self._pipelines.register(\"patch_update\", patch_update_workflow, initial_state_keys=patch_update_initial_keys)\n        patch_delete_workflow = self._build_delete_memory_item_workflow()\n        patch_delete_initial_keys = CRUDMixin._list_delete_memory_item_initial_keys()\n        self._pipelines.register(\"patch_delete\", patch_delete_workflow, initial_state_keys=patch_delete_initial_keys)\n        crud_list_items_workflow = self._build_list_memory_items_workflow()\n        crud_list_memories_initial_keys = CRUDMixin._list_list_memories_initial_keys()\n        self._pipelines.register(\n            \"crud_list_memory_items\", crud_list_items_workflow, initial_state_keys=crud_list_memories_initial_keys\n        )\n        crud_list_categories_workflow = self._build_list_memory_categories_workflow()\n        self._pipelines.register(\n            \"crud_list_memory_categories\",\n            crud_list_categories_workflow,\n            initial_state_keys=crud_list_memories_initial_keys,\n        )\n        crud_clear_memory_workflow = self._build_clear_memory_workflow()\n        crud_clear_memory_initial_keys = CRUDMixin._list_clear_memories_initial_keys()\n        self._pipelines.register(\n            \"crud_clear_memory\", crud_clear_memory_workflow, initial_state_keys=crud_clear_memory_initial_keys\n        )\n\n    async def _run_workflow(self, workflow_name: str, initial_state: WorkflowState) -> WorkflowState:\n        \"\"\"Execute a workflow through the configured runner backend.\"\"\"\n        steps = self._pipelines.build(workflow_name)\n        runner_context = {\"workflow_name\": workflow_name}\n        return await self._workflow_runner.run(\n            workflow_name,\n            steps,\n            initial_state,\n            runner_context,\n            interceptor_registry=self._workflow_interceptors,\n        )\n\n    @staticmethod\n    def _extract_json_blob(raw: str) -> str:\n        start = raw.find(\"{\")\n        end = raw.rfind(\"}\")\n        if start == -1 or end == -1 or end <= start:\n            msg = \"No JSON object found\"\n            raise ValueError(msg)\n        return raw[start : end + 1]\n\n    @staticmethod\n    def _escape_prompt_value(value: str) -> str:\n        return value.replace(\"{\", \"{{\").replace(\"}\", \"}}\")\n\n    def _model_dump_without_embeddings(self, obj: BaseModel) -> dict[str, Any]:\n        data = obj.model_dump(exclude={\"embedding\"})\n        return data\n\n    @staticmethod\n    def _validate_config(\n        config: Mapping[str, Any] | BaseModel | None,\n        model_type: type[TConfigModel],\n    ) -> TConfigModel:\n        if isinstance(config, model_type):\n            return config\n        if config is None:\n            return model_type()\n        return model_type.model_validate(config)\n\n    def configure_pipeline(self, *, step_id: str, configs: Mapping[str, Any], pipeline: str = \"memorize\") -> int:\n        revision = self._pipelines.config_step(pipeline, step_id, dict(configs))\n        return revision\n\n    def insert_step_after(\n        self,\n        *,\n        target_step_id: str,\n        new_step: WorkflowStep,\n        pipeline: str = \"memorize\",\n    ) -> int:\n        revision = self._pipelines.insert_after(pipeline, target_step_id, new_step)\n        return revision\n\n    def insert_step_before(\n        self,\n        *,\n        target_step_id: str,\n        new_step: WorkflowStep,\n        pipeline: str = \"memorize\",\n    ) -> int:\n        revision = self._pipelines.insert_before(pipeline, target_step_id, new_step)\n        return revision\n\n    def replace_step(\n        self,\n        *,\n        target_step_id: str,\n        new_step: WorkflowStep,\n        pipeline: str = \"memorize\",\n    ) -> int:\n        revision = self._pipelines.replace_step(pipeline, target_step_id, new_step)\n        return revision\n\n    def remove_step(self, *, target_step_id: str, pipeline: str = \"memorize\") -> int:\n        revision = self._pipelines.remove_step(pipeline, target_step_id)\n        return revision\n"
  },
  {
    "path": "src/memu/app/settings.py",
    "content": "from collections.abc import Mapping\nfrom typing import Annotated, Any, Literal\n\nfrom pydantic import AfterValidator, BaseModel, BeforeValidator, Field, RootModel, StringConstraints, model_validator\n\nfrom memu.prompts.category_summary import (\n    DEFAULT_CATEGORY_SUMMARY_PROMPT_ORDINAL,\n)\nfrom memu.prompts.category_summary import (\n    PROMPT as CATEGORY_SUMMARY_PROMPT,\n)\nfrom memu.prompts.memory_type import (\n    DEFAULT_MEMORY_CUSTOM_PROMPT_ORDINAL,\n    DEFAULT_MEMORY_TYPES,\n)\nfrom memu.prompts.memory_type import (\n    PROMPTS as DEFAULT_MEMORY_TYPE_PROMPTS,\n)\n\n\ndef normalize_value(v: str) -> str:\n    if isinstance(v, str):\n        return v.strip().lower()\n    return v\n\n\nNormalize = BeforeValidator(normalize_value)\n\n\ndef _default_memory_types() -> list[str]:\n    return list(DEFAULT_MEMORY_TYPES)\n\n\ndef _default_memory_type_prompts() -> \"dict[str, str | CustomPrompt]\":\n    return dict(DEFAULT_MEMORY_TYPE_PROMPTS)\n\n\nclass PromptBlock(BaseModel):\n    label: str | None = None\n    ordinal: int = Field(default=0)\n    prompt: str | None = None\n\n\nclass CustomPrompt(RootModel[dict[str, PromptBlock]]):\n    root: dict[str, PromptBlock] = Field(default_factory=dict)\n\n    def get(self, key: str, default: PromptBlock | None = None) -> PromptBlock | None:\n        return self.root.get(key, default)\n\n    def items(self) -> list[tuple[str, PromptBlock]]:\n        return list(self.root.items())\n\n\ndef complete_prompt_blocks(prompt: CustomPrompt, default_blocks: Mapping[str, int]) -> CustomPrompt:\n    for key, ordinal in default_blocks.items():\n        if key not in prompt.root:\n            prompt.root[key] = PromptBlock(ordinal=ordinal)\n    return prompt\n\n\nCompleteMemoryTypePrompt = AfterValidator(lambda v: complete_prompt_blocks(v, DEFAULT_MEMORY_CUSTOM_PROMPT_ORDINAL))\n\n\nCompleteCategoryPrompt = AfterValidator(lambda v: complete_prompt_blocks(v, DEFAULT_CATEGORY_SUMMARY_PROMPT_ORDINAL))\n\n\nclass CategoryConfig(BaseModel):\n    name: str\n    description: str = \"\"\n    target_length: int | None = None\n    summary_prompt: str | Annotated[CustomPrompt, CompleteCategoryPrompt] | None = None\n\n\ndef _default_memory_categories() -> list[CategoryConfig]:\n    return [\n        CategoryConfig.model_validate(cat)\n        for cat in (\n            {\"name\": \"personal_info\", \"description\": \"Personal information about the user\"},\n            {\"name\": \"preferences\", \"description\": \"User preferences, likes and dislikes\"},\n            {\"name\": \"relationships\", \"description\": \"Information about relationships with others\"},\n            {\"name\": \"activities\", \"description\": \"Activities, hobbies, and interests\"},\n            {\"name\": \"goals\", \"description\": \"Goals, aspirations, and objectives\"},\n            {\"name\": \"experiences\", \"description\": \"Past experiences and events\"},\n            {\"name\": \"knowledge\", \"description\": \"Knowledge, facts, and learned information\"},\n            {\"name\": \"opinions\", \"description\": \"Opinions, viewpoints, and perspectives\"},\n            {\"name\": \"habits\", \"description\": \"Habits, routines, and patterns\"},\n            {\"name\": \"work_life\", \"description\": \"Work-related information and professional life\"},\n        )\n    ]\n\n\nclass LazyLLMSource(BaseModel):\n    source: str | None = Field(default=None, description=\"default source for lazyllm client backend\")\n    llm_source: str | None = Field(default=None, description=\"LLM source for lazyllm client backend\")\n    embed_source: str | None = Field(default=None, description=\"Embedding source for lazyllm client backend\")\n    vlm_source: str | None = Field(default=None, description=\"VLM source for lazyllm client backend\")\n    stt_source: str | None = Field(default=None, description=\"STT source for lazyllm client backend\")\n    vlm_model: str = Field(default=\"qwen-vl-plus\", description=\"Vision language model for lazyllm client backend\")\n    stt_model: str = Field(default=\"qwen-audio-turbo\", description=\"Speech-to-text model for lazyllm client backend\")\n\n\nclass LLMConfig(BaseModel):\n    provider: str = Field(\n        default=\"openai\",\n        description=\"Identifier for the LLM provider implementation (used by HTTP client backend).\",\n    )\n    base_url: str = Field(default=\"https://api.openai.com/v1\")\n    api_key: str = Field(default=\"OPENAI_API_KEY\")\n    chat_model: str = Field(default=\"gpt-4o-mini\")\n    client_backend: str = Field(\n        default=\"sdk\",\n        description=\"Which LLM client backend to use: 'httpx' (httpx), 'sdk' (official OpenAI), or 'lazyllm_backend' (for more LLM source like Qwen, Doubao, SIliconflow, etc.)\",\n    )\n    lazyllm_source: LazyLLMSource = Field(default=LazyLLMSource())\n    endpoint_overrides: dict[str, str] = Field(\n        default_factory=dict,\n        description=\"Optional overrides for HTTP endpoints (keys: 'chat'/'summary').\",\n    )\n    embed_model: str = Field(\n        default=\"text-embedding-3-small\",\n        description=\"Default embedding model used for vectorization.\",\n    )\n    embed_batch_size: int = Field(\n        default=1,\n        description=\"Maximum batch size for embedding API calls (used by SDK client backends).\",\n    )\n\n    @model_validator(mode=\"after\")\n    def set_provider_defaults(self) -> \"LLMConfig\":\n        if self.provider == \"grok\":\n            # If values match the OpenAI defaults, switch them to Grok defaults\n            if self.base_url == \"https://api.openai.com/v1\":\n                self.base_url = \"https://api.x.ai/v1\"\n            if self.api_key == \"OPENAI_API_KEY\":\n                self.api_key = \"XAI_API_KEY\"\n            if self.chat_model == \"gpt-4o-mini\":\n                self.chat_model = \"grok-2-latest\"\n        return self\n\n\nclass BlobConfig(BaseModel):\n    provider: str = Field(default=\"local\")\n    resources_dir: str = Field(default=\"./data/resources\")\n\n\nclass RetrieveCategoryConfig(BaseModel):\n    enabled: bool = Field(default=True, description=\"Whether to enable category retrieval.\")\n    top_k: int = Field(default=5, description=\"Total number of categories to retrieve.\")\n\n\nclass RetrieveItemConfig(BaseModel):\n    enabled: bool = Field(default=True, description=\"Whether to enable item retrieval.\")\n    top_k: int = Field(default=5, description=\"Total number of items to retrieve.\")\n    # Reference-aware retrieval\n    use_category_references: bool = Field(\n        default=False,\n        description=\"When category retrieval is insufficient, follow [ref:ITEM_ID] citations to fetch referenced items.\",\n    )\n    # Salience-aware retrieval settings\n    ranking: Literal[\"similarity\", \"salience\"] = Field(\n        default=\"similarity\",\n        description=\"Ranking strategy: 'similarity' (cosine only) or 'salience' (weighted by reinforcement + recency).\",\n    )\n    recency_decay_days: float = Field(\n        default=30.0,\n        description=\"Half-life in days for recency decay in salience scoring. After this many days, recency factor is ~0.5.\",\n    )\n\n\nclass RetrieveResourceConfig(BaseModel):\n    enabled: bool = Field(default=True, description=\"Whether to enable resource retrieval.\")\n    top_k: int = Field(default=5, description=\"Total number of resources to retrieve.\")\n\n\nclass RetrieveConfig(BaseModel):\n    \"\"\"Configure retrieval behavior for `MemoryUser.retrieve`.\n\n    Attributes:\n        method: Retrieval strategy. Use \"rag\" for embedding-based vector search or\n            \"llm\" to delegate ranking to the LLM.\n        top_k: Maximum number of results to return per category (and per stage),\n            controlling breadth of the retrieved context.\n    \"\"\"\n\n    method: Annotated[Literal[\"rag\", \"llm\"], Normalize] = \"rag\"\n    # top_k: int = Field(\n    #     default=5,\n    #     description=\"Maximum number of results to return per category.\",\n    # )\n    route_intention: bool = Field(\n        default=True, description=\"Whether to route intention (judge needs retrieval & rewrite query).\"\n    )\n    # route_intention_prompt: str = Field(default=\"\", description=\"User prompt for route intention.\")\n    # route_intention_llm_profile: str = Field(default=\"default\", description=\"LLM profile for route intention.\")\n    category: RetrieveCategoryConfig = Field(default=RetrieveCategoryConfig())\n    item: RetrieveItemConfig = Field(default=RetrieveItemConfig())\n    resource: RetrieveResourceConfig = Field(default=RetrieveResourceConfig())\n    sufficiency_check: bool = Field(default=True, description=\"Whether to check sufficiency after each tier.\")\n    sufficiency_check_prompt: str = Field(default=\"\", description=\"User prompt for sufficiency check.\")\n    sufficiency_check_llm_profile: str = Field(default=\"default\", description=\"LLM profile for sufficiency check.\")\n    llm_ranking_llm_profile: str = Field(default=\"default\", description=\"LLM profile for LLM ranking.\")\n\n\nclass MemorizeConfig(BaseModel):\n    category_assign_threshold: float = Field(default=0.25)\n    multimodal_preprocess_prompts: dict[str, str | CustomPrompt] = Field(\n        default_factory=dict,\n        description=\"Optional mapping of modality -> preprocess system prompt.\",\n    )\n    preprocess_llm_profile: str = Field(default=\"default\", description=\"LLM profile for preprocess.\")\n    memory_types: list[str] = Field(\n        default_factory=_default_memory_types,\n        description=\"Ordered list of memory types (profile/event/knowledge/behavior by default).\",\n    )\n    memory_type_prompts: dict[str, str | Annotated[CustomPrompt, CompleteMemoryTypePrompt]] = Field(\n        default_factory=_default_memory_type_prompts,\n        description=\"User prompt overrides for each memory type extraction.\",\n    )\n    memory_extract_llm_profile: str = Field(default=\"default\", description=\"LLM profile for memory extract.\")\n    memory_categories: list[CategoryConfig] = Field(\n        default_factory=_default_memory_categories,\n        description=\"Global memory category definitions embedded at service startup.\",\n    )\n    # default_category_summary_prompt: str | CustomPrompt = Field(\n    default_category_summary_prompt: str | Annotated[CustomPrompt, CompleteCategoryPrompt] = Field(\n        default=CATEGORY_SUMMARY_PROMPT,\n        description=\"Default system prompt for auto-generated category summaries.\",\n    )\n    default_category_summary_target_length: int = Field(\n        default=400,\n        description=\"Target max length for auto-generated category summaries.\",\n    )\n    category_update_llm_profile: str = Field(default=\"default\", description=\"LLM profile for category summary.\")\n    # Reference tracking for category summaries\n    enable_item_references: bool = Field(\n        default=False,\n        description=\"Enable inline [ref:ITEM_ID] citations in category summaries linking to source memory items.\",\n    )\n    enable_item_reinforcement: bool = Field(\n        default=False,\n        description=\"Enable reinforcement tracking for memory items.\",\n    )\n\n\nclass PatchConfig(BaseModel):\n    pass\n\n\nclass DefaultUserModel(BaseModel):\n    user_id: str | None = None\n    # Agent/session scoping for multi-agent and multi-session memory filtering\n    # agent_id: str | None = None\n    # session_id: str | None = None\n\n\nclass UserConfig(BaseModel):\n    model: type[BaseModel] = Field(default=DefaultUserModel)\n\n\nKey = Annotated[str, StringConstraints(min_length=1)]\n\n\nclass LLMProfilesConfig(RootModel[dict[Key, LLMConfig]]):\n    root: dict[str, LLMConfig] = Field(default_factory=lambda: {\"default\": LLMConfig()})\n\n    def get(self, key: str, default: LLMConfig | None = None) -> LLMConfig | None:\n        return self.root.get(key, default)\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def ensure_default(cls, data: Any) -> Any:\n        # if data is None:\n        #     return {\"default\": LLMConfig()}\n        # if isinstance(data, dict) and \"default\" not in data:\n        #     data = dict(data)\n        #     data[\"default\"] = LLMConfig()\n        # return data\n        if data is None:\n            data = {}\n        elif isinstance(data, dict):\n            data = dict(data)\n        else:\n            return data\n        if \"default\" not in data:\n            data[\"default\"] = LLMConfig()\n        if \"embedding\" not in data:\n            data[\"embedding\"] = data[\"default\"]\n        return data\n\n    @property\n    def profiles(self) -> dict[str, LLMConfig]:\n        return self.root\n\n    @property\n    def default(self) -> LLMConfig:\n        return self.root.get(\"default\", LLMConfig())\n\n\nclass MetadataStoreConfig(BaseModel):\n    provider: Annotated[Literal[\"inmemory\", \"postgres\", \"sqlite\"], Normalize] = \"inmemory\"\n    ddl_mode: Annotated[Literal[\"create\", \"validate\"], Normalize] = \"create\"\n    dsn: str | None = Field(default=None, description=\"Database connection string (required for postgres/sqlite).\")\n\n\nclass VectorIndexConfig(BaseModel):\n    provider: Annotated[Literal[\"bruteforce\", \"pgvector\", \"none\"], Normalize] = \"bruteforce\"\n    dsn: str | None = Field(default=None, description=\"Postgres connection string when provider=pgvector.\")\n\n\nclass DatabaseConfig(BaseModel):\n    metadata_store: MetadataStoreConfig = Field(default_factory=MetadataStoreConfig)\n    vector_index: VectorIndexConfig | None = Field(default=None)\n\n    def model_post_init(self, __context: Any) -> None:\n        if self.vector_index is None:\n            if self.metadata_store.provider == \"postgres\":\n                self.vector_index = VectorIndexConfig(provider=\"pgvector\", dsn=self.metadata_store.dsn)\n            else:\n                self.vector_index = VectorIndexConfig(provider=\"bruteforce\")\n        elif self.vector_index.provider == \"pgvector\" and self.vector_index.dsn is None:\n            self.vector_index = self.vector_index.model_copy(update={\"dsn\": self.metadata_store.dsn})\n"
  },
  {
    "path": "src/memu/blob/__init__.py",
    "content": ""
  },
  {
    "path": "src/memu/blob/local_fs.py",
    "content": "from __future__ import annotations\n\nimport pathlib\nimport shutil\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\n\n\nclass LocalFS:\n    def __init__(self, base_dir: str):\n        self.base = pathlib.Path(base_dir)\n        self.base.mkdir(parents=True, exist_ok=True)\n\n    def _get_filename_from_url(self, url: str, modality: str) -> str:\n        \"\"\"\n        Extract a clean filename from URL, handling query parameters.\n\n        Args:\n            url: The URL to parse\n            modality: The resource modality (for extension inference)\n\n        Returns:\n            A clean filename without query parameters\n        \"\"\"\n        parsed = urlparse(url)\n        path = parsed.path\n\n        # Get base filename from path\n        filename = pathlib.Path(path).name\n\n        # If filename has no extension or is just a script name (like grab.php),\n        # try to get the real extension from query parameters or use modality\n        if not filename or \".\" not in filename or filename.endswith(\".php\"):\n            # Check for 'type' parameter in query string (e.g., ?type=mp3)\n            query_params = parse_qs(parsed.query)\n            if \"type\" in query_params:\n                ext = query_params[\"type\"][0]\n                # Generate a filename based on the ID if available\n                filename = f\"audio_{query_params['id'][0]}.{ext}\" if \"id\" in query_params else f\"resource.{ext}\"\n            else:\n                # Use modality to infer extension\n                ext_map = {\n                    \"audio\": \"mp3\",\n                    \"video\": \"mp4\",\n                    \"image\": \"jpg\",\n                    \"document\": \"txt\",\n                }\n                ext = ext_map.get(modality, \"bin\")\n                filename = f\"resource.{ext}\"\n\n        # Remove any remaining query parameters from filename\n        filename = filename.split(\"?\")[0]\n\n        return filename\n\n    async def fetch(self, url: str, modality: str) -> tuple[str, str | None]:\n        # Local path\n        p = pathlib.Path(url)\n        if p.exists():\n            dst = self.base / p.name\n            if str(p.resolve()) != str(dst.resolve()):\n                shutil.copyfile(p, dst)\n            text = None\n            if modality in (\"conversation\", \"text\", \"document\"):\n                text = dst.read_text(encoding=\"utf-8\")\n            return str(dst), text\n\n        # HTTP - get clean filename\n        filename = self._get_filename_from_url(url, modality)\n        dst = self.base / filename\n\n        async with httpx.AsyncClient(timeout=60) as client:\n            r = await client.get(url)\n            r.raise_for_status()\n            dst.write_bytes(r.content)\n        text = None\n        if modality in (\"conversation\", \"text\", \"document\"):\n            text = r.text\n        return str(dst), text\n"
  },
  {
    "path": "src/memu/client/__init__.py",
    "content": "\"\"\"\nmemU Client Wrapper for Auto-Recall Memory Injection.\n\nThis module provides optional wrappers around OpenAI/Anthropic clients\nthat automatically inject recalled memories into prompts.\n\nUsage:\n    from memu.client import wrap_openai\n    from openai import OpenAI\n\n    client = OpenAI()\n    service = MemoryService(...)\n\n    # Wrap the client for auto-recall\n    wrapped_client = wrap_openai(client, service, user_id=\"user123\")\n\n    # Now all chat completions automatically include relevant memories\n    response = wrapped_client.chat.completions.create(\n        model=\"gpt-4\",\n        messages=[{\"role\": \"user\", \"content\": \"What's my favorite drink?\"}]\n    )\n\"\"\"\n\nfrom memu.client.openai_wrapper import MemuOpenAIWrapper, wrap_openai\n\n__all__ = [\"MemuOpenAIWrapper\", \"wrap_openai\"]\n"
  },
  {
    "path": "src/memu/client/openai_wrapper.py",
    "content": "\"\"\"\nOpenAI Client Wrapper for Auto-Recall Memory Injection.\n\nWraps OpenAI client to automatically inject recalled memories into prompts.\nFully opt-in and backward compatible.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom typing import TYPE_CHECKING, Any\n\nif TYPE_CHECKING:\n    from memu.app.service import MemoryService\n\n\nclass MemuChatCompletions:\n    \"\"\"Wrapper for chat.completions that injects recalled memories.\"\"\"\n\n    def __init__(\n        self,\n        original_completions,\n        service: MemoryService,\n        user_data: dict[str, Any],\n        ranking: str = \"salience\",\n        top_k: int = 5,\n    ):\n        self._original = original_completions\n        self._service = service\n        self._user_data = user_data\n        self._ranking = ranking\n        self._top_k = top_k\n\n    def _extract_user_query(self, messages: list[dict]) -> str:\n        \"\"\"Extract the most recent user message.\"\"\"\n        for msg in reversed(messages):\n            if msg.get(\"role\") == \"user\":\n                content = msg.get(\"content\", \"\")\n                if isinstance(content, str):\n                    return content\n                # Handle content as list (vision models)\n                if isinstance(content, list):\n                    for part in content:\n                        if isinstance(part, dict) and part.get(\"type\") == \"text\":\n                            return part.get(\"text\", \"\")\n        return \"\"\n\n    def _inject_memories(self, messages: list[dict], memories: list[dict]) -> list[dict]:\n        \"\"\"Inject recalled memories into the system prompt.\"\"\"\n        if not memories:\n            return messages\n\n        # Format memories as context\n        memory_lines = [f\"- {m.get('summary', '')}\" for m in memories]\n        recall_context = (\n            \"\\n\\n<memu_context>\\n\"\n            \"Relevant context about the user (use only if relevant to the query):\\n\"\n            + \"\\n\".join(memory_lines)\n            + \"\\n</memu_context>\"\n        )\n\n        # Clone messages to avoid mutation\n        messages = [dict(m) for m in messages]\n\n        # Inject into system message or create one\n        if messages and messages[0].get(\"role\") == \"system\":\n            messages[0][\"content\"] = messages[0][\"content\"] + recall_context\n        else:\n            messages.insert(0, {\"role\": \"system\", \"content\": recall_context.lstrip(\"\\n\")})\n\n        return messages\n\n    async def _retrieve_memories(self, query: str) -> list[dict]:\n        \"\"\"Retrieve relevant memories for the query.\"\"\"\n        try:\n            result = await self._service.retrieve(\n                queries=[{\"role\": \"user\", \"content\": query}],\n                where=self._user_data,\n            )\n            return result.get(\"items\", [])\n        except Exception:\n            # Fail silently - don't break the LLM call\n            return []\n\n    def create(self, **kwargs) -> Any:\n        \"\"\"Wrapped create method with auto-recall injection.\"\"\"\n        messages = kwargs.get(\"messages\", [])\n        query = self._extract_user_query(messages)\n\n        if query:\n            # Run async retrieval in sync context\n            try:\n                loop = asyncio.get_event_loop()\n                if loop.is_running():\n                    # Already in async context, create task\n                    import concurrent.futures\n\n                    with concurrent.futures.ThreadPoolExecutor() as pool:\n                        memories = pool.submit(asyncio.run, self._retrieve_memories(query)).result()\n                else:\n                    memories = loop.run_until_complete(self._retrieve_memories(query))\n            except RuntimeError:\n                memories = asyncio.run(self._retrieve_memories(query))\n\n            if memories:\n                kwargs[\"messages\"] = self._inject_memories(messages, memories)\n\n        return self._original.create(**kwargs)\n\n    async def acreate(self, **kwargs) -> Any:\n        \"\"\"Async wrapped create method with auto-recall injection.\"\"\"\n        messages = kwargs.get(\"messages\", [])\n        query = self._extract_user_query(messages)\n\n        if query:\n            memories = await self._retrieve_memories(query)\n            if memories:\n                kwargs[\"messages\"] = self._inject_memories(messages, memories)\n\n        # Call original async method if available\n        if hasattr(self._original, \"acreate\"):\n            return await self._original.acreate(**kwargs)\n        return self._original.create(**kwargs)\n\n    def __getattr__(self, name: str) -> Any:\n        \"\"\"Proxy all other attributes to original.\"\"\"\n        return getattr(self._original, name)\n\n\nclass MemuChat:\n    \"\"\"Wrapper for chat namespace.\"\"\"\n\n    def __init__(\n        self,\n        original_chat,\n        service: MemoryService,\n        user_data: dict[str, Any],\n        ranking: str = \"salience\",\n        top_k: int = 5,\n    ):\n        self._original = original_chat\n        self.completions = MemuChatCompletions(\n            original_chat.completions,\n            service,\n            user_data,\n            ranking,\n            top_k,\n        )\n\n    def __getattr__(self, name: str) -> Any:\n        \"\"\"Proxy all other attributes to original.\"\"\"\n        return getattr(self._original, name)\n\n\nclass MemuOpenAIWrapper:\n    \"\"\"\n    Wrapper around OpenAI client that auto-injects recalled memories.\n\n    Usage:\n        from openai import OpenAI\n        from memu.client import MemuOpenAIWrapper\n\n        client = OpenAI()\n        service = MemoryService(...)\n\n        wrapped = MemuOpenAIWrapper(\n            client,\n            service,\n            user_data={\"user_id\": \"user123\"},\n        )\n\n        # Memories are automatically injected\n        response = wrapped.chat.completions.create(\n            model=\"gpt-4\",\n            messages=[{\"role\": \"user\", \"content\": \"What's my favorite drink?\"}]\n        )\n    \"\"\"\n\n    def __init__(\n        self,\n        client,\n        service: MemoryService,\n        user_data: dict[str, Any],\n        ranking: str = \"salience\",\n        top_k: int = 5,\n    ):\n        \"\"\"\n        Initialize the wrapper.\n\n        Args:\n            client: OpenAI client instance\n            service: memU MemoryService instance\n            user_data: User scope data (user_id, agent_id, session_id, etc.)\n            ranking: Retrieval ranking strategy (\"similarity\" or \"salience\")\n            top_k: Number of memories to retrieve\n        \"\"\"\n        self._client = client\n        self._service = service\n        self._user_data = user_data\n        self._ranking = ranking\n        self._top_k = top_k\n\n        # Wrap chat namespace\n        self.chat = MemuChat(\n            client.chat,\n            service,\n            user_data,\n            ranking,\n            top_k,\n        )\n\n    def __getattr__(self, name: str) -> Any:\n        \"\"\"Proxy all other attributes to original client.\"\"\"\n        return getattr(self._client, name)\n\n\ndef wrap_openai(\n    client,\n    service: MemoryService,\n    user_data: dict[str, Any] | None = None,\n    user_id: str | None = None,\n    agent_id: str | None = None,\n    session_id: str | None = None,\n    ranking: str = \"salience\",\n    top_k: int = 5,\n) -> MemuOpenAIWrapper:\n    \"\"\"\n    Wrap an OpenAI client for auto-recall memory injection.\n\n    Args:\n        client: OpenAI client instance\n        service: memU MemoryService instance\n        user_data: Full user scope dict (alternative to individual params)\n        user_id: User identifier\n        agent_id: Agent identifier (for multi-agent scoping)\n        session_id: Session identifier\n        ranking: Retrieval ranking (\"similarity\" or \"salience\")\n        top_k: Number of memories to retrieve\n\n    Returns:\n        Wrapped client with auto-recall enabled\n\n    Example:\n        from openai import OpenAI\n        from memu.client import wrap_openai\n\n        client = wrap_openai(\n            OpenAI(),\n            service,\n            user_id=\"user123\",\n            ranking=\"salience\",\n        )\n\n        response = client.chat.completions.create(\n            model=\"gpt-4\",\n            messages=[{\"role\": \"user\", \"content\": \"What do I like?\"}]\n        )\n    \"\"\"\n    if user_data is None:\n        user_data = {}\n    if user_id:\n        user_data[\"user_id\"] = user_id\n    if agent_id:\n        user_data[\"agent_id\"] = agent_id\n    if session_id:\n        user_data[\"session_id\"] = session_id\n\n    return MemuOpenAIWrapper(client, service, user_data, ranking, top_k)\n"
  },
  {
    "path": "src/memu/database/__init__.py",
    "content": "\"\"\"Storage backends for MemU.\"\"\"\n\nfrom memu.database.factory import build_database\nfrom memu.database.interfaces import (\n    CategoryItemRecord,\n    Database,\n    MemoryCategoryRecord,\n    MemoryItemRecord,\n    ResourceRecord,\n)\nfrom memu.database.repositories import CategoryItemRepo, MemoryCategoryRepo, MemoryItemRepo, ResourceRepo\n\n__all__ = [\n    \"CategoryItemRecord\",\n    \"CategoryItemRepo\",\n    \"Database\",\n    \"MemoryCategoryRecord\",\n    \"MemoryCategoryRepo\",\n    \"MemoryItemRecord\",\n    \"MemoryItemRepo\",\n    \"ResourceRecord\",\n    \"ResourceRepo\",\n    \"build_database\",\n    \"inmemory\",\n    \"postgres\",\n    \"schema\",\n    \"sqlite\",\n]\n"
  },
  {
    "path": "src/memu/database/factory.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import BaseModel\n\nfrom memu.app.settings import DatabaseConfig\nfrom memu.database.inmemory import build_inmemory_database\nfrom memu.database.interfaces import Database\n\nif TYPE_CHECKING:\n    pass\n\n\ndef build_database(\n    *,\n    config: DatabaseConfig,\n    user_model: type[BaseModel],\n) -> Database:\n    \"\"\"\n    Initialize a database backend for the configured provider.\n\n    Supported providers:\n        - \"inmemory\": In-memory storage (default, no persistence)\n        - \"postgres\": PostgreSQL with optional pgvector support\n        - \"sqlite\": SQLite file-based storage (lightweight, portable)\n    \"\"\"\n    provider = config.metadata_store.provider\n    if provider == \"inmemory\":\n        return build_inmemory_database(config=config, user_model=user_model)\n    elif provider == \"postgres\":\n        # Lazy import to avoid requiring pgvector when not using postgres\n        from memu.database.postgres import build_postgres_database\n\n        return build_postgres_database(config=config, user_model=user_model)\n    elif provider == \"sqlite\":\n        # Lazy import to avoid loading SQLite dependencies when not needed\n        from memu.database.sqlite import build_sqlite_database\n\n        return build_sqlite_database(config=config, user_model=user_model)\n    else:\n        msg = f\"Unsupported metadata_store provider: {provider}\"\n        raise ValueError(msg)\n"
  },
  {
    "path": "src/memu/database/inmemory/__init__.py",
    "content": "from __future__ import annotations\n\nfrom pydantic import BaseModel\n\nfrom memu.app.settings import DatabaseConfig\nfrom memu.database.inmemory.models import build_inmemory_models\nfrom memu.database.inmemory.repo import InMemoryStore\n\n\ndef build_inmemory_database(\n    *,\n    config: DatabaseConfig,\n    user_model: type[BaseModel],\n) -> InMemoryStore:\n    resource_model, memory_category_model, memory_item_model, category_item_model = build_inmemory_models(user_model)\n    return InMemoryStore(\n        scope_model=user_model,\n        resource_model=resource_model,\n        memory_item_model=memory_item_model,\n        memory_category_model=memory_category_model,\n        category_item_model=category_item_model,\n    )\n\n\n__all__ = [\"build_inmemory_database\"]\n"
  },
  {
    "path": "src/memu/database/inmemory/models.py",
    "content": "from __future__ import annotations\n\nfrom pydantic import BaseModel\n\nfrom memu.database.models import (\n    CategoryItem,\n    MemoryCategory,\n    MemoryItem,\n    Resource,\n    merge_scope_model,\n)\n\n\nclass InMemoryResource(Resource):\n    \"\"\"Concrete in-memory resource model.\"\"\"\n\n\nclass InMemoryMemoryItem(MemoryItem):\n    \"\"\"Concrete in-memory memory item model.\"\"\"\n\n\nclass InMemoryMemoryCategory(MemoryCategory):\n    \"\"\"Concrete in-memory memory category model.\"\"\"\n\n\nclass InMemoryCategoryItem(CategoryItem):\n    \"\"\"Concrete in-memory relation model.\"\"\"\n\n\ndef build_inmemory_models(\n    user_model: type[BaseModel],\n) -> tuple[\n    type[InMemoryResource],\n    type[InMemoryMemoryCategory],\n    type[InMemoryMemoryItem],\n    type[InMemoryCategoryItem],\n]:\n    \"\"\"\n    Build scoped in-memory models that inherit from both the base interface and the user scope model.\n    \"\"\"\n    resource_model = merge_scope_model(user_model, InMemoryResource, name_suffix=\"Resource\")\n    memory_category_model = merge_scope_model(user_model, InMemoryMemoryCategory, name_suffix=\"MemoryCategory\")\n    memory_item_model = merge_scope_model(user_model, InMemoryMemoryItem, name_suffix=\"MemoryItem\")\n    category_item_model = merge_scope_model(user_model, InMemoryCategoryItem, name_suffix=\"CategoryItem\")\n    return resource_model, memory_category_model, memory_item_model, category_item_model\n\n\n__all__ = [\n    \"InMemoryCategoryItem\",\n    \"InMemoryMemoryCategory\",\n    \"InMemoryMemoryItem\",\n    \"InMemoryResource\",\n    \"build_inmemory_models\",\n]\n"
  },
  {
    "path": "src/memu/database/inmemory/repo.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom memu.database.inmemory.models import build_inmemory_models\nfrom memu.database.inmemory.repositories import (\n    InMemoryCategoryItemRepository,\n    InMemoryMemoryCategoryRepository,\n    InMemoryMemoryItemRepository,\n    InMemoryResourceRepository,\n)\nfrom memu.database.inmemory.state import InMemoryState\nfrom memu.database.interfaces import Database\nfrom memu.database.models import CategoryItem, MemoryCategory, MemoryItem, Resource\nfrom memu.database.repositories import MemoryCategoryRepo, ResourceRepo\n\n\nclass InMemoryStore(Database):\n    def __init__(\n        self,\n        *,\n        scope_model: type[BaseModel] | None = None,\n        resource_model: type[Any] | None = None,\n        memory_item_model: type[Any] | None = None,\n        memory_category_model: type[Any] | None = None,\n        category_item_model: type[Any] | None = None,\n        state: InMemoryState | None = None,\n    ) -> None:\n        self.scope_model = scope_model or BaseModel\n        (\n            default_resource_model,\n            default_memory_category_model,\n            default_memory_item_model,\n            default_category_item_model,\n        ) = build_inmemory_models(self.scope_model)\n\n        self.state = state or InMemoryState()\n        self.resources: dict[str, Resource] = self.state.resources\n        self.items: dict[str, MemoryItem] = self.state.items\n        self.categories: dict[str, MemoryCategory] = self.state.categories\n        self.relations: list[CategoryItem] = self.state.relations\n\n        resource_model = resource_model or default_resource_model or Resource\n        memory_item_model = memory_item_model or default_memory_item_model or MemoryItem\n        memory_category_model = memory_category_model or default_memory_category_model or MemoryCategory\n        category_item_model = category_item_model or default_category_item_model or CategoryItem\n\n        self.resource_repo: ResourceRepo = InMemoryResourceRepository(state=self.state, resource_model=resource_model)\n        self.memory_category_repo: MemoryCategoryRepo = InMemoryMemoryCategoryRepository(\n            state=self.state, memory_category_model=memory_category_model\n        )\n        self.memory_item_repo = InMemoryMemoryItemRepository(state=self.state, memory_item_model=memory_item_model)\n        self.category_item_repo = InMemoryCategoryItemRepository(\n            state=self.state, category_item_model=category_item_model\n        )\n\n    def close(self) -> None:\n        return None\n"
  },
  {
    "path": "src/memu/database/inmemory/repositories/__init__.py",
    "content": "from memu.database.inmemory.repositories.category_item_repo import (\n    CategoryItemRepo,\n    InMemoryCategoryItemRepository,\n)\nfrom memu.database.inmemory.repositories.memory_category_repo import (\n    InMemoryMemoryCategoryRepository,\n    MemoryCategoryRepo,\n)\nfrom memu.database.inmemory.repositories.memory_item_repo import InMemoryMemoryItemRepository, MemoryItemRepo\nfrom memu.database.inmemory.repositories.resource_repo import InMemoryResourceRepository, ResourceRepo\n\n__all__ = [\n    \"CategoryItemRepo\",\n    \"InMemoryCategoryItemRepository\",\n    \"InMemoryMemoryCategoryRepository\",\n    \"InMemoryMemoryItemRepository\",\n    \"InMemoryResourceRepository\",\n    \"MemoryCategoryRepo\",\n    \"MemoryItemRepo\",\n    \"ResourceRepo\",\n]\n"
  },
  {
    "path": "src/memu/database/inmemory/repositories/category_item_repo.py",
    "content": "from __future__ import annotations\n\nimport uuid\nfrom collections.abc import Mapping\nfrom typing import Any, override\n\nfrom memu.database.inmemory.repositories.filter import matches_where\nfrom memu.database.inmemory.state import InMemoryState\nfrom memu.database.models import CategoryItem\nfrom memu.database.repositories.category_item import CategoryItemRepo\n\n\nclass InMemoryCategoryItemRepository(CategoryItemRepo):\n    def __init__(self, *, state: InMemoryState, category_item_model: type[CategoryItem]) -> None:\n        self._state = state\n        self.category_item_model = category_item_model\n        self.relations: list[CategoryItem] = self._state.relations\n\n    def list_relations(self, where: Mapping[str, Any] | None = None) -> list[CategoryItem]:\n        if not where:\n            return list(self.relations)\n        return [rel for rel in self.relations if matches_where(rel, where)]\n\n    def link_item_category(self, item_id: str, cat_id: str, user_data: dict[str, Any]) -> CategoryItem:\n        _ = item_id  # enforced by caller via existing state\n        for rel in self.relations:\n            if rel.item_id == item_id and rel.category_id == cat_id:\n                return rel\n        rel = self.category_item_model(id=str(uuid.uuid4()), item_id=item_id, category_id=cat_id, **user_data)\n        self.relations.append(rel)\n        return rel\n\n    def load_existing(self) -> None:\n        return None\n\n    @override\n    def get_item_categories(self, item_id: str) -> list[CategoryItem]:\n        return [rel for rel in self.relations if rel.item_id == item_id]\n\n    @override\n    def unlink_item_category(self, item_id: str, cat_id: str) -> None:\n        self.relations = [rel for rel in self.relations if not (rel.item_id == item_id and rel.category_id == cat_id)]\n\n\n__all__ = [\"InMemoryCategoryItemRepository\"]\n"
  },
  {
    "path": "src/memu/database/inmemory/repositories/filter.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any\n\n\ndef matches_where(obj: Any, where: Mapping[str, Any] | None) -> bool:\n    \"\"\"Basic field/`__in` matcher for in-memory repos.\"\"\"\n    if not where:\n        return True\n    for raw_key, expected in where.items():\n        if expected is None:\n            continue\n        field, op = [*raw_key.split(\"__\", 1), None][:2]\n        actual = getattr(obj, str(field), None)\n        if op == \"in\":\n            if isinstance(expected, str):\n                if actual != expected:\n                    return False\n            else:\n                try:\n                    if actual not in expected:\n                        return False\n                except TypeError:\n                    return False\n        else:\n            if actual != expected:\n                return False\n    return True\n\n\n__all__ = [\"matches_where\"]\n"
  },
  {
    "path": "src/memu/database/inmemory/repositories/memory_category_repo.py",
    "content": "from __future__ import annotations\n\nimport uuid\nfrom collections.abc import Mapping\nfrom typing import Any\n\nimport pendulum\n\nfrom memu.database.inmemory.repositories.filter import matches_where\nfrom memu.database.inmemory.state import InMemoryState\nfrom memu.database.models import MemoryCategory\nfrom memu.database.repositories.memory_category import MemoryCategoryRepo as MemoryCategoryRepoProtocol\n\n\nclass InMemoryMemoryCategoryRepository(MemoryCategoryRepoProtocol):\n    def __init__(self, *, state: InMemoryState, memory_category_model: type[MemoryCategory]) -> None:\n        self._state = state\n        self.memory_category_model = memory_category_model\n        self.categories: dict[str, MemoryCategory] = self._state.categories\n\n    def list_categories(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryCategory]:\n        if not where:\n            return dict(self.categories)\n        return {cid: cat for cid, cat in self.categories.items() if matches_where(cat, where)}\n\n    def clear_categories(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryCategory]:\n        if not where:\n            matches = self.categories.copy()\n            self.categories.clear()\n            return matches\n        matches = {cid: cat for cid, cat in self.categories.items() if matches_where(cat, where)}\n        self.categories = {cid: cat for cid, cat in self.categories.items() if cid not in matches}\n        return matches\n\n    def get_or_create_category(\n        self, *, name: str, description: str, embedding: list[float], user_data: dict[str, Any]\n    ) -> MemoryCategory:\n        for c in self.categories.values():\n            if c.name == name and all(getattr(c, k) == v for k, v in user_data.items()):\n                now = pendulum.now(\"UTC\")\n                if c.embedding is None:\n                    c.embedding = embedding\n                    c.updated_at = now\n                if not c.description:\n                    c.description = description\n                    c.updated_at = now\n                return c\n        cid = str(uuid.uuid4())\n        cat = self.memory_category_model(id=cid, name=name, description=description, embedding=embedding, **user_data)\n        self.categories[cid] = cat\n        return cat\n\n    def update_category(\n        self,\n        *,\n        category_id: str,\n        name: str | None = None,\n        description: str | None = None,\n        embedding: list[float] | None = None,\n        summary: str | None = None,\n    ) -> MemoryCategory:\n        cat = self.categories.get(category_id)\n        if cat is None:\n            msg = f\"Category with id {category_id} not found\"\n            raise KeyError(msg)\n\n        if name is not None:\n            cat.name = name\n        if description is not None:\n            cat.description = description\n        if embedding is not None:\n            cat.embedding = embedding\n        if summary is not None:\n            cat.summary = summary\n\n        cat.updated_at = pendulum.now(\"UTC\")\n        return cat\n\n    def load_existing(self) -> None:\n        return None\n\n\nMemoryCategoryRepo = InMemoryMemoryCategoryRepository\n\n__all__ = [\"InMemoryMemoryCategoryRepository\", \"MemoryCategoryRepo\"]\n"
  },
  {
    "path": "src/memu/database/inmemory/repositories/memory_item_repo.py",
    "content": "from __future__ import annotations\n\nimport uuid\nfrom collections.abc import Mapping\nfrom typing import Any, override\n\nimport pendulum\n\nfrom memu.database.inmemory.repositories.filter import matches_where\nfrom memu.database.inmemory.state import InMemoryState\nfrom memu.database.inmemory.vector import cosine_topk, cosine_topk_salience\nfrom memu.database.models import MemoryItem, MemoryType, compute_content_hash\nfrom memu.database.repositories.memory_item import MemoryItemRepo\n\n\nclass InMemoryMemoryItemRepository(MemoryItemRepo):\n    def __init__(self, *, state: InMemoryState, memory_item_model: type[MemoryItem]) -> None:\n        self._state = state\n        self.memory_item_model = memory_item_model\n        self.items: dict[str, MemoryItem] = self._state.items\n\n    def list_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]:\n        if not where:\n            return dict(self.items)\n        return {mid: item for mid, item in self.items.items() if matches_where(item, where)}\n\n    def list_items_by_ref_ids(\n        self, ref_ids: list[str], where: Mapping[str, Any] | None = None\n    ) -> dict[str, MemoryItem]:\n        \"\"\"List items by their ref_id in the extra column.\n\n        Args:\n            ref_ids: List of ref_ids to query.\n            where: Additional filter conditions.\n\n        Returns:\n            Dict mapping item_id -> MemoryItem for items whose extra.ref_id is in ref_ids.\n        \"\"\"\n        if not ref_ids:\n            return {}\n        ref_id_set = set(ref_ids)\n        result: dict[str, MemoryItem] = {}\n        for mid, item in self.items.items():\n            # Check where filter first\n            if where and not matches_where(item, where):\n                continue\n            # Check if ref_id is in the requested set\n            item_ref_id = (item.extra or {}).get(\"ref_id\")\n            if item_ref_id and item_ref_id in ref_id_set:\n                result[mid] = item\n        return result\n\n    def clear_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]:\n        if not where:\n            matches = self.items.copy()\n            self.items.clear()\n            return matches\n        matches = {mid: item for mid, item in self.items.items() if matches_where(item, where)}\n        self.items = {mid: item for mid, item in self.items.items() if mid not in matches}\n        return matches\n\n    def _find_by_hash(self, content_hash: str, user_data: dict[str, Any]) -> MemoryItem | None:\n        \"\"\"\n        Find existing item by content hash within the same user scope.\n\n        This enables deduplication: if the same content exists for the same user,\n        we reinforce it instead of creating a duplicate.\n        \"\"\"\n        for item in self.items.values():\n            # Read content_hash from extra dict\n            item_hash = (item.extra or {}).get(\"content_hash\")\n            if item_hash != content_hash:\n                continue\n            # Check scope match (user_id, agent_id, etc.)\n            if matches_where(item, user_data):\n                return item\n        return None\n\n    def create_item(\n        self,\n        *,\n        resource_id: str,\n        memory_type: MemoryType,\n        summary: str,\n        embedding: list[float],\n        user_data: dict[str, Any],\n        reinforce: bool = False,\n        tool_record: dict[str, Any] | None = None,\n    ) -> MemoryItem:\n        if reinforce and memory_type != \"tool\":\n            return self.create_item_reinforce(\n                resource_id=resource_id,\n                memory_type=memory_type,\n                summary=summary,\n                embedding=embedding,\n                user_data=user_data,\n            )\n\n        # Build extra dict with tool_record fields at top level\n        extra: dict[str, Any] = {}\n        if tool_record:\n            if tool_record.get(\"when_to_use\") is not None:\n                extra[\"when_to_use\"] = tool_record[\"when_to_use\"]\n            if tool_record.get(\"metadata\") is not None:\n                extra[\"metadata\"] = tool_record[\"metadata\"]\n            if tool_record.get(\"tool_calls\") is not None:\n                extra[\"tool_calls\"] = tool_record[\"tool_calls\"]\n\n        mid = str(uuid.uuid4())\n        it = self.memory_item_model(\n            id=mid,\n            resource_id=resource_id,\n            memory_type=memory_type,\n            summary=summary,\n            embedding=embedding,\n            extra=extra if extra else {},\n            **user_data,\n        )\n        self.items[mid] = it\n        return it\n\n    def create_item_reinforce(\n        self,\n        *,\n        resource_id: str,\n        memory_type: MemoryType,\n        summary: str,\n        embedding: list[float],\n        user_data: dict[str, Any],\n        reinforce: bool = False,\n    ) -> MemoryItem:\n        content_hash = compute_content_hash(summary, memory_type)\n\n        # Check for existing item with same hash in same scope (deduplication)\n        existing = self._find_by_hash(content_hash, user_data)\n        if existing:\n            # Reinforce existing memory instead of creating duplicate\n            current_extra = existing.extra or {}\n            current_count = current_extra.get(\"reinforcement_count\", 1)\n            existing.extra = {\n                **current_extra,\n                \"reinforcement_count\": current_count + 1,\n                \"last_reinforced_at\": pendulum.now(\"UTC\").isoformat(),\n            }\n            existing.updated_at = pendulum.now(\"UTC\")\n            return existing\n\n        # Create new item with salience tracking in extra\n        mid = str(uuid.uuid4())\n        now = pendulum.now(\"UTC\")\n        item_extra = user_data.pop(\"extra\", {}) if \"extra\" in user_data else {}\n        item_extra.update({\n            \"content_hash\": content_hash,\n            \"reinforcement_count\": 1,\n            \"last_reinforced_at\": now.isoformat(),\n        })\n        it = self.memory_item_model(\n            id=mid,\n            resource_id=resource_id,\n            memory_type=memory_type,\n            summary=summary,\n            embedding=embedding,\n            extra=item_extra,\n            **user_data,\n        )\n        self.items[mid] = it\n        return it\n\n    def vector_search_items(\n        self,\n        query_vec: list[float],\n        top_k: int,\n        where: Mapping[str, Any] | None = None,\n        *,\n        ranking: str = \"similarity\",\n        recency_decay_days: float = 30.0,\n    ) -> list[tuple[str, float]]:\n        pool = self.list_items(where)\n\n        if ranking == \"salience\":\n            # Salience-aware ranking: similarity x reinforcement x recency\n            # Read values from extra dict\n            corpus = [\n                (\n                    i.id,\n                    i.embedding,\n                    (i.extra or {}).get(\"reinforcement_count\", 1),\n                    self._parse_datetime((i.extra or {}).get(\"last_reinforced_at\")),\n                )\n                for i in pool.values()\n            ]\n            return cosine_topk_salience(query_vec, corpus, k=top_k, recency_decay_days=recency_decay_days)\n\n        # Default: pure cosine similarity (backward compatible)\n        hits = cosine_topk(query_vec, [(i.id, i.embedding) for i in pool.values()], k=top_k)\n        return hits\n\n    def load_existing(self) -> None:\n        return None\n\n    def get_item(self, item_id: str) -> MemoryItem | None:\n        return self.items.get(item_id)\n\n    @staticmethod\n    def _parse_datetime(dt_str: str | None) -> pendulum.DateTime | None:\n        \"\"\"Parse ISO datetime string from extra dict.\"\"\"\n        if dt_str is None:\n            return None\n        try:\n            parsed = pendulum.parse(dt_str)\n        except (ValueError, TypeError):\n            return None\n        else:\n            if isinstance(parsed, pendulum.DateTime):\n                return parsed\n            return None\n\n    @override\n    def delete_item(self, item_id: str) -> None:\n        if item_id in self.items:\n            del self.items[item_id]\n\n    @override\n    def update_item(\n        self,\n        *,\n        item_id: str,\n        memory_type: MemoryType | None = None,\n        summary: str | None = None,\n        embedding: list[float] | None = None,\n        extra: dict[str, Any] | None = None,\n        tool_record: dict[str, Any] | None = None,\n    ) -> MemoryItem:\n        item = self.items.get(item_id)\n        if item is None:\n            msg = f\"Item with id {item_id} not found\"\n            raise KeyError(msg)\n\n        if memory_type is not None:\n            item.memory_type = memory_type\n        if summary is not None:\n            item.summary = summary\n        if embedding is not None:\n            item.embedding = embedding\n\n        # Merge extra and tool_record into existing extra dict\n        current_extra = item.extra or {}\n        if extra is not None:\n            current_extra = {**current_extra, **extra}\n        if tool_record is not None:\n            # Merge tool_record fields at top level\n            for key in (\"when_to_use\", \"metadata\", \"tool_calls\"):\n                if tool_record.get(key) is not None:\n                    current_extra[key] = tool_record[key]\n        if extra is not None or tool_record is not None:\n            item.extra = current_extra\n\n        self.items[item_id] = item\n        return item\n\n\n__all__ = [\"InMemoryMemoryItemRepository\"]\n"
  },
  {
    "path": "src/memu/database/inmemory/repositories/resource_repo.py",
    "content": "from __future__ import annotations\n\nimport uuid\nfrom collections.abc import Mapping\nfrom typing import Any\n\nfrom memu.database.inmemory.repositories.filter import matches_where\nfrom memu.database.inmemory.state import InMemoryState\nfrom memu.database.models import Resource\nfrom memu.database.repositories.resource import ResourceRepo as ResourceRepoProtocol\n\n\nclass InMemoryResourceRepository(ResourceRepoProtocol):\n    def __init__(self, *, state: InMemoryState, resource_model: type[Resource]) -> None:\n        self._state = state\n        self.resource_model = resource_model\n        self.resources: dict[str, Resource] = self._state.resources\n\n    def list_resources(self, where: Mapping[str, Any] | None = None) -> dict[str, Resource]:\n        if not where:\n            return dict(self.resources)\n        return {rid: res for rid, res in self.resources.items() if matches_where(res, where)}\n\n    def clear_resources(self, where: Mapping[str, Any] | None = None) -> dict[str, Resource]:\n        if not where:\n            matches = self.resources.copy()\n            self.resources.clear()\n            return matches\n        matches = {rid: res for rid, res in self.resources.items() if matches_where(res, where)}\n        self.resources = {rid: res for rid, res in self.resources.items() if rid not in matches}\n        return matches\n\n    def create_resource(\n        self,\n        *,\n        url: str,\n        modality: str,\n        local_path: str,\n        caption: str | None,\n        embedding: list[float] | None,\n        user_data: dict[str, Any],\n    ) -> Resource:\n        rid = str(uuid.uuid4())\n        res = self.resource_model(\n            id=rid,\n            url=url,\n            modality=modality,\n            local_path=local_path,\n            caption=caption,\n            embedding=embedding,\n            **user_data,\n        )\n        self.resources[rid] = res\n        return res\n\n    def load_existing(self) -> None:\n        return None\n\n\nResourceRepo = InMemoryResourceRepository\n\n__all__ = [\"InMemoryResourceRepository\", \"ResourceRepo\"]\n"
  },
  {
    "path": "src/memu/database/inmemory/state.py",
    "content": "from __future__ import annotations\n\nfrom memu.database.state import DatabaseState\n\nInMemoryState = DatabaseState\n\n__all__ = [\"DatabaseState\", \"InMemoryState\"]\n"
  },
  {
    "path": "src/memu/database/inmemory/vector.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom collections.abc import Iterable\nfrom datetime import datetime\nfrom typing import cast\n\nimport numpy as np\n\n\ndef _cosine(a: np.ndarray, b: np.ndarray) -> float:\n    denom = (np.linalg.norm(a) * np.linalg.norm(b)) + 1e-9\n    return float(np.dot(a, b) / denom)\n\n\ndef salience_score(\n    similarity: float,\n    reinforcement_count: int,\n    last_reinforced_at: datetime | None,\n    recency_decay_days: float = 30.0,\n) -> float:\n    \"\"\"\n    Compute salience-aware score combining similarity, reinforcement, and recency.\n\n    Formula: similarity * reinforcement_factor * recency_factor\n\n    - reinforcement_factor: log(count + 1) to dampen extreme counts\n      (Logarithmic scaling prevents runaway dominance by frequently repeated facts)\n    - recency_factor: exponential decay based on days since last reinforcement\n      (Uses half-life decay: after recency_decay_days, factor is ~0.5)\n\n    Args:\n        similarity: Cosine similarity score (0 to 1)\n        reinforcement_count: Number of times this memory was reinforced\n        last_reinforced_at: When the memory was last reinforced\n        recency_decay_days: Half-life for recency decay in days\n\n    Returns:\n        Salience score (higher = more salient)\n    \"\"\"\n    # Reinforcement factor (logarithmic to prevent runaway scores)\n    reinforcement_factor = math.log(reinforcement_count + 1)\n\n    # Recency factor (exponential decay with half-life)\n    if last_reinforced_at is None:\n        recency_factor = 0.5  # Unknown recency gets neutral score\n    else:\n        now = datetime.now(last_reinforced_at.tzinfo) if last_reinforced_at.tzinfo else datetime.utcnow()\n        days_ago = (now - last_reinforced_at).total_seconds() / 86400\n        # 0.693 = ln(2), gives us proper half-life decay\n        recency_factor = math.exp(-0.693 * days_ago / recency_decay_days)\n\n    return similarity * reinforcement_factor * recency_factor\n\n\ndef cosine_topk(\n    query_vec: list[float],\n    corpus: Iterable[tuple[str, list[float] | None]],\n    k: int = 5,\n) -> list[tuple[str, float]]:\n    # Filter out None vectors and collect valid entries\n    ids: list[str] = []\n    vecs: list[list[float]] = []\n    for _id, vec in corpus:\n        if vec is not None:\n            ids.append(_id)\n            vecs.append(cast(list[float], vec))\n\n    if not vecs:\n        return []\n\n    # Vectorized computation: stack all vectors into a matrix\n    q = np.array(query_vec, dtype=np.float32)\n    matrix = np.array(vecs, dtype=np.float32)  # shape: (n, dim)\n\n    # Compute all cosine similarities at once\n    q_norm = np.linalg.norm(q)\n    vec_norms = np.linalg.norm(matrix, axis=1)\n    scores = matrix @ q / (vec_norms * q_norm + 1e-9)\n\n    # Use argpartition for O(n) topk selection instead of O(n log n) sort\n    n = len(scores)\n    actual_k = min(k, n)\n    if actual_k == n:\n        topk_indices = np.argsort(scores)[::-1]\n    else:\n        # Get indices of top k elements (unordered), then sort only those\n        topk_indices = np.argpartition(scores, -actual_k)[-actual_k:]\n        topk_indices = topk_indices[np.argsort(scores[topk_indices])[::-1]]\n\n    return [(ids[i], float(scores[i])) for i in topk_indices]\n\n\ndef cosine_topk_salience(\n    query_vec: list[float],\n    corpus: Iterable[tuple[str, list[float] | None, int, datetime | None]],\n    k: int = 5,\n    recency_decay_days: float = 30.0,\n) -> list[tuple[str, float]]:\n    \"\"\"\n    Top-k retrieval using salience-aware scoring.\n\n    Ranks memories by: similarity * log(reinforcement+1) * recency_decay\n\n    Args:\n        query_vec: Query embedding vector\n        corpus: Iterable of (id, embedding, reinforcement_count, last_reinforced_at)\n        k: Number of top results to return\n        recency_decay_days: Half-life for recency decay\n\n    Returns:\n        List of (id, salience_score) tuples, sorted by score descending\n    \"\"\"\n    q = np.array(query_vec, dtype=np.float32)\n    scored: list[tuple[str, float]] = []\n\n    for _id, vec, reinforcement_count, last_reinforced_at in corpus:\n        if vec is None:\n            continue\n        vec_list = cast(list[float], vec)\n        v = np.array(vec_list, dtype=np.float32)\n        similarity = _cosine(q, v)\n        score = salience_score(similarity, reinforcement_count, last_reinforced_at, recency_decay_days)\n        scored.append((_id, score))\n\n    scored.sort(key=lambda x: x[1], reverse=True)\n    return scored[:k]\n\n\ndef query_cosine(query_vec: list[float], vecs: list[list[float]]) -> list[tuple[int, float]]:\n    res: list[tuple[int, float]] = []\n    q = np.array(query_vec, dtype=np.float32)\n    for i, v in enumerate(vecs):\n        vec_array = np.array(v, dtype=np.float32)\n        res.append((i, _cosine(q, vec_array)))\n    res.sort(key=lambda x: x[1], reverse=True)\n    return res\n"
  },
  {
    "path": "src/memu/database/interfaces.py",
    "content": "from __future__ import annotations\n\nfrom typing import Protocol, runtime_checkable\n\nfrom memu.database.models import CategoryItem as CategoryItemRecord\nfrom memu.database.models import MemoryCategory as MemoryCategoryRecord\nfrom memu.database.models import MemoryItem as MemoryItemRecord\nfrom memu.database.models import Resource as ResourceRecord\nfrom memu.database.repositories import CategoryItemRepo, MemoryCategoryRepo, MemoryItemRepo, ResourceRepo\n\n\n@runtime_checkable\nclass Database(Protocol):\n    \"\"\"Backend-agnostic database contract.\"\"\"\n\n    resource_repo: ResourceRepo\n    memory_category_repo: MemoryCategoryRepo\n    memory_item_repo: MemoryItemRepo\n    category_item_repo: CategoryItemRepo\n\n    resources: dict[str, ResourceRecord]\n    items: dict[str, MemoryItemRecord]\n    categories: dict[str, MemoryCategoryRecord]\n    relations: list[CategoryItemRecord]\n\n    def close(self) -> None: ...\n\n\n__all__ = [\n    \"CategoryItemRecord\",\n    \"Database\",\n    \"MemoryCategoryRecord\",\n    \"MemoryItemRecord\",\n    \"ResourceRecord\",\n]\n"
  },
  {
    "path": "src/memu/database/models.py",
    "content": "from __future__ import annotations\n\nimport hashlib\nimport json\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Literal\n\nimport pendulum\nfrom pydantic import BaseModel, ConfigDict, Field\n\nMemoryType = Literal[\"profile\", \"event\", \"knowledge\", \"behavior\", \"skill\", \"tool\"]\n\n\ndef compute_content_hash(summary: str, memory_type: str) -> str:\n    \"\"\"\n    Generate unique hash for memory deduplication.\n\n    Operates on post-summary content. Normalizes whitespace to handle\n    minor formatting differences like \"I love coffee\" vs \"I  love  coffee\".\n\n    Args:\n        summary: The memory summary text\n        memory_type: The type of memory (profile, event, etc.)\n\n    Returns:\n        A 16-character hex hash string\n    \"\"\"\n    # Normalize: lowercase, strip, collapse whitespace\n    normalized = \" \".join(summary.lower().split())\n    content = f\"{memory_type}:{normalized}\"\n    return hashlib.sha256(content.encode()).hexdigest()[:16]\n\n\nclass BaseRecord(BaseModel):\n    \"\"\"Backend-agnostic record interface.\"\"\"\n\n    id: str = Field(default_factory=lambda: str(uuid.uuid4()))\n    created_at: datetime = Field(default_factory=lambda: pendulum.now(\"UTC\"))\n    updated_at: datetime = Field(default_factory=lambda: pendulum.now(\"UTC\"))\n\n\nclass ToolCallResult(BaseModel):\n    \"\"\"Represents the result of a tool invocation for Tool Memory.\"\"\"\n\n    tool_name: str = Field(..., description=\"Name of the tool that was called\")\n    input: dict[str, Any] | str = Field(default=\"\", description=\"Tool input parameters\")\n    output: str = Field(default=\"\", description=\"Tool output result\")\n    success: bool = Field(default=True, description=\"Whether the tool invocation succeeded\")\n    time_cost: float = Field(default=0.0, description=\"Time consumed by the tool invocation in seconds\")\n    token_cost: int = Field(default=-1, description=\"Token consumption of the tool (-1 if unknown)\")\n    score: float = Field(default=0.0, description=\"Quality score from 0.0 to 1.0\")\n    call_hash: str = Field(default=\"\", description=\"Hash of input+output for deduplication\")\n    created_at: datetime = Field(default_factory=lambda: pendulum.now(\"UTC\"))\n\n    def generate_hash(self) -> str:\n        \"\"\"Generate MD5 hash from tool input and output for deduplication.\"\"\"\n        input_str = json.dumps(self.input, sort_keys=True) if isinstance(self.input, dict) else str(self.input)\n        combined = f\"{self.tool_name}|{input_str}|{self.output}\"\n        return hashlib.md5(combined.encode(\"utf-8\"), usedforsecurity=False).hexdigest()\n\n    def ensure_hash(self) -> None:\n        \"\"\"Ensure call_hash is set, generate if empty.\"\"\"\n        if not self.call_hash:\n            self.call_hash = self.generate_hash()\n\n\nclass Resource(BaseRecord):\n    url: str\n    modality: str\n    local_path: str\n    caption: str | None = None\n    embedding: list[float] | None = None\n\n\nclass MemoryItem(BaseRecord):\n    resource_id: str | None\n    memory_type: str\n    summary: str\n    embedding: list[float] | None = None\n    happened_at: datetime | None = None\n    extra: dict[str, Any] = {}\n    # extra may contain:\n    # # reinforcement tracking fields\n    # - content_hash: str\n    # - reinforcement_count: int\n    # - last_reinforced_at: str (isoformat)\n    # # Reference tracking field\n    # - ref_id: str\n    # # Tool memory fields\n    # - when_to_use: str - Hint for when this memory should be retrieved\n    # - metadata: dict - Type-specific metadata (e.g., tool_name, avg_success_rate)\n    # - tool_calls: list[dict] - Tool call history for tool memories (serialized ToolCallResult)\n\n\nclass MemoryCategory(BaseRecord):\n    name: str\n    description: str\n    embedding: list[float] | None = None\n    summary: str | None = None\n\n\nclass CategoryItem(BaseRecord):\n    item_id: str\n    category_id: str\n\n\ndef merge_scope_model[TBaseRecord: BaseRecord](\n    user_model: type[BaseModel], core_model: type[TBaseRecord], *, name_suffix: str\n) -> type[TBaseRecord]:\n    \"\"\"Create a scoped model inheriting both the user scope model and the core model.\"\"\"\n    overlap = set(user_model.model_fields) & set(core_model.model_fields)\n    if overlap:\n        msg = f\"Scope fields conflict with core model fields: {sorted(overlap)}\"\n        raise TypeError(msg)\n\n    return type(\n        f\"{user_model.__name__}{core_model.__name__}{name_suffix}\",\n        (user_model, core_model),\n        {\"model_config\": ConfigDict(extra=\"allow\")},\n    )\n\n\ndef build_scoped_models(\n    user_model: type[BaseModel],\n) -> tuple[type[Resource], type[MemoryCategory], type[MemoryItem], type[CategoryItem]]:\n    \"\"\"\n    Build scoped interface models (Pydantic) that inherit from the base record models and user scope.\n    \"\"\"\n    resource_model = merge_scope_model(user_model, Resource, name_suffix=\"Resource\")\n    memory_category_model = merge_scope_model(user_model, MemoryCategory, name_suffix=\"MemoryCategory\")\n    memory_item_model = merge_scope_model(user_model, MemoryItem, name_suffix=\"MemoryItem\")\n    category_item_model = merge_scope_model(user_model, CategoryItem, name_suffix=\"CategoryItem\")\n    return resource_model, memory_category_model, memory_item_model, category_item_model\n\n\n__all__ = [\n    \"BaseRecord\",\n    \"CategoryItem\",\n    \"MemoryCategory\",\n    \"MemoryItem\",\n    \"MemoryType\",\n    \"Resource\",\n    \"ToolCallResult\",\n    \"build_scoped_models\",\n    \"compute_content_hash\",\n    \"merge_scope_model\",\n]\n"
  },
  {
    "path": "src/memu/database/postgres/__init__.py",
    "content": "from __future__ import annotations\n\nfrom pydantic import BaseModel\n\nfrom memu.app.settings import DatabaseConfig\nfrom memu.database.postgres.postgres import PostgresStore\nfrom memu.database.postgres.schema import SQLAModels, get_sqlalchemy_models\n\n\ndef build_postgres_database(\n    *,\n    config: DatabaseConfig,\n    user_model: type[BaseModel],\n) -> PostgresStore:\n    dsn = config.metadata_store.dsn\n    if not dsn:\n        msg = \"Postgres metadata_store requires a DSN\"\n        raise ValueError(msg)\n\n    vector_provider = config.vector_index.provider if config.vector_index else None\n    sqla_models: SQLAModels = get_sqlalchemy_models(scope_model=user_model)\n\n    return PostgresStore(\n        dsn=dsn,\n        ddl_mode=config.metadata_store.ddl_mode,\n        vector_provider=vector_provider,\n        scope_model=user_model,\n        resource_model=sqla_models.Resource,\n        memory_category_model=sqla_models.MemoryCategory,\n        memory_item_model=sqla_models.MemoryItem,\n        category_item_model=sqla_models.CategoryItem,\n        sqla_models=sqla_models,\n    )\n\n\n__all__ = [\"build_postgres_database\"]\n"
  },
  {
    "path": "src/memu/database/postgres/migration.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom pathlib import Path\nfrom typing import Any, Literal\n\nfrom sqlalchemy import create_engine, inspect, text\n\nfrom memu.database.postgres.schema import get_metadata\n\ntry:  # Optional dependency for Postgres backend\n    from alembic import command\n    from alembic.config import Config as AlembicConfig\nexcept ImportError as exc:  # pragma: no cover - optional dependency\n    msg = \"alembic is required for Postgres migrations\"\n    raise ImportError(msg) from exc\n\nlogger = logging.getLogger(__name__)\n\nDDLMode = Literal[\"create\", \"validate\"]\n\n\ndef make_alembic_config(*, dsn: str, scope_model: type[Any]) -> AlembicConfig:\n    cfg = AlembicConfig()\n    cfg.set_main_option(\"script_location\", str(Path(__file__).with_name(\"migrations\")))\n    cfg.set_main_option(\"sqlalchemy.url\", dsn)\n    cfg.attributes[\"scope_model\"] = scope_model\n    return cfg\n\n\ndef run_migrations(*, dsn: str, scope_model: type[Any], ddl_mode: DDLMode = \"create\") -> None:\n    \"\"\"\n    Run database migrations based on the ddl_mode setting.\n\n    Args:\n        dsn: Database connection string\n        scope_model: User scope model for scoped tables\n        ddl_mode: \"create\" to create missing tables, \"validate\" to only check schema\n    \"\"\"\n    metadata = get_metadata(scope_model)\n    engine = create_engine(dsn)\n\n    if ddl_mode == \"create\":\n        # Enable pgvector extension if needed (requires superuser or extension already installed)\n        with engine.connect() as conn:\n            try:\n                conn.execute(text(\"CREATE EXTENSION IF NOT EXISTS vector\"))\n                conn.commit()\n                logger.info(\"pgvector extension enabled\")\n            except Exception as e:\n                # Check if extension already exists\n                result = conn.execute(text(\"SELECT 1 FROM pg_extension WHERE extname = 'vector'\")).fetchone()\n                if result:\n                    logger.info(\"pgvector extension already installed\")\n                else:\n                    msg = (\n                        \"Failed to create pgvector extension. \"\n                        \"Please run 'CREATE EXTENSION vector;' as a superuser first.\"\n                    )\n                    raise RuntimeError(msg) from e\n\n        # Create all tables that don't exist\n        metadata.create_all(engine)\n        logger.info(\"Database tables created/verified\")\n    elif ddl_mode == \"validate\":\n        # Validate that all expected tables exist\n        inspector = inspect(engine)\n        existing_tables = set(inspector.get_table_names())\n        expected_tables = set(metadata.tables.keys())\n        missing_tables = expected_tables - existing_tables\n\n        if missing_tables:\n            msg = f\"Database schema validation failed. Missing tables: {sorted(missing_tables)}\"\n            raise RuntimeError(msg)\n        logger.info(\"Database schema validated successfully\")\n\n    # Run any pending Alembic migrations\n    cfg = make_alembic_config(dsn=dsn, scope_model=scope_model)\n    command.upgrade(cfg, \"head\")\n\n\n__all__ = [\"DDLMode\", \"make_alembic_config\", \"run_migrations\"]\n"
  },
  {
    "path": "src/memu/database/postgres/migrations/__init__.py",
    "content": "# Alembic migrations package for memu Postgres storage.\n"
  },
  {
    "path": "src/memu/database/postgres/migrations/env.py",
    "content": "from __future__ import annotations\n\nfrom logging.config import fileConfig\n\nfrom alembic import context\nfrom sqlalchemy import MetaData, engine_from_config, pool\n\nfrom memu.database.postgres.schema import get_metadata\n\nconfig = context.config\n\nif config.config_file_name is not None:  # pragma: no cover - alembic bootstrap\n    fileConfig(config.config_file_name)\n\n\ndef get_target_metadata() -> MetaData | None:\n    scope_model = config.attributes.get(\"scope_model\")\n    return get_metadata(scope_model)\n\n\ntarget_metadata: MetaData | None = get_target_metadata()\n\n\ndef run_migrations_offline() -> None:\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True,\n        dialect_opts={\"paramstyle\": \"named\"},\n        compare_type=True,\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online() -> None:\n    configuration = {\"sqlalchemy.url\": config.get_main_option(\"sqlalchemy.url\")}\n    connectable = engine_from_config(\n        configuration,\n        prefix=\"sqlalchemy.\",\n        poolclass=pool.NullPool,\n    )\n\n    with connectable.connect() as connection:\n        context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "src/memu/database/postgres/models.py",
    "content": "from __future__ import annotations\n\nimport uuid\nfrom datetime import datetime\nfrom typing import Any\n\nimport pendulum\n\ntry:\n    from pgvector.sqlalchemy import VECTOR as Vector\nexcept ImportError as exc:\n    msg = \"pgvector is required for Postgres vector support\"\n    raise ImportError(msg) from exc\n\nfrom pydantic import BaseModel\nfrom sqlalchemy import ForeignKey, MetaData, String, Text\nfrom sqlalchemy.dialects.postgresql import JSONB\nfrom sqlmodel import Column, DateTime, Field, Index, SQLModel, func\n\nfrom memu.database.models import CategoryItem, MemoryCategory, MemoryItem, MemoryType, Resource\n\n\nclass TZDateTime(DateTime):\n    def __init__(self, timezone: bool = True, **kw: Any) -> None:\n        super().__init__(timezone=timezone, **kw)\n\n\nclass BaseModelMixin(SQLModel):\n    id: str = Field(\n        default_factory=lambda: str(uuid.uuid4()),\n        primary_key=True,\n        index=True,\n        sa_type=String,\n    )\n    created_at: datetime = Field(\n        default_factory=lambda: pendulum.now(\"UTC\"),\n        sa_type=TZDateTime,\n        sa_column_kwargs={\"server_default\": func.now()},\n    )\n    updated_at: datetime = Field(\n        default_factory=lambda: pendulum.now(\"UTC\"),\n        sa_type=TZDateTime,\n    )\n\n\nclass ResourceModel(BaseModelMixin, Resource):\n    url: str = Field(sa_column=Column(String, nullable=False))\n    modality: str = Field(sa_column=Column(String, nullable=False))\n    local_path: str = Field(sa_column=Column(String, nullable=False))\n    caption: str | None = Field(default=None, sa_column=Column(Text, nullable=True))\n    embedding: list[float] | None = Field(default=None, sa_column=Column(Vector(), nullable=True))\n\n\nclass MemoryItemModel(BaseModelMixin, MemoryItem):\n    resource_id: str | None = Field(sa_column=Column(ForeignKey(\"resources.id\", ondelete=\"CASCADE\"), nullable=True))\n    memory_type: MemoryType = Field(sa_column=Column(String, nullable=False))\n    summary: str = Field(sa_column=Column(Text, nullable=False))\n    embedding: list[float] | None = Field(default=None, sa_column=Column(Vector(), nullable=True))\n    happened_at: datetime | None = Field(default=None, sa_column=Column(DateTime, nullable=True))\n    extra: dict[str, Any] = Field(default={}, sa_column=Column(JSONB, nullable=True))\n\n\nclass MemoryCategoryModel(BaseModelMixin, MemoryCategory):\n    name: str = Field(sa_column=Column(String, nullable=False, index=True))\n    description: str = Field(sa_column=Column(Text, nullable=False))\n    embedding: list[float] | None = Field(default=None, sa_column=Column(Vector(), nullable=True))\n    summary: str | None = Field(default=None, sa_column=Column(Text, nullable=True))\n\n\nclass CategoryItemModel(BaseModelMixin, CategoryItem):\n    item_id: str = Field(sa_column=Column(ForeignKey(\"memory_items.id\", ondelete=\"CASCADE\"), nullable=False))\n    category_id: str = Field(sa_column=Column(ForeignKey(\"memory_categories.id\", ondelete=\"CASCADE\"), nullable=False))\n\n    __table_args__ = (Index(\"idx_category_items_unique\", \"item_id\", \"category_id\", unique=True),)\n\n\ndef _normalize_table_args(table_args: Any) -> tuple[list[Any], dict[str, Any]]:\n    if table_args is None:\n        return [], {}\n    if isinstance(table_args, dict):\n        return [], dict(table_args)\n    if not isinstance(table_args, tuple):\n        return [table_args], {}\n\n    args = list(table_args)\n    kwargs: dict[str, Any] = {}\n    if args and isinstance(args[-1], dict):\n        kwargs = dict(args.pop())\n    return args, kwargs\n\n\ndef _merge_models(\n    user_model: type[BaseModel],\n    core_model: type[SQLModel],\n    *,\n    name_suffix: str,\n    base_attrs: dict[str, Any],\n) -> type[SQLModel]:\n    overlap = set(user_model.model_fields) & set(core_model.model_fields)\n    if overlap:\n        msg = f\"Scope fields conflict with core model fields: {sorted(overlap)}\"\n        raise TypeError(msg)\n\n    return type(\n        f\"{user_model.__name__}{core_model.__name__}{name_suffix}\",\n        (user_model, core_model),\n        base_attrs,\n    )\n\n\ndef build_table_model(\n    user_model: type[BaseModel],\n    core_model: type[SQLModel],\n    *,\n    tablename: str,\n    metadata: MetaData | None = None,\n    extra_table_args: tuple[Any, ...] | None = None,\n    unique_with_scope: list[str] | None = None,\n) -> type[SQLModel]:\n    overlap = set(user_model.model_fields) & set(core_model.model_fields)\n    if overlap:\n        msg = f\"Scope fields conflict with core model fields: {sorted(overlap)}\"\n        raise TypeError(msg)\n\n    scope_fields = list(user_model.model_fields.keys())\n    base_table_args, table_kwargs = _normalize_table_args(getattr(core_model, \"__table_args__\", None))\n    table_args = list(base_table_args)\n    if extra_table_args:\n        table_args.extend(extra_table_args)\n    if scope_fields:\n        table_args.append(Index(f\"ix_{tablename}__scope\", *scope_fields))\n    if unique_with_scope:\n        unique_cols = [*unique_with_scope, *scope_fields]\n        table_args.append(Index(f\"ix_{tablename}__unique_scoped\", *unique_cols, unique=True))\n\n    base_attrs: dict[str, Any] = {\"__module__\": core_model.__module__, \"__tablename__\": tablename}\n    if metadata is not None:\n        base_attrs[\"metadata\"] = metadata\n    if table_args or table_kwargs:\n        if table_kwargs:\n            base_attrs[\"__table_args__\"] = (*table_args, table_kwargs)\n        else:\n            base_attrs[\"__table_args__\"] = tuple(table_args)\n\n    base = _merge_models(user_model, core_model, name_suffix=\"Base\", base_attrs=base_attrs)\n\n    # Use type() instead of create_model to properly preserve SQLModel table behavior\n    table_attrs: dict[str, Any] = {\"__module__\": core_model.__module__}\n    return type(\n        f\"{user_model.__name__}{core_model.__name__}Table\",\n        (base,),\n        table_attrs,\n        table=True,\n    )\n\n\ndef build_scoped_models(\n    user_model: type[BaseModel],\n) -> tuple[type[SQLModel], type[SQLModel], type[SQLModel], type[SQLModel]]:\n    \"\"\"\n    Build scoped SQLModel tables for each entity (resource, category, item, relation).\n    \"\"\"\n    resource_model = build_table_model(user_model, ResourceModel, tablename=\"resources\")\n    memory_category_model = build_table_model(\n        user_model, MemoryCategoryModel, tablename=\"memory_categories\", unique_with_scope=[\"name\"]\n    )\n    memory_item_model = build_table_model(user_model, MemoryItemModel, tablename=\"memory_items\")\n    category_item_model = build_table_model(user_model, CategoryItemModel, tablename=\"category_items\")\n    return resource_model, memory_category_model, memory_item_model, category_item_model\n\n\n__all__ = [\n    \"BaseModelMixin\",\n    \"CategoryItemModel\",\n    \"MemoryCategoryModel\",\n    \"MemoryItemModel\",\n    \"ResourceModel\",\n    \"build_scoped_models\",\n    \"build_table_model\",\n]\n"
  },
  {
    "path": "src/memu/database/postgres/postgres.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom memu.database.interfaces import Database\nfrom memu.database.models import CategoryItem, MemoryCategory, MemoryItem, Resource\nfrom memu.database.postgres.migration import DDLMode, run_migrations\nfrom memu.database.postgres.repositories.category_item_repo import PostgresCategoryItemRepo\nfrom memu.database.postgres.repositories.memory_category_repo import PostgresMemoryCategoryRepo\nfrom memu.database.postgres.repositories.memory_item_repo import PostgresMemoryItemRepo\nfrom memu.database.postgres.repositories.resource_repo import PostgresResourceRepo\nfrom memu.database.postgres.schema import SQLAModels, get_sqlalchemy_models, require_sqlalchemy\nfrom memu.database.postgres.session import SessionManager\nfrom memu.database.repositories import CategoryItemRepo, MemoryCategoryRepo, MemoryItemRepo, ResourceRepo\nfrom memu.database.state import DatabaseState\n\nlogger = logging.getLogger(__name__)\n\n\nclass PostgresStore(Database):\n    resource_repo: ResourceRepo\n    memory_category_repo: MemoryCategoryRepo\n    memory_item_repo: MemoryItemRepo\n    category_item_repo: CategoryItemRepo\n    resources: dict[str, Resource]\n    items: dict[str, MemoryItem]\n    categories: dict[str, MemoryCategory]\n    relations: list[CategoryItem]\n\n    def __init__(\n        self,\n        *,\n        dsn: str,\n        ddl_mode: DDLMode = \"create\",\n        vector_provider: str | None = None,\n        scope_model: type[BaseModel] | None = None,\n        base_model: type[BaseModel] | None = None,\n        resource_model: type[Any] | None = None,\n        memory_category_model: type[Any] | None = None,\n        memory_item_model: type[Any] | None = None,\n        category_item_model: type[Any] | None = None,\n        sqla_models: SQLAModels | None = None,\n    ) -> None:\n        require_sqlalchemy()\n        self.dsn = dsn\n        self.ddl_mode = ddl_mode\n        self.vector_provider = vector_provider\n        self._use_vector_type = vector_provider == \"pgvector\"\n        self._scope_model: type[BaseModel] = scope_model or base_model or BaseModel\n        self._scope_fields = list(getattr(self._scope_model, \"model_fields\", {}).keys())\n        self._state = DatabaseState()\n        self._sessions = SessionManager(dsn=self.dsn)\n        self._sqla_models: SQLAModels = sqla_models or get_sqlalchemy_models(scope_model=self._scope_model)\n        run_migrations(dsn=self.dsn, scope_model=self._scope_model, ddl_mode=self.ddl_mode)\n\n        resource_model = resource_model or self._sqla_models.Resource\n        memory_category_model = memory_category_model or self._sqla_models.MemoryCategory\n        memory_item_model = memory_item_model or self._sqla_models.MemoryItem\n        category_item_model = category_item_model or self._sqla_models.CategoryItem\n\n        self.resource_repo = PostgresResourceRepo(\n            state=self._state,\n            resource_model=resource_model,\n            sqla_models=self._sqla_models,\n            sessions=self._sessions,\n            scope_fields=self._scope_fields,\n        )\n        self.memory_category_repo = PostgresMemoryCategoryRepo(\n            state=self._state,\n            memory_category_model=memory_category_model,\n            sqla_models=self._sqla_models,\n            sessions=self._sessions,\n            scope_fields=self._scope_fields,\n        )\n        self.memory_item_repo = PostgresMemoryItemRepo(\n            state=self._state,\n            memory_item_model=memory_item_model,\n            sqla_models=self._sqla_models,\n            sessions=self._sessions,\n            scope_fields=self._scope_fields,\n            use_vector=self._use_vector_type,\n        )\n        self.category_item_repo = PostgresCategoryItemRepo(\n            state=self._state,\n            category_item_model=category_item_model,\n            sqla_models=self._sqla_models,\n            sessions=self._sessions,\n            scope_fields=self._scope_fields,\n        )\n\n        self.resources = self._state.resources\n        self.items = self._state.items\n        self.categories = self._state.categories\n        self.relations = self._state.relations\n\n        # self._load_existing()\n\n    def close(self) -> None:\n        self._sessions.close()\n\n    def _load_existing(self) -> None:\n        self.resource_repo.load_existing()\n        self.memory_category_repo.load_existing()\n        self.memory_item_repo.load_existing()\n        self.category_item_repo.load_existing()\n"
  },
  {
    "path": "src/memu/database/postgres/repositories/__init__.py",
    "content": "from memu.database.postgres.repositories.category_item_repo import PostgresCategoryItemRepo\nfrom memu.database.postgres.repositories.memory_category_repo import PostgresMemoryCategoryRepo\nfrom memu.database.postgres.repositories.memory_item_repo import PostgresMemoryItemRepo\nfrom memu.database.postgres.repositories.resource_repo import PostgresResourceRepo\n\n__all__ = [\n    \"PostgresCategoryItemRepo\",\n    \"PostgresMemoryCategoryRepo\",\n    \"PostgresMemoryItemRepo\",\n    \"PostgresResourceRepo\",\n]\n"
  },
  {
    "path": "src/memu/database/postgres/repositories/base.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom collections.abc import Mapping\nfrom typing import Any\n\nimport pendulum\n\nfrom memu.database.postgres.session import SessionManager\nfrom memu.database.state import DatabaseState\n\nlogger = logging.getLogger(__name__)\n\n\nclass PostgresRepoBase:\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        sqla_models: Any,\n        sessions: SessionManager,\n        scope_fields: list[str],\n        use_vector: bool = True,\n    ) -> None:\n        self._state = state\n        self._sqla_models = sqla_models\n        self._sessions = sessions\n        self._scope_fields = scope_fields\n        self._use_vector = use_vector\n\n    def _scope_kwargs_from(self, obj: Any) -> dict[str, Any]:\n        return {field: getattr(obj, field, None) for field in self._scope_fields}\n\n    def _normalize_embedding(self, embedding: Any) -> list[float] | None:\n        if embedding is None:\n            return None\n        if hasattr(embedding, \"to_list\"):\n            try:\n                return [float(x) for x in embedding.to_list()]\n            except Exception:\n                logger.debug(\"Could not convert pgvector value %s\", embedding)\n                return None\n        if isinstance(embedding, str):\n            stripped = embedding.strip(\"[]\")\n            if not stripped:\n                return []\n            return [float(x) for x in stripped.split(\",\")]\n        try:\n            return [float(x) for x in embedding]\n        except Exception:\n            logger.debug(\"Could not normalize embedding %s\", embedding)\n            return None\n\n    def _prepare_embedding(self, embedding: list[float] | None) -> Any:\n        if embedding is None:\n            return None\n        return embedding\n\n    def _merge_and_commit(self, obj: Any) -> None:\n        with self._sessions.session() as session:\n            session.merge(obj)\n            session.commit()\n\n    def _now(self) -> pendulum.DateTime:\n        return pendulum.now(\"UTC\")\n\n    def _build_filters(self, model: Any, where: Mapping[str, Any] | None) -> list[Any]:\n        if not where:\n            return []\n        filters: list[Any] = []\n        for raw_key, expected in where.items():\n            if expected is None:\n                continue\n            field, op = [*raw_key.split(\"__\", 1), None][:2]\n            column = getattr(model, str(field), None)\n            if column is None:\n                msg = f\"Unknown filter field '{field}' for model '{model.__name__}'\"\n                raise ValueError(msg)\n            if op == \"in\":\n                if isinstance(expected, str):\n                    filters.append(column == expected)\n                else:\n                    filters.append(column.in_(expected))\n            else:\n                filters.append(column == expected)\n        return filters\n\n    @staticmethod\n    def _matches_where(obj: Any, where: Mapping[str, Any] | None) -> bool:\n        if not where:\n            return True\n        for raw_key, expected in where.items():\n            if expected is None:\n                continue\n            field, op = [*raw_key.split(\"__\", 1), None][:2]\n            actual = getattr(obj, str(field), None)\n            if op == \"in\":\n                if isinstance(expected, str):\n                    if actual != expected:\n                        return False\n                else:\n                    try:\n                        if actual not in expected:\n                            return False\n                    except TypeError:\n                        return False\n            else:\n                if actual != expected:\n                    return False\n        return True\n\n\n__all__ = [\"PostgresRepoBase\"]\n"
  },
  {
    "path": "src/memu/database/postgres/repositories/category_item_repo.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any\n\nfrom memu.database.models import CategoryItem\nfrom memu.database.postgres.repositories.base import PostgresRepoBase\nfrom memu.database.postgres.session import SessionManager\nfrom memu.database.repositories.category_item import CategoryItemRepo\nfrom memu.database.state import DatabaseState\n\n\nclass PostgresCategoryItemRepo(PostgresRepoBase, CategoryItemRepo):\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        category_item_model: type[CategoryItem],\n        sqla_models: Any,\n        sessions: SessionManager,\n        scope_fields: list[str],\n    ) -> None:\n        super().__init__(state=state, sqla_models=sqla_models, sessions=sessions, scope_fields=scope_fields)\n        self._category_item_model = category_item_model\n        self.relations: list[CategoryItem] = self._state.relations\n\n    def list_relations(self, where: Mapping[str, Any] | None = None) -> list[CategoryItem]:\n        from sqlmodel import select\n\n        filters = self._build_filters(self._sqla_models.CategoryItem, where)\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.CategoryItem).where(*filters)).all()\n        return [self._cache_relation(row) for row in rows]\n\n    def link_item_category(self, item_id: str, cat_id: str, user_data: dict[str, Any]) -> CategoryItem:\n        from sqlmodel import select\n\n        # Avoid duplicate inserts using local cache\n        for rel in self.relations:\n            if rel.item_id == item_id and rel.category_id == cat_id:\n                return rel\n\n        now = self._now()\n        new_rel = self._category_item_model(\n            item_id=item_id,\n            category_id=cat_id,\n            **user_data,\n            created_at=now,\n            updated_at=now,\n        )\n\n        with self._sessions.session() as session:\n            existing = session.scalar(\n                select(self._sqla_models.CategoryItem).where(\n                    self._sqla_models.CategoryItem.item_id == item_id,\n                    self._sqla_models.CategoryItem.category_id == cat_id,\n                )\n            )\n            if existing:\n                return self._cache_relation(existing)\n\n            session.add(new_rel)\n            session.commit()\n            session.refresh(new_rel)\n\n        return self._cache_relation(new_rel)\n\n    def unlink_item_category(self, item_id: str, cat_id: str) -> None:\n        from sqlmodel import delete\n\n        with self._sessions.session() as session:\n            session.exec(\n                delete(self._sqla_models.CategoryItem).where(\n                    self._sqla_models.CategoryItem.item_id == item_id,\n                    self._sqla_models.CategoryItem.category_id == cat_id,\n                )\n            )\n            session.commit()\n\n    def get_item_categories(self, item_id: str) -> list[CategoryItem]:\n        from sqlmodel import select\n\n        with self._sessions.session() as session:\n            rows = session.scalars(\n                select(self._sqla_models.CategoryItem).where(self._sqla_models.CategoryItem.item_id == item_id)\n            ).all()\n        return [self._cache_relation(row) for row in rows]\n\n    def load_existing(self) -> None:\n        from sqlmodel import select\n\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.CategoryItem)).all()\n            for row in rows:\n                self._cache_relation(row)\n\n    def _cache_relation(self, rel: CategoryItem) -> CategoryItem:\n        self.relations.append(rel)\n        return rel\n\n\n__all__ = [\"PostgresCategoryItemRepo\"]\n"
  },
  {
    "path": "src/memu/database/postgres/repositories/memory_category_repo.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any\n\nfrom memu.database.models import MemoryCategory\nfrom memu.database.postgres.repositories.base import PostgresRepoBase\nfrom memu.database.postgres.session import SessionManager\nfrom memu.database.repositories.memory_category import MemoryCategoryRepo\nfrom memu.database.state import DatabaseState\n\n\nclass PostgresMemoryCategoryRepo(PostgresRepoBase, MemoryCategoryRepo):\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        memory_category_model: type[MemoryCategory],\n        sqla_models: Any,\n        sessions: SessionManager,\n        scope_fields: list[str],\n    ) -> None:\n        super().__init__(state=state, sqla_models=sqla_models, sessions=sessions, scope_fields=scope_fields)\n        self._memory_category_model = memory_category_model\n        self.categories: dict[str, MemoryCategory] = self._state.categories\n\n    def list_categories(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryCategory]:\n        from sqlmodel import select\n\n        filters = self._build_filters(self._sqla_models.MemoryCategory, where)\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.MemoryCategory).where(*filters)).all()\n            result: dict[str, MemoryCategory] = {}\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                cat = self._cache_category(row)\n                result[cat.id] = cat\n        return result\n\n    def clear_categories(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryCategory]:\n        from sqlmodel import delete, select\n\n        filters = self._build_filters(self._sqla_models.MemoryCategory, where)\n        with self._sessions.session() as session:\n            # First get the objects to delete\n            rows = session.scalars(select(self._sqla_models.MemoryCategory).where(*filters)).all()\n            deleted: dict[str, MemoryCategory] = {}\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                deleted[row.id] = row\n\n            if not deleted:\n                return {}\n\n            # Delete from database\n            session.exec(delete(self._sqla_models.MemoryCategory).where(*filters))\n            session.commit()\n\n            # Clean up cache\n            for cat_id in deleted:\n                self.categories.pop(cat_id, None)\n\n        return deleted\n\n    def get_or_create_category(\n        self,\n        *,\n        name: str,\n        description: str,\n        embedding: list[float],\n        user_data: dict[str, Any],\n    ) -> MemoryCategory:\n        from sqlmodel import select\n\n        now = self._now()\n        with self._sessions.session() as session:\n            filters = [self._sqla_models.MemoryCategory.name == name]\n            for key, value in user_data.items():\n                filters.append(getattr(self._sqla_models.MemoryCategory, key) == value)\n            existing = session.scalar(select(self._sqla_models.MemoryCategory).where(*filters))\n\n            if existing:\n                updated = False\n                if getattr(existing, \"embedding\", None) is None:\n                    existing.embedding = self._prepare_embedding(embedding)\n                    updated = True\n                if getattr(existing, \"description\", None) is None:\n                    existing.description = description\n                    updated = True\n                if updated:\n                    existing.updated_at = now\n                    session.add(existing)\n                    session.commit()\n                    session.refresh(existing)\n                return self._cache_category(existing)\n\n            cat = self._memory_category_model(\n                name=name,\n                description=description,\n                embedding=self._prepare_embedding(embedding),\n                created_at=now,\n                updated_at=now,\n                **user_data,\n            )\n            session.add(cat)\n            session.commit()\n            session.refresh(cat)\n\n        return self._cache_category(cat)\n\n    def update_category(\n        self,\n        *,\n        category_id: str,\n        name: str | None = None,\n        description: str | None = None,\n        embedding: list[float] | None = None,\n        summary: str | None = None,\n    ) -> MemoryCategory:\n        from sqlmodel import select\n\n        now = self._now()\n        with self._sessions.session() as session:\n            cat = session.scalar(\n                select(self._sqla_models.MemoryCategory).where(self._sqla_models.MemoryCategory.id == category_id)\n            )\n            if cat is None:\n                msg = f\"Category with id {category_id} not found\"\n                raise KeyError(msg)\n\n            if name is not None:\n                cat.name = name\n            if description is not None:\n                cat.description = description\n            if embedding is not None:\n                cat.embedding = self._prepare_embedding(embedding)\n            if summary is not None:\n                cat.summary = summary\n\n            cat.updated_at = now\n            session.add(cat)\n            session.commit()\n            session.refresh(cat)\n            cat.embedding = self._normalize_embedding(cat.embedding)\n\n        return self._cache_category(cat)\n\n    def load_existing(self) -> None:\n        from sqlmodel import select\n\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.MemoryCategory)).all()\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                self._cache_category(row)\n\n    def _cache_category(self, cat: MemoryCategory) -> MemoryCategory:\n        self.categories[cat.id] = cat\n        return cat\n\n\n__all__ = [\"PostgresMemoryCategoryRepo\"]\n"
  },
  {
    "path": "src/memu/database/postgres/repositories/memory_item_repo.py",
    "content": "from __future__ import annotations\n\nimport math\nfrom collections.abc import Mapping\nfrom datetime import datetime\nfrom typing import Any\n\nfrom memu.database.models import MemoryItem, MemoryType, compute_content_hash\nfrom memu.database.postgres.repositories.base import PostgresRepoBase\nfrom memu.database.postgres.session import SessionManager\nfrom memu.database.state import DatabaseState\n\n\nclass PostgresMemoryItemRepo(PostgresRepoBase):\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        memory_item_model: type[MemoryItem],\n        sqla_models: Any,\n        sessions: SessionManager,\n        scope_fields: list[str],\n        use_vector: bool,\n    ) -> None:\n        super().__init__(\n            state=state, sqla_models=sqla_models, sessions=sessions, scope_fields=scope_fields, use_vector=use_vector\n        )\n        self._memory_item_model = memory_item_model\n        self.items: dict[str, MemoryItem] = self._state.items\n\n    def get_item(self, memory_id: str) -> MemoryItem | None:\n        from sqlmodel import select\n\n        with self._sessions.session() as session:\n            row = session.scalar(\n                select(self._sqla_models.MemoryItem).where(self._sqla_models.MemoryItem.id == memory_id)\n            )\n            if row:\n                row.embedding = self._normalize_embedding(row.embedding)\n                return self._cache_item(row)\n        return None\n\n    def list_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]:\n        from sqlmodel import select\n\n        filters = self._build_filters(self._sqla_models.MemoryItem, where)\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.MemoryItem).where(*filters)).all()\n            result: dict[str, MemoryItem] = {}\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                item = self._cache_item(row)\n                result[item.id] = item\n        return result\n\n    def list_items_by_ref_ids(\n        self, ref_ids: list[str], where: Mapping[str, Any] | None = None\n    ) -> dict[str, MemoryItem]:\n        \"\"\"List items by their ref_id in the extra column.\n\n        Args:\n            ref_ids: List of ref_ids to query.\n            where: Additional filter conditions.\n\n        Returns:\n            Dict mapping item_id -> MemoryItem for items whose extra->>'ref_id' is in ref_ids.\n        \"\"\"\n        if not ref_ids:\n            return {}\n\n        from sqlmodel import select\n\n        filters = self._build_filters(self._sqla_models.MemoryItem, where)\n        # Add filter for extra->>'ref_id' IN ref_ids (only rows with ref_id key)\n        ref_id_col = self._sqla_models.MemoryItem.extra[\"ref_id\"].astext\n        filters.append(ref_id_col.isnot(None))\n        filters.append(ref_id_col.in_(ref_ids))\n\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.MemoryItem).where(*filters)).all()\n            result: dict[str, MemoryItem] = {}\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                item = self._cache_item(row)\n                result[item.id] = item\n        return result\n\n    def clear_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]:\n        from sqlmodel import delete, select\n\n        filters = self._build_filters(self._sqla_models.MemoryItem, where)\n        with self._sessions.session() as session:\n            # First get the objects to delete\n            rows = session.scalars(select(self._sqla_models.MemoryItem).where(*filters)).all()\n            deleted: dict[str, MemoryItem] = {}\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                deleted[row.id] = row\n\n            if not deleted:\n                return {}\n\n            # Delete from database\n            session.exec(delete(self._sqla_models.MemoryItem).where(*filters))\n            session.commit()\n\n            # Clean up cache\n            for item_id in deleted:\n                self.items.pop(item_id, None)\n\n        return deleted\n\n    def create_item(\n        self,\n        *,\n        resource_id: str | None = None,\n        memory_type: MemoryType,\n        summary: str,\n        embedding: list[float],\n        user_data: dict[str, Any],\n        reinforce: bool = False,\n        tool_record: dict[str, Any] | None = None,\n    ) -> MemoryItem:\n        if reinforce and memory_type != \"tool\":\n            return self.create_item_reinforce(\n                resource_id=resource_id,\n                memory_type=memory_type,\n                summary=summary,\n                embedding=embedding,\n                user_data=user_data,\n            )\n\n        # Build extra dict with tool_record fields at top level\n        extra: dict[str, Any] = {}\n        if tool_record:\n            if tool_record.get(\"when_to_use\") is not None:\n                extra[\"when_to_use\"] = tool_record[\"when_to_use\"]\n            if tool_record.get(\"metadata\") is not None:\n                extra[\"metadata\"] = tool_record[\"metadata\"]\n            if tool_record.get(\"tool_calls\") is not None:\n                extra[\"tool_calls\"] = tool_record[\"tool_calls\"]\n\n        item = self._memory_item_model(\n            resource_id=resource_id,\n            memory_type=memory_type,\n            summary=summary,\n            embedding=self._prepare_embedding(embedding),\n            extra=extra if extra else {},\n            **user_data,\n            created_at=self._now(),\n            updated_at=self._now(),\n        )\n\n        with self._sessions.session() as session:\n            session.add(item)\n            session.commit()\n            session.refresh(item)\n\n        self.items[item.id] = item\n        return item\n\n    def create_item_reinforce(\n        self,\n        *,\n        resource_id: str | None = None,\n        memory_type: MemoryType,\n        summary: str,\n        embedding: list[float],\n        user_data: dict[str, Any],\n    ) -> MemoryItem:\n        from sqlmodel import select\n\n        content_hash = compute_content_hash(summary, memory_type)\n\n        with self._sessions.session() as session:\n            # Check for existing item with same hash in same scope (deduplication)\n            # Use extra->>'content_hash' for query performance\n            content_hash_col = self._sqla_models.MemoryItem.extra[\"content_hash\"].astext\n            filters = [content_hash_col == content_hash]\n            filters.extend(self._build_filters(self._sqla_models.MemoryItem, user_data))\n\n            existing = session.scalar(select(self._sqla_models.MemoryItem).where(*filters))\n\n            if existing:\n                # Reinforce existing memory instead of creating duplicate\n                current_extra = existing.extra or {}\n                current_count = current_extra.get(\"reinforcement_count\", 1)\n                existing.extra = {\n                    **current_extra,\n                    \"reinforcement_count\": current_count + 1,\n                    \"last_reinforced_at\": self._now().isoformat(),\n                }\n                existing.updated_at = self._now()\n                session.add(existing)\n                session.commit()\n                session.refresh(existing)\n                existing.embedding = self._normalize_embedding(existing.embedding)\n                return self._cache_item(existing)\n\n            # Create new item with salience tracking in extra\n            now = self._now()\n\n            item = self._memory_item_model(\n                resource_id=resource_id,\n                memory_type=memory_type,\n                summary=summary,\n                embedding=self._prepare_embedding(embedding),\n                **user_data,\n                created_at=now,\n                updated_at=now,\n                extra={\n                    \"content_hash\": content_hash,\n                    \"reinforcement_count\": 1,\n                    \"last_reinforced_at\": now.isoformat(),\n                },\n            )\n\n            session.add(item)\n            session.commit()\n            session.refresh(item)\n\n        self.items[item.id] = item\n        return item\n\n    def update_item(\n        self,\n        *,\n        item_id: str,\n        memory_type: MemoryType | None = None,\n        summary: str | None = None,\n        embedding: list[float] | None = None,\n        extra: dict[str, Any] | None = None,\n        tool_record: dict[str, Any] | None = None,\n    ) -> MemoryItem:\n        from sqlmodel import select\n\n        now = self._now()\n        with self._sessions.session() as session:\n            item = session.scalar(\n                select(self._sqla_models.MemoryItem).where(self._sqla_models.MemoryItem.id == item_id)\n            )\n            if item is None:\n                msg = f\"Item with id {item_id} not found\"\n                raise KeyError(msg)\n\n            if memory_type is not None:\n                item.memory_type = memory_type\n            if summary is not None:\n                item.summary = summary\n            if embedding is not None:\n                item.embedding = self._prepare_embedding(embedding)\n\n            # Merge extra and tool_record into existing extra dict\n            current_extra = item.extra or {}\n            if extra is not None:\n                current_extra = {**current_extra, **extra}\n            if tool_record is not None:\n                # Merge tool_record fields at top level\n                for key in (\"when_to_use\", \"metadata\", \"tool_calls\"):\n                    if tool_record.get(key) is not None:\n                        current_extra[key] = tool_record[key]\n            if extra is not None or tool_record is not None:\n                item.extra = current_extra\n\n            item.updated_at = now\n            session.add(item)\n            session.commit()\n            session.refresh(item)\n            item.embedding = self._normalize_embedding(item.embedding)\n\n        return self._cache_item(item)\n\n    def delete_item(self, item_id: str) -> None:\n        from sqlmodel import delete\n\n        with self._sessions.session() as session:\n            session.exec(delete(self._sqla_models.MemoryItem).where(self._sqla_models.MemoryItem.id == item_id))\n            session.commit()\n\n    def vector_search_items(\n        self,\n        query_vec: list[float],\n        top_k: int,\n        where: Mapping[str, Any] | None = None,\n        *,\n        ranking: str = \"similarity\",\n        recency_decay_days: float = 30.0,\n    ) -> list[tuple[str, float]]:\n        if not self._use_vector or ranking == \"salience\":\n            # For salience ranking or when pgvector is not available, use local search\n            return self._vector_search_local(\n                query_vec, top_k, where=where, ranking=ranking, recency_decay_days=recency_decay_days\n            )\n\n        from sqlmodel import select\n\n        distance = self._sqla_models.MemoryItem.embedding.cosine_distance(query_vec)\n        filters = [self._sqla_models.MemoryItem.embedding.isnot(None)]\n        filters.extend(self._build_filters(self._sqla_models.MemoryItem, where))\n        stmt = (\n            select(self._sqla_models.MemoryItem.id, (1 - distance).label(\"score\"))\n            .where(*filters)\n            .order_by(distance)\n            .limit(top_k)\n        )\n        with self._sessions.session() as session:\n            rows = session.execute(stmt).all()\n        return [(rid, float(score)) for rid, score in rows]\n\n    def load_existing(self) -> None:\n        from sqlmodel import select\n\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.MemoryItem)).all()\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                self._cache_item(row)\n\n    def _vector_search_local(\n        self,\n        query_vec: list[float],\n        top_k: int,\n        where: Mapping[str, Any] | None = None,\n        *,\n        ranking: str = \"similarity\",\n        recency_decay_days: float = 30.0,\n    ) -> list[tuple[str, float]]:\n        scored: list[tuple[str, float]] = []\n        for item in self.items.values():\n            if item.embedding is None:\n                continue\n            if not self._matches_where(item, where):\n                continue\n\n            similarity = self._cosine(query_vec, item.embedding)\n\n            if ranking == \"salience\":\n                # Salience-aware scoring - read from extra dict\n                extra = item.extra or {}\n                reinforcement_count = extra.get(\"reinforcement_count\", 1)\n                last_reinforced_at = self._parse_datetime(extra.get(\"last_reinforced_at\"))\n                score = self._salience_score(\n                    similarity,\n                    reinforcement_count,\n                    last_reinforced_at,\n                    recency_decay_days,\n                )\n            else:\n                score = similarity\n\n            scored.append((item.id, score))\n\n        scored.sort(key=lambda x: x[1], reverse=True)\n        return scored[:top_k]\n\n    @staticmethod\n    def _salience_score(\n        similarity: float,\n        reinforcement_count: int,\n        last_reinforced_at: datetime | None,\n        recency_decay_days: float,\n    ) -> float:\n        \"\"\"Compute salience score: similarity * reinforcement * recency.\"\"\"\n        reinforcement_factor = math.log(reinforcement_count + 1)\n\n        if last_reinforced_at is None:\n            recency_factor = 0.5\n        else:\n            now = datetime.now(last_reinforced_at.tzinfo) if last_reinforced_at.tzinfo else datetime.utcnow()\n            days_ago = (now - last_reinforced_at).total_seconds() / 86400\n            recency_factor = math.exp(-0.693 * days_ago / recency_decay_days)\n\n        return similarity * reinforcement_factor * recency_factor\n\n    def _cache_item(self, item: MemoryItem) -> MemoryItem:\n        self.items[item.id] = item\n        return item\n\n    @staticmethod\n    def _parse_datetime(dt_str: str | None) -> datetime | None:\n        \"\"\"Parse ISO datetime string from extra dict.\"\"\"\n        if dt_str is None:\n            return None\n        try:\n            import pendulum\n\n            parsed = pendulum.parse(dt_str)\n        except (ValueError, TypeError):\n            return None\n        else:\n            if isinstance(parsed, datetime):\n                return parsed\n            return None\n\n    @staticmethod\n    def _cosine(a: list[float], b: list[float]) -> float:\n        denom = (sum(x * x for x in a) ** 0.5) * (sum(y * y for y in b) ** 0.5) + 1e-9\n        return float(sum(x * y for x, y in zip(a, b, strict=True)) / denom)\n\n\n__all__ = [\"PostgresMemoryItemRepo\"]\n"
  },
  {
    "path": "src/memu/database/postgres/repositories/resource_repo.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any\n\nfrom memu.database.models import Resource\nfrom memu.database.postgres.repositories.base import PostgresRepoBase\nfrom memu.database.postgres.session import SessionManager\nfrom memu.database.repositories.resource import ResourceRepo\nfrom memu.database.state import DatabaseState\n\n\nclass PostgresResourceRepo(PostgresRepoBase, ResourceRepo):\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        resource_model: type[Resource],\n        sqla_models: Any,\n        sessions: SessionManager,\n        scope_fields: list[str],\n    ) -> None:\n        super().__init__(state=state, sqla_models=sqla_models, sessions=sessions, scope_fields=scope_fields)\n        self._resource_model = resource_model\n        self.resources: dict[str, Resource] = self._state.resources\n\n    def list_resources(self, where: Mapping[str, Any] | None = None) -> dict[str, Resource]:\n        from sqlmodel import select\n\n        filters = self._build_filters(self._sqla_models.Resource, where)\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.Resource).where(*filters)).all()\n            result: dict[str, Resource] = {}\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                res = self._cache_resource(row)\n                result[res.id] = res\n        return result\n\n    def clear_resources(self, where: Mapping[str, Any] | None = None) -> dict[str, Resource]:\n        from sqlmodel import delete, select\n\n        filters = self._build_filters(self._sqla_models.Resource, where)\n        with self._sessions.session() as session:\n            # First get the objects to delete\n            rows = session.scalars(select(self._sqla_models.Resource).where(*filters)).all()\n            deleted: dict[str, Resource] = {}\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                deleted[row.id] = row\n\n            if not deleted:\n                return {}\n\n            # Delete from database\n            session.exec(delete(self._sqla_models.Resource).where(*filters))\n            session.commit()\n\n            # Clean up cache\n            for res_id in deleted:\n                self.resources.pop(res_id, None)\n\n        return deleted\n\n    def create_resource(\n        self,\n        *,\n        url: str,\n        modality: str,\n        local_path: str,\n        caption: str | None,\n        embedding: list[float] | None,\n        user_data: dict[str, Any],\n    ) -> Resource:\n        res = self._resource_model(\n            url=url,\n            modality=modality,\n            local_path=local_path,\n            caption=caption,\n            embedding=self._prepare_embedding(embedding),\n            **user_data,\n            created_at=self._now(),\n            updated_at=self._now(),\n        )\n\n        with self._sessions.session() as session:\n            session.add(res)\n            session.commit()\n            session.refresh(res)\n\n        return self._cache_resource(res)\n\n    def load_existing(self) -> None:\n        from sqlmodel import select\n\n        with self._sessions.session() as session:\n            rows = session.scalars(select(self._sqla_models.Resource)).all()\n            for row in rows:\n                row.embedding = self._normalize_embedding(row.embedding)\n                self._cache_resource(row)\n\n    def _cache_resource(self, res: Resource) -> Resource:\n        self.resources[res.id] = res\n        return res\n\n\n__all__ = [\"PostgresResourceRepo\"]\n"
  },
  {
    "path": "src/memu/database/postgres/schema.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\ntry:\n    from sqlmodel import SQLModel\nexcept ImportError as exc:\n    msg = \"sqlmodel is required for Postgres storage support\"\n    raise ImportError(msg) from exc\n\ntry:\n    from sqlalchemy import MetaData\nexcept ImportError as exc:\n    msg = \"sqlalchemy is required for Postgres storage support\"\n    raise ImportError(msg) from exc\n\ntry:\n    from pgvector.sqlalchemy import VECTOR as Vector\nexcept ImportError as exc:\n    msg = \"pgvector is required for Postgres vector support\"\n    raise ImportError(msg) from exc\n\nfrom memu.database.postgres.models import (\n    CategoryItemModel,\n    MemoryCategoryModel,\n    MemoryItemModel,\n    ResourceModel,\n    build_table_model,\n)\n\n\n@dataclass\nclass SQLAModels:\n    Base: type[Any]\n    Resource: type[Any]\n    MemoryCategory: type[Any]\n    MemoryItem: type[Any]\n    CategoryItem: type[Any]\n\n\n_MODEL_CACHE: dict[type[Any], SQLAModels] = {}\n\n\ndef require_sqlalchemy() -> None:\n    return None\n\n\ndef get_sqlalchemy_models(*, scope_model: type[BaseModel] | None = None) -> SQLAModels:\n    \"\"\"\n    Build (and cache) SQLModel ORM models for Postgres storage.\n    \"\"\"\n    require_sqlalchemy()\n    scope = scope_model or BaseModel\n    cache_key = scope\n    cached = _MODEL_CACHE.get(cache_key)\n    if cached:\n        return cached\n\n    metadata_obj = MetaData()\n\n    resource_model = build_table_model(\n        scope,\n        ResourceModel,\n        tablename=\"resources\",\n        metadata=metadata_obj,\n    )\n    memory_category_model = build_table_model(\n        scope,\n        MemoryCategoryModel,\n        tablename=\"memory_categories\",\n        metadata=metadata_obj,\n    )\n    memory_item_model = build_table_model(\n        scope,\n        MemoryItemModel,\n        tablename=\"memory_items\",\n        metadata=metadata_obj,\n    )\n    category_item_model = build_table_model(\n        scope,\n        CategoryItemModel,\n        tablename=\"category_items\",\n        metadata=metadata_obj,\n    )\n\n    class Base(SQLModel):\n        __abstract__ = True\n        metadata = metadata_obj\n\n    models = SQLAModels(\n        Base=Base,\n        Resource=resource_model,\n        MemoryCategory=memory_category_model,\n        MemoryItem=memory_item_model,\n        CategoryItem=category_item_model,\n    )\n    _MODEL_CACHE[cache_key] = models\n    return models\n\n\ndef get_metadata(scope_model: type[BaseModel] | None = None) -> MetaData:\n    from typing import cast\n\n    return cast(MetaData, get_sqlalchemy_models(scope_model=scope_model).Base.metadata)\n\n\n__all__ = [\"SQLAModels\", \"Vector\", \"get_metadata\", \"get_sqlalchemy_models\", \"require_sqlalchemy\"]\n"
  },
  {
    "path": "src/memu/database/postgres/session.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import Any\n\ntry:  # Optional dependency for Postgres backend\n    from sqlmodel import Session, create_engine\nexcept ImportError as exc:  # pragma: no cover - optional dependency\n    msg = \"sqlmodel is required for Postgres storage support\"\n    raise ImportError(msg) from exc\n\nlogger = logging.getLogger(__name__)\n\n\nclass SessionManager:\n    \"\"\"Handle engine lifecycle and session creation for Postgres store.\"\"\"\n\n    def __init__(self, *, dsn: str, engine_kwargs: dict[str, Any] | None = None) -> None:\n        kw = {\"pool_pre_ping\": True}\n        if engine_kwargs:\n            kw.update(engine_kwargs)\n        self._engine = create_engine(dsn, **kw)\n\n    def session(self) -> Session:\n        return Session(self._engine, expire_on_commit=False)\n\n    def close(self) -> None:\n        try:\n            self._engine.dispose()\n        except Exception:\n            logger.exception(\"Failed to close Postgres engine\")\n\n\n__all__ = [\"SessionManager\"]\n"
  },
  {
    "path": "src/memu/database/repositories/__init__.py",
    "content": "from memu.database.repositories.category_item import CategoryItemRepo\nfrom memu.database.repositories.memory_category import MemoryCategoryRepo\nfrom memu.database.repositories.memory_item import MemoryItemRepo\nfrom memu.database.repositories.resource import ResourceRepo\n\n__all__ = [\"CategoryItemRepo\", \"MemoryCategoryRepo\", \"MemoryItemRepo\", \"ResourceRepo\"]\n"
  },
  {
    "path": "src/memu/database/repositories/category_item.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any, Protocol, runtime_checkable\n\nfrom memu.database.models import CategoryItem\n\n\n@runtime_checkable\nclass CategoryItemRepo(Protocol):\n    \"\"\"Repository contract for item/category relations.\"\"\"\n\n    relations: list[CategoryItem]\n\n    def list_relations(self, where: Mapping[str, Any] | None = None) -> list[CategoryItem]: ...\n\n    def link_item_category(self, item_id: str, cat_id: str, user_data: dict[str, Any]) -> CategoryItem: ...\n\n    def unlink_item_category(self, item_id: str, cat_id: str) -> None: ...\n\n    def get_item_categories(self, item_id: str) -> list[CategoryItem]: ...\n\n    def load_existing(self) -> None: ...\n"
  },
  {
    "path": "src/memu/database/repositories/memory_category.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any, Protocol, runtime_checkable\n\nfrom memu.database.models import MemoryCategory\n\n\n@runtime_checkable\nclass MemoryCategoryRepo(Protocol):\n    \"\"\"Repository contract for memory categories.\"\"\"\n\n    categories: dict[str, MemoryCategory]\n\n    def list_categories(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryCategory]: ...\n\n    def clear_categories(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryCategory]: ...\n\n    def get_or_create_category(\n        self, *, name: str, description: str, embedding: list[float], user_data: dict[str, Any]\n    ) -> MemoryCategory: ...\n\n    def update_category(\n        self,\n        *,\n        category_id: str,\n        name: str | None = None,\n        description: str | None = None,\n        embedding: list[float] | None = None,\n        summary: str | None = None,\n    ) -> MemoryCategory: ...\n\n    def load_existing(self) -> None: ...\n"
  },
  {
    "path": "src/memu/database/repositories/memory_item.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any, Protocol, runtime_checkable\n\nfrom memu.database.models import MemoryItem, MemoryType\n\n\n@runtime_checkable\nclass MemoryItemRepo(Protocol):\n    \"\"\"Repository contract for memory items.\"\"\"\n\n    items: dict[str, MemoryItem]\n\n    def get_item(self, item_id: str) -> MemoryItem | None: ...\n\n    def list_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]: ...\n\n    def clear_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]: ...\n\n    def create_item(\n        self,\n        *,\n        resource_id: str,\n        memory_type: MemoryType,\n        summary: str,\n        embedding: list[float],\n        user_data: dict[str, Any],\n        reinforce: bool = False,\n        tool_record: dict[str, Any] | None = None,\n    ) -> MemoryItem: ...\n\n    def update_item(\n        self,\n        *,\n        item_id: str,\n        memory_type: MemoryType | None = None,\n        summary: str | None = None,\n        embedding: list[float] | None = None,\n        extra: dict[str, Any] | None = None,\n        tool_record: dict[str, Any] | None = None,\n    ) -> MemoryItem: ...\n\n    def delete_item(self, item_id: str) -> None: ...\n\n    def list_items_by_ref_ids(\n        self, ref_ids: list[str], where: Mapping[str, Any] | None = None\n    ) -> dict[str, MemoryItem]: ...\n\n    def vector_search_items(\n        self, query_vec: list[float], top_k: int, where: Mapping[str, Any] | None = None\n    ) -> list[tuple[str, float]]: ...\n\n    def load_existing(self) -> None: ...\n"
  },
  {
    "path": "src/memu/database/repositories/resource.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Mapping\nfrom typing import Any, Protocol, runtime_checkable\n\nfrom memu.database.models import Resource\n\n\n@runtime_checkable\nclass ResourceRepo(Protocol):\n    \"\"\"Repository contract for resource records.\"\"\"\n\n    resources: dict[str, Resource]\n\n    def list_resources(self, where: Mapping[str, Any] | None = None) -> dict[str, Resource]: ...\n\n    def clear_resources(self, where: Mapping[str, Any] | None = None) -> dict[str, Resource]: ...\n\n    def create_resource(\n        self,\n        *,\n        url: str,\n        modality: str,\n        local_path: str,\n        caption: str | None,\n        embedding: list[float] | None,\n        user_data: dict[str, Any],\n    ) -> Resource: ...\n\n    def load_existing(self) -> None: ...\n"
  },
  {
    "path": "src/memu/database/sqlite/__init__.py",
    "content": "\"\"\"SQLite database backend for MemU.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pydantic import BaseModel\n\nfrom memu.app.settings import DatabaseConfig\nfrom memu.database.sqlite.sqlite import SQLiteStore\n\n\ndef build_sqlite_database(\n    *,\n    config: DatabaseConfig,\n    user_model: type[BaseModel],\n) -> SQLiteStore:\n    \"\"\"Build a SQLite database store instance.\n\n    Args:\n        config: Database configuration containing metadata_store settings.\n        user_model: Pydantic model for user scope fields.\n\n    Returns:\n        Configured SQLiteStore instance.\n    \"\"\"\n    dsn = config.metadata_store.dsn\n    if not dsn:\n        # Default to a local file if no DSN provided\n        dsn = \"sqlite:///memu.db\"\n\n    return SQLiteStore(\n        dsn=dsn,\n        scope_model=user_model,\n    )\n\n\n__all__ = [\"SQLiteStore\", \"build_sqlite_database\"]\n"
  },
  {
    "path": "src/memu/database/sqlite/models.py",
    "content": "\"\"\"SQLite-specific models for MemU database storage.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport logging\nimport uuid\nfrom datetime import datetime\nfrom typing import Any\n\nimport pendulum\nfrom pydantic import BaseModel\nfrom sqlalchemy import JSON, MetaData, String, Text\nfrom sqlmodel import Column, DateTime, Field, Index, SQLModel, func\n\nfrom memu.database.models import CategoryItem, MemoryCategory, MemoryItem, MemoryType, Resource\n\nlogger = logging.getLogger(__name__)\n\n\nclass TZDateTime(DateTime):\n    \"\"\"DateTime type with timezone support.\"\"\"\n\n    def __init__(self, timezone: bool = True, **kw: Any) -> None:\n        super().__init__(timezone=timezone, **kw)\n\n\nclass SQLiteBaseModelMixin(SQLModel):\n    \"\"\"Base mixin for SQLite models with common fields.\"\"\"\n\n    id: str = Field(\n        default_factory=lambda: str(uuid.uuid4()),\n        primary_key=True,\n        index=True,\n        sa_type=String,\n    )\n    created_at: datetime = Field(\n        default_factory=lambda: pendulum.now(\"UTC\"),\n        sa_type=TZDateTime,\n        sa_column_kwargs={\"server_default\": func.now()},\n    )\n    updated_at: datetime = Field(\n        default_factory=lambda: pendulum.now(\"UTC\"),\n        sa_type=TZDateTime,\n    )\n\n\nclass SQLiteResourceModel(SQLiteBaseModelMixin, Resource):\n    \"\"\"SQLite resource model.\"\"\"\n\n    url: str = Field(sa_column=Column(String, nullable=False))\n    modality: str = Field(sa_column=Column(String, nullable=False))\n    local_path: str = Field(sa_column=Column(String, nullable=False))\n    caption: str | None = Field(default=None, sa_column=Column(Text, nullable=True))\n    # Store embedding as JSON string since SQLite doesn't have native vector type\n    embedding_json: str | None = Field(default=None, sa_column=Column(Text, nullable=True))\n\n    @property\n    def embedding(self) -> list[float] | None:\n        \"\"\"Parse embedding from JSON string.\"\"\"\n        if self.embedding_json is None:\n            return None\n        try:\n            return list(json.loads(self.embedding_json))\n        except (json.JSONDecodeError, TypeError) as e:\n            logger.warning(\"Failed to parse resource embedding JSON: %s\", e)\n            return None\n\n    @embedding.setter\n    def embedding(self, value: list[float] | None) -> None:\n        \"\"\"Serialize embedding to JSON string.\"\"\"\n        if value is None:\n            self.embedding_json = None\n        else:\n            self.embedding_json = json.dumps(value)\n\n\nclass SQLiteMemoryItemModel(SQLiteBaseModelMixin, MemoryItem):\n    \"\"\"SQLite memory item model.\"\"\"\n\n    resource_id: str | None = Field(sa_column=Column(String, nullable=True))\n    memory_type: MemoryType = Field(sa_column=Column(String, nullable=False))\n    summary: str = Field(sa_column=Column(Text, nullable=False))\n    # Store embedding as JSON string since SQLite doesn't have native vector type\n    embedding_json: str | None = Field(default=None, sa_column=Column(Text, nullable=True))\n    happened_at: datetime | None = Field(default=None, sa_column=Column(DateTime, nullable=True))\n    extra: dict[str, Any] = Field(default={}, sa_column=Column(JSON, nullable=True))\n\n    @property\n    def embedding(self) -> list[float] | None:\n        \"\"\"Parse embedding from JSON string.\"\"\"\n        if self.embedding_json is None:\n            return None\n        try:\n            return list(json.loads(self.embedding_json))\n        except (json.JSONDecodeError, TypeError) as e:\n            logger.warning(\"Failed to parse memory item embedding JSON: %s\", e)\n            return None\n\n    @embedding.setter\n    def embedding(self, value: list[float] | None) -> None:\n        \"\"\"Serialize embedding to JSON string.\"\"\"\n        if value is None:\n            self.embedding_json = None\n        else:\n            self.embedding_json = json.dumps(value)\n\n\nclass SQLiteMemoryCategoryModel(SQLiteBaseModelMixin, MemoryCategory):\n    \"\"\"SQLite memory category model.\"\"\"\n\n    name: str = Field(sa_column=Column(String, nullable=False, index=True))\n    description: str = Field(sa_column=Column(Text, nullable=False))\n    # Store embedding as JSON string since SQLite doesn't have native vector type\n    embedding_json: str | None = Field(default=None, sa_column=Column(Text, nullable=True))\n    summary: str | None = Field(default=None, sa_column=Column(Text, nullable=True))\n\n    @property\n    def embedding(self) -> list[float] | None:\n        \"\"\"Parse embedding from JSON string.\"\"\"\n        if self.embedding_json is None:\n            return None\n        try:\n            return list(json.loads(self.embedding_json))\n        except (json.JSONDecodeError, TypeError) as e:\n            logger.warning(\"Failed to parse category embedding JSON: %s\", e)\n            return None\n\n    @embedding.setter\n    def embedding(self, value: list[float] | None) -> None:\n        \"\"\"Serialize embedding to JSON string.\"\"\"\n        if value is None:\n            self.embedding_json = None\n        else:\n            self.embedding_json = json.dumps(value)\n\n\nclass SQLiteCategoryItemModel(SQLiteBaseModelMixin, CategoryItem):\n    \"\"\"SQLite category-item relation model.\"\"\"\n\n    item_id: str = Field(sa_column=Column(String, nullable=False))\n    category_id: str = Field(sa_column=Column(String, nullable=False))\n\n    __table_args__ = (Index(\"idx_sqlite_category_items_unique\", \"item_id\", \"category_id\", unique=True),)\n\n\ndef _normalize_table_args(table_args: Any) -> tuple[list[Any], dict[str, Any]]:\n    \"\"\"Normalize SQLAlchemy table args to a consistent format.\"\"\"\n    if table_args is None:\n        return [], {}\n    if isinstance(table_args, dict):\n        return [], dict(table_args)\n    if not isinstance(table_args, tuple):\n        return [table_args], {}\n\n    args = list(table_args)\n    kwargs: dict[str, Any] = {}\n    if args and isinstance(args[-1], dict):\n        kwargs = dict(args.pop())\n    return args, kwargs\n\n\ndef _merge_models(\n    user_model: type[BaseModel],\n    core_model: type[SQLModel],\n    *,\n    name_suffix: str,\n    base_attrs: dict[str, Any],\n) -> type[SQLModel]:\n    \"\"\"Merge user scope model with core SQLModel.\"\"\"\n    overlap = set(user_model.model_fields) & set(core_model.model_fields)\n    if overlap:\n        msg = f\"Scope fields conflict with core model fields: {sorted(overlap)}\"\n        raise TypeError(msg)\n\n    return type(\n        f\"{user_model.__name__}{core_model.__name__}{name_suffix}\",\n        (user_model, core_model),\n        base_attrs,\n    )\n\n\ndef build_sqlite_table_model(\n    user_model: type[BaseModel],\n    core_model: type[SQLModel],\n    *,\n    tablename: str,\n    metadata: MetaData | None = None,\n    extra_table_args: tuple[Any, ...] | None = None,\n    unique_with_scope: list[str] | None = None,\n) -> type[SQLModel]:\n    \"\"\"Build a scoped SQLite table model.\"\"\"\n    overlap = set(user_model.model_fields) & set(core_model.model_fields)\n    if overlap:\n        msg = f\"Scope fields conflict with core model fields: {sorted(overlap)}\"\n        raise TypeError(msg)\n\n    scope_fields = list(user_model.model_fields.keys())\n    base_table_args, table_kwargs = _normalize_table_args(getattr(core_model, \"__table_args__\", None))\n    table_args = list(base_table_args)\n    if extra_table_args:\n        table_args.extend(extra_table_args)\n    if scope_fields:\n        table_args.append(Index(f\"ix_{tablename}__scope\", *scope_fields))\n    if unique_with_scope:\n        unique_cols = [*unique_with_scope, *scope_fields]\n        table_args.append(Index(f\"ix_{tablename}__unique_scoped\", *unique_cols, unique=True))\n\n    base_attrs: dict[str, Any] = {\"__module__\": core_model.__module__, \"__tablename__\": tablename}\n    if metadata is not None:\n        base_attrs[\"metadata\"] = metadata\n    if table_args or table_kwargs:\n        if table_kwargs:\n            base_attrs[\"__table_args__\"] = (*table_args, table_kwargs)\n        else:\n            base_attrs[\"__table_args__\"] = tuple(table_args)\n\n    base = _merge_models(user_model, core_model, name_suffix=\"SQLiteBase\", base_attrs=base_attrs)\n\n    # Use type() instead of create_model to properly preserve SQLModel table behavior\n    table_attrs: dict[str, Any] = {\"__module__\": core_model.__module__}\n    return type(\n        f\"{user_model.__name__}{core_model.__name__}SQLiteTable\",\n        (base,),\n        table_attrs,\n        table=True,\n    )\n\n\n__all__ = [\n    \"SQLiteBaseModelMixin\",\n    \"SQLiteCategoryItemModel\",\n    \"SQLiteMemoryCategoryModel\",\n    \"SQLiteMemoryItemModel\",\n    \"SQLiteResourceModel\",\n    \"build_sqlite_table_model\",\n]\n"
  },
  {
    "path": "src/memu/database/sqlite/repositories/__init__.py",
    "content": "\"\"\"SQLite repository implementations for MemU.\"\"\"\n\nfrom memu.database.sqlite.repositories.base import SQLiteRepoBase\nfrom memu.database.sqlite.repositories.category_item_repo import SQLiteCategoryItemRepo\nfrom memu.database.sqlite.repositories.memory_category_repo import SQLiteMemoryCategoryRepo\nfrom memu.database.sqlite.repositories.memory_item_repo import SQLiteMemoryItemRepo\nfrom memu.database.sqlite.repositories.resource_repo import SQLiteResourceRepo\n\n__all__ = [\n    \"SQLiteCategoryItemRepo\",\n    \"SQLiteMemoryCategoryRepo\",\n    \"SQLiteMemoryItemRepo\",\n    \"SQLiteRepoBase\",\n    \"SQLiteResourceRepo\",\n]\n"
  },
  {
    "path": "src/memu/database/sqlite/repositories/base.py",
    "content": "\"\"\"Base repository class for SQLite backend.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport logging\nfrom collections.abc import Mapping\nfrom typing import Any\n\nimport pendulum\n\nfrom memu.database.sqlite.session import SQLiteSessionManager\nfrom memu.database.state import DatabaseState\n\nlogger = logging.getLogger(__name__)\n\n\nclass SQLiteRepoBase:\n    \"\"\"Base class for SQLite repository implementations.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        sqla_models: Any,\n        sessions: SQLiteSessionManager,\n        scope_fields: list[str],\n    ) -> None:\n        \"\"\"Initialize base repository.\n\n        Args:\n            state: Shared database state for caching.\n            sqla_models: SQLAlchemy model definitions.\n            sessions: Session manager for database connections.\n            scope_fields: List of user scope field names.\n        \"\"\"\n        self._state = state\n        self._sqla_models = sqla_models\n        self._sessions = sessions\n        self._scope_fields = scope_fields\n\n    def _scope_kwargs_from(self, obj: Any) -> dict[str, Any]:\n        \"\"\"Extract scope fields from an object.\"\"\"\n        return {field: getattr(obj, field, None) for field in self._scope_fields}\n\n    def _normalize_embedding(self, embedding: Any) -> list[float] | None:\n        \"\"\"Normalize embedding from various formats to list[float].\"\"\"\n        if embedding is None:\n            return None\n        # Handle JSON string format (SQLite stores embeddings as JSON)\n        if isinstance(embedding, str):\n            try:\n                return [float(x) for x in json.loads(embedding)]\n            except (json.JSONDecodeError, TypeError):\n                logger.debug(\"Could not parse embedding JSON: %s\", embedding)\n                return None\n        # Handle list format\n        try:\n            return [float(x) for x in embedding]\n        except (ValueError, TypeError, OverflowError):\n            logger.debug(\"Could not normalize embedding %s\", embedding)\n            return None\n\n    def _prepare_embedding(self, embedding: list[float] | None) -> str | None:\n        \"\"\"Serialize embedding to JSON string for SQLite storage.\"\"\"\n        if embedding is None:\n            return None\n        return json.dumps(embedding)\n\n    def _merge_and_commit(self, obj: Any) -> None:\n        \"\"\"Merge object into session and commit.\"\"\"\n        with self._sessions.session() as session:\n            session.merge(obj)\n            session.commit()\n\n    def _now(self) -> pendulum.DateTime:\n        \"\"\"Get current UTC time.\"\"\"\n        return pendulum.now(\"UTC\")\n\n    def _build_filters(self, model: Any, where: Mapping[str, Any] | None) -> list[Any]:\n        \"\"\"Build SQLAlchemy filter expressions from where clause.\"\"\"\n        if not where:\n            return []\n        filters: list[Any] = []\n        for raw_key, expected in where.items():\n            if expected is None:\n                continue\n            field, op = [*raw_key.split(\"__\", 1), None][:2]\n            column = getattr(model, str(field), None)\n            if column is None:\n                msg = f\"Unknown filter field '{field}' for model '{model.__name__}'\"\n                raise ValueError(msg)\n            if op == \"in\":\n                if isinstance(expected, str):\n                    filters.append(column == expected)\n                else:\n                    filters.append(column.in_(expected))\n            else:\n                filters.append(column == expected)\n        return filters\n\n    @staticmethod\n    def _matches_where(obj: Any, where: Mapping[str, Any] | None) -> bool:\n        \"\"\"Check if object matches where clause (for in-memory filtering).\"\"\"\n        if not where:\n            return True\n        for raw_key, expected in where.items():\n            if expected is None:\n                continue\n            field, op = [*raw_key.split(\"__\", 1), None][:2]\n            actual = getattr(obj, str(field), None)\n            if op == \"in\":\n                if isinstance(expected, str):\n                    if actual != expected:\n                        return False\n                else:\n                    try:\n                        if actual not in expected:\n                            return False\n                    except TypeError:\n                        return False\n            else:\n                if actual != expected:\n                    return False\n        return True\n\n\n__all__ = [\"SQLiteRepoBase\"]\n"
  },
  {
    "path": "src/memu/database/sqlite/repositories/category_item_repo.py",
    "content": "\"\"\"SQLite category-item relation repository implementation.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import Mapping\nfrom typing import Any\n\nfrom sqlmodel import select\n\nfrom memu.database.models import CategoryItem\nfrom memu.database.repositories.category_item import CategoryItemRepo\nfrom memu.database.sqlite.repositories.base import SQLiteRepoBase\nfrom memu.database.sqlite.schema import SQLiteSQLAModels\nfrom memu.database.sqlite.session import SQLiteSessionManager\nfrom memu.database.state import DatabaseState\n\nlogger = logging.getLogger(__name__)\n\n\nclass SQLiteCategoryItemRepo(SQLiteRepoBase, CategoryItemRepo):\n    \"\"\"SQLite implementation of category-item relation repository.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        category_item_model: type[Any],\n        sqla_models: SQLiteSQLAModels,\n        sessions: SQLiteSessionManager,\n        scope_fields: list[str],\n    ) -> None:\n        \"\"\"Initialize category-item repository.\n\n        Args:\n            state: Shared database state for caching.\n            category_item_model: SQLModel class for category-item relations.\n            sqla_models: SQLAlchemy model container.\n            sessions: Session manager for database connections.\n            scope_fields: List of user scope field names.\n        \"\"\"\n        super().__init__(\n            state=state,\n            sqla_models=sqla_models,\n            sessions=sessions,\n            scope_fields=scope_fields,\n        )\n        self._category_item_model = category_item_model\n        self.relations = self._state.relations\n\n    def list_relations(self, where: Mapping[str, Any] | None = None) -> list[CategoryItem]:\n        \"\"\"List category-item relations matching the where clause.\n\n        Args:\n            where: Optional filter conditions.\n\n        Returns:\n            List of CategoryItem relations.\n        \"\"\"\n        with self._sessions.session() as session:\n            stmt = select(self._category_item_model)\n            filters = self._build_filters(self._category_item_model, where)\n            if filters:\n                stmt = stmt.where(*filters)\n            rows = session.exec(stmt).all()\n\n        result: list[CategoryItem] = []\n        for row in rows:\n            rel = CategoryItem(\n                id=row.id,\n                item_id=row.item_id,\n                category_id=row.category_id,\n                created_at=row.created_at,\n                updated_at=row.updated_at,\n                **self._scope_kwargs_from(row),\n            )\n            result.append(rel)\n            # Update cache\n            if not any(r.id == rel.id for r in self.relations):\n                self.relations.append(rel)\n\n        return result\n\n    def link_item_category(self, item_id: str, category_id: str, user_data: dict[str, Any]) -> CategoryItem:\n        \"\"\"Create a link between an item and a category.\n\n        Args:\n            item_id: Memory item ID.\n            category_id: Category ID.\n            user_data: User scope data.\n\n        Returns:\n            Created CategoryItem relation.\n        \"\"\"\n        # Check if relation already exists\n        where: dict[str, Any] = {\n            \"item_id\": item_id,\n            \"category_id\": category_id,\n            **user_data,\n        }\n        with self._sessions.session() as session:\n            stmt = select(self._category_item_model)\n            filters = self._build_filters(self._category_item_model, where)\n            if filters:\n                stmt = stmt.where(*filters)\n            existing = session.exec(stmt).first()\n\n            if existing:\n                rel = CategoryItem(\n                    id=existing.id,\n                    item_id=existing.item_id,\n                    category_id=existing.category_id,\n                    created_at=existing.created_at,\n                    updated_at=existing.updated_at,\n                    **self._scope_kwargs_from(existing),\n                )\n                return rel\n\n            # Create new relation\n            now = self._now()\n            row = self._category_item_model(\n                item_id=item_id,\n                category_id=category_id,\n                created_at=now,\n                updated_at=now,\n                **user_data,\n            )\n            session.add(row)\n            session.commit()\n            session.refresh(row)\n\n        rel = CategoryItem(\n            id=row.id,\n            item_id=row.item_id,\n            category_id=row.category_id,\n            created_at=row.created_at,\n            updated_at=row.updated_at,\n            **user_data,\n        )\n        self.relations.append(rel)\n        return rel\n\n    def unlink_item_category(self, item_id: str, category_id: str) -> None:\n        \"\"\"Remove a link between an item and a category.\n\n        Args:\n            item_id: Memory item ID.\n            category_id: Category ID.\n        \"\"\"\n        with self._sessions.session() as session:\n            stmt = select(self._category_item_model).where(\n                self._category_item_model.item_id == item_id,\n                self._category_item_model.category_id == category_id,\n            )\n            row = session.exec(stmt).first()\n            if row:\n                session.delete(row)\n                session.commit()\n                # Remove from cache\n                self.relations[:] = [\n                    r for r in self.relations if not (r.item_id == item_id and r.category_id == category_id)\n                ]\n\n    def get_item_categories(self, item_id: str) -> list[CategoryItem]:\n        \"\"\"Get all category relations for a given item.\n\n        Args:\n            item_id: Memory item ID.\n\n        Returns:\n            List of CategoryItem relations for the item.\n        \"\"\"\n        return self.list_relations({\"item_id\": item_id})\n\n    def load_existing(self) -> None:\n        \"\"\"Load all existing relations from database into cache.\"\"\"\n        self.list_relations()\n\n\n__all__ = [\"SQLiteCategoryItemRepo\"]\n"
  },
  {
    "path": "src/memu/database/sqlite/repositories/memory_category_repo.py",
    "content": "\"\"\"SQLite memory category repository implementation.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import Mapping\nfrom typing import Any\n\nfrom sqlmodel import delete, select\n\nfrom memu.database.models import MemoryCategory\nfrom memu.database.repositories.memory_category import MemoryCategoryRepo\nfrom memu.database.sqlite.repositories.base import SQLiteRepoBase\nfrom memu.database.sqlite.schema import SQLiteSQLAModels\nfrom memu.database.sqlite.session import SQLiteSessionManager\nfrom memu.database.state import DatabaseState\n\nlogger = logging.getLogger(__name__)\n\n\nclass SQLiteMemoryCategoryRepo(SQLiteRepoBase, MemoryCategoryRepo):\n    \"\"\"SQLite implementation of memory category repository.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        memory_category_model: type[Any],\n        sqla_models: SQLiteSQLAModels,\n        sessions: SQLiteSessionManager,\n        scope_fields: list[str],\n    ) -> None:\n        \"\"\"Initialize memory category repository.\n\n        Args:\n            state: Shared database state for caching.\n            memory_category_model: SQLModel class for memory categories.\n            sqla_models: SQLAlchemy model container.\n            sessions: Session manager for database connections.\n            scope_fields: List of user scope field names.\n        \"\"\"\n        super().__init__(\n            state=state,\n            sqla_models=sqla_models,\n            sessions=sessions,\n            scope_fields=scope_fields,\n        )\n        self._memory_category_model = memory_category_model\n        self.categories = self._state.categories\n\n    def list_categories(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryCategory]:\n        \"\"\"List categories matching the where clause.\n\n        Args:\n            where: Optional filter conditions.\n\n        Returns:\n            Dictionary of category ID to MemoryCategory mapping.\n        \"\"\"\n        with self._sessions.session() as session:\n            stmt = select(self._memory_category_model)\n            filters = self._build_filters(self._memory_category_model, where)\n            if filters:\n                stmt = stmt.where(*filters)\n            rows = session.exec(stmt).all()\n\n        result: dict[str, MemoryCategory] = {}\n        for row in rows:\n            cat = MemoryCategory(\n                id=row.id,\n                name=row.name,\n                description=row.description,\n                embedding=self._normalize_embedding(row.embedding_json),\n                summary=row.summary,\n                created_at=row.created_at,\n                updated_at=row.updated_at,\n                **self._scope_kwargs_from(row),\n            )\n            result[row.id] = cat\n            self.categories[row.id] = cat\n\n        return result\n\n    def clear_categories(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryCategory]:\n        \"\"\"Clear categories matching the where clause.\n\n        Args:\n            where: Optional filter conditions.\n\n        Returns:\n            Dictionary of deleted category ID to MemoryCategory mapping.\n        \"\"\"\n        filters = self._build_filters(self._memory_category_model, where)\n        with self._sessions.session() as session:\n            # First get the objects to delete\n            stmt = select(self._memory_category_model)\n            if filters:\n                stmt = stmt.where(*filters)\n            rows = session.exec(stmt).all()\n\n            deleted: dict[str, MemoryCategory] = {}\n            for row in rows:\n                cat = MemoryCategory(\n                    id=row.id,\n                    name=row.name,\n                    description=row.description,\n                    embedding=self._normalize_embedding(row.embedding_json),\n                    summary=row.summary,\n                    created_at=row.created_at,\n                    updated_at=row.updated_at,\n                    **self._scope_kwargs_from(row),\n                )\n                deleted[row.id] = cat\n\n            if not deleted:\n                return {}\n\n            # Delete from database\n            del_stmt = delete(self._memory_category_model)\n            if filters:\n                del_stmt = del_stmt.where(*filters)\n            session.exec(del_stmt)\n            session.commit()\n\n            # Clean up cache\n            for cat_id in deleted:\n                self.categories.pop(cat_id, None)\n\n        return deleted\n\n    def get_or_create_category(\n        self, *, name: str, description: str, embedding: list[float], user_data: dict[str, Any]\n    ) -> MemoryCategory:\n        \"\"\"Get existing category by name or create a new one.\n\n        Args:\n            name: Category name.\n            description: Category description.\n            embedding: Embedding vector.\n            user_data: User scope data.\n\n        Returns:\n            Existing or newly created MemoryCategory.\n        \"\"\"\n        # Check for existing category with same name and scope\n        where: dict[str, Any] = {\"name\": name, **user_data}\n        with self._sessions.session() as session:\n            stmt = select(self._memory_category_model)\n            filters = self._build_filters(self._memory_category_model, where)\n            if filters:\n                stmt = stmt.where(*filters)\n            existing = session.exec(stmt).first()\n\n            if existing:\n                cat = MemoryCategory(\n                    id=existing.id,\n                    name=existing.name,\n                    description=existing.description,\n                    embedding=self._normalize_embedding(existing.embedding_json),\n                    summary=existing.summary,\n                    created_at=existing.created_at,\n                    updated_at=existing.updated_at,\n                    **self._scope_kwargs_from(existing),\n                )\n                self.categories[existing.id] = cat\n                return cat\n\n            # Create new category\n            now = self._now()\n            row = self._memory_category_model(\n                name=name,\n                description=description,\n                embedding_json=self._prepare_embedding(embedding),\n                summary=None,\n                created_at=now,\n                updated_at=now,\n                **user_data,\n            )\n            session.add(row)\n            session.commit()\n            session.refresh(row)\n\n        cat = MemoryCategory(\n            id=row.id,\n            name=row.name,\n            description=row.description,\n            embedding=embedding,\n            summary=None,\n            created_at=row.created_at,\n            updated_at=row.updated_at,\n            **user_data,\n        )\n        self.categories[row.id] = cat\n        return cat\n\n    def update_category(\n        self,\n        *,\n        category_id: str,\n        name: str | None = None,\n        description: str | None = None,\n        embedding: list[float] | None = None,\n        summary: str | None = None,\n    ) -> MemoryCategory:\n        \"\"\"Update an existing category.\n\n        Args:\n            category_id: ID of category to update.\n            name: New name (optional).\n            description: New description (optional).\n            embedding: New embedding vector (optional).\n            summary: New summary text (optional).\n\n        Returns:\n            Updated MemoryCategory object.\n\n        Raises:\n            KeyError: If category not found.\n        \"\"\"\n        with self._sessions.session() as session:\n            stmt = select(self._memory_category_model).where(self._memory_category_model.id == category_id)\n            row = session.exec(stmt).first()\n\n            if row is None:\n                msg = f\"Category with id {category_id} not found\"\n                raise KeyError(msg)\n\n            if name is not None:\n                row.name = name\n            if description is not None:\n                row.description = description\n            if embedding is not None:\n                row.embedding_json = self._prepare_embedding(embedding)\n            if summary is not None:\n                row.summary = summary\n            row.updated_at = self._now()\n\n            session.add(row)\n            session.commit()\n            session.refresh(row)\n\n        cat = MemoryCategory(\n            id=row.id,\n            name=row.name,\n            description=row.description,\n            embedding=self._normalize_embedding(row.embedding_json),\n            summary=row.summary,\n            created_at=row.created_at,\n            updated_at=row.updated_at,\n            **self._scope_kwargs_from(row),\n        )\n        self.categories[row.id] = cat\n        return cat\n\n    def load_existing(self) -> None:\n        \"\"\"Load all existing categories from database into cache.\"\"\"\n        self.list_categories()\n\n\n__all__ = [\"SQLiteMemoryCategoryRepo\"]\n"
  },
  {
    "path": "src/memu/database/sqlite/repositories/memory_item_repo.py",
    "content": "\"\"\"SQLite memory item repository implementation.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import Mapping\nfrom typing import Any\n\nimport pendulum\nfrom sqlmodel import delete, select\n\nfrom memu.database.inmemory.vector import cosine_topk, cosine_topk_salience\nfrom memu.database.models import MemoryItem, MemoryType, compute_content_hash\nfrom memu.database.repositories.memory_item import MemoryItemRepo\nfrom memu.database.sqlite.repositories.base import SQLiteRepoBase\nfrom memu.database.sqlite.schema import SQLiteSQLAModels\nfrom memu.database.sqlite.session import SQLiteSessionManager\nfrom memu.database.state import DatabaseState\n\nlogger = logging.getLogger(__name__)\n\n\nclass SQLiteMemoryItemRepo(SQLiteRepoBase, MemoryItemRepo):\n    \"\"\"SQLite implementation of memory item repository.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        memory_item_model: type[Any],\n        sqla_models: SQLiteSQLAModels,\n        sessions: SQLiteSessionManager,\n        scope_fields: list[str],\n    ) -> None:\n        \"\"\"Initialize memory item repository.\n\n        Args:\n            state: Shared database state for caching.\n            memory_item_model: SQLModel class for memory items.\n            sqla_models: SQLAlchemy model container.\n            sessions: Session manager for database connections.\n            scope_fields: List of user scope field names.\n        \"\"\"\n        super().__init__(\n            state=state,\n            sqla_models=sqla_models,\n            sessions=sessions,\n            scope_fields=scope_fields,\n        )\n        self._memory_item_model = memory_item_model\n        self.items = self._state.items\n\n    def get_item(self, item_id: str) -> MemoryItem | None:\n        \"\"\"Get a memory item by ID.\n\n        Args:\n            item_id: The item ID to look up.\n\n        Returns:\n            MemoryItem if found, None otherwise.\n        \"\"\"\n        # Check cache first\n        if item_id in self.items:\n            return self.items[item_id]\n\n        with self._sessions.session() as session:\n            stmt = select(self._memory_item_model).where(self._memory_item_model.id == item_id)\n            row = session.exec(stmt).first()\n\n        if row is None:\n            return None\n\n        item = MemoryItem(\n            id=row.id,\n            resource_id=row.resource_id,\n            memory_type=row.memory_type,\n            summary=row.summary,\n            embedding=self._normalize_embedding(row.embedding_json),\n            created_at=row.created_at,\n            updated_at=row.updated_at,\n            **self._scope_kwargs_from(row),\n        )\n        self.items[row.id] = item\n        return item\n\n    def list_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]:\n        \"\"\"List memory items matching the where clause.\n\n        Args:\n            where: Optional filter conditions.\n\n        Returns:\n            Dictionary of item ID to MemoryItem mapping.\n        \"\"\"\n        with self._sessions.session() as session:\n            stmt = select(self._memory_item_model)\n            filters = self._build_filters(self._memory_item_model, where)\n            if filters:\n                stmt = stmt.where(*filters)\n            rows = session.exec(stmt).all()\n\n        result: dict[str, MemoryItem] = {}\n        for row in rows:\n            item = MemoryItem(\n                id=row.id,\n                resource_id=row.resource_id,\n                memory_type=row.memory_type,\n                summary=row.summary,\n                embedding=self._normalize_embedding(row.embedding_json),\n                created_at=row.created_at,\n                updated_at=row.updated_at,\n                **self._scope_kwargs_from(row),\n            )\n            result[row.id] = item\n            self.items[row.id] = item\n\n        return result\n\n    def list_items_by_ref_ids(\n        self, ref_ids: list[str], where: Mapping[str, Any] | None = None\n    ) -> dict[str, MemoryItem]:\n        \"\"\"List items by their ref_id in the extra column.\n\n        Args:\n            ref_ids: List of ref_ids to query.\n            where: Additional filter conditions.\n\n        Returns:\n            Dict mapping item_id -> MemoryItem for items whose extra.ref_id is in ref_ids.\n        \"\"\"\n        if not ref_ids:\n            return {}\n\n        from sqlalchemy import func\n\n        with self._sessions.session() as session:\n            stmt = select(self._memory_item_model)\n            filters = self._build_filters(self._memory_item_model, where)\n            # Add filter for json_extract(extra, '$.ref_id') IN ref_ids (only rows with ref_id key)\n            ref_id_col = func.json_extract(self._memory_item_model.extra, \"$.ref_id\")\n            filters.append(ref_id_col.isnot(None))\n            filters.append(ref_id_col.in_(ref_ids))\n            if filters:\n                stmt = stmt.where(*filters)\n            rows = session.exec(stmt).all()\n\n        result: dict[str, MemoryItem] = {}\n        for row in rows:\n            item = MemoryItem(\n                id=row.id,\n                resource_id=row.resource_id,\n                memory_type=row.memory_type,\n                summary=row.summary,\n                embedding=self._normalize_embedding(row.embedding_json),\n                created_at=row.created_at,\n                updated_at=row.updated_at,\n                **self._scope_kwargs_from(row),\n            )\n            result[row.id] = item\n            self.items[row.id] = item\n\n        return result\n\n    def clear_items(self, where: Mapping[str, Any] | None = None) -> dict[str, MemoryItem]:\n        \"\"\"Clear items matching the where clause.\n\n        Args:\n            where: Optional filter conditions.\n\n        Returns:\n            Dictionary of deleted item ID to MemoryItem mapping.\n        \"\"\"\n        filters = self._build_filters(self._memory_item_model, where)\n        with self._sessions.session() as session:\n            # First get the objects to delete\n            stmt = select(self._memory_item_model)\n            if filters:\n                stmt = stmt.where(*filters)\n            rows = session.exec(stmt).all()\n\n            deleted: dict[str, MemoryItem] = {}\n            for row in rows:\n                item = MemoryItem(\n                    id=row.id,\n                    resource_id=row.resource_id,\n                    memory_type=row.memory_type,\n                    summary=row.summary,\n                    embedding=self._normalize_embedding(row.embedding_json),\n                    created_at=row.created_at,\n                    updated_at=row.updated_at,\n                    **self._scope_kwargs_from(row),\n                )\n                deleted[row.id] = item\n\n            if not deleted:\n                return {}\n\n            # Delete from database\n            del_stmt = delete(self._memory_item_model)\n            if filters:\n                del_stmt = del_stmt.where(*filters)\n            session.exec(del_stmt)\n            session.commit()\n\n            # Clean up cache\n            for item_id in deleted:\n                self.items.pop(item_id, None)\n\n        return deleted\n\n    def create_item(\n        self,\n        *,\n        resource_id: str,\n        memory_type: MemoryType,\n        summary: str,\n        embedding: list[float],\n        user_data: dict[str, Any],\n        reinforce: bool = False,\n        tool_record: dict[str, Any] | None = None,\n    ) -> MemoryItem:\n        \"\"\"Create a new memory item.\n\n        Args:\n            resource_id: Associated resource ID.\n            memory_type: Type of memory.\n            summary: Memory summary text.\n            embedding: Embedding vector.\n            user_data: User scope data.\n            reinforce: If True, reinforce existing item instead of creating duplicate.\n            tool_record: Tool-related fields (when_to_use, metadata, tool_calls) to store in extra.\n\n        Returns:\n            Created MemoryItem object.\n        \"\"\"\n        if reinforce and memory_type != \"tool\":\n            return self.create_item_reinforce(\n                resource_id=resource_id,\n                memory_type=memory_type,\n                summary=summary,\n                embedding=embedding,\n                user_data=user_data,\n            )\n\n        # Build extra dict with tool_record fields at top level\n        extra: dict[str, Any] = {}\n        if tool_record:\n            if tool_record.get(\"when_to_use\") is not None:\n                extra[\"when_to_use\"] = tool_record[\"when_to_use\"]\n            if tool_record.get(\"metadata\") is not None:\n                extra[\"metadata\"] = tool_record[\"metadata\"]\n            if tool_record.get(\"tool_calls\") is not None:\n                extra[\"tool_calls\"] = tool_record[\"tool_calls\"]\n\n        now = self._now()\n        row = self._memory_item_model(\n            resource_id=resource_id,\n            memory_type=memory_type,\n            summary=summary,\n            embedding_json=self._prepare_embedding(embedding),\n            extra=extra if extra else {},\n            created_at=now,\n            updated_at=now,\n            **user_data,\n        )\n        with self._sessions.session() as session:\n            session.add(row)\n            session.commit()\n            session.refresh(row)\n\n        item = MemoryItem(\n            id=row.id,\n            resource_id=row.resource_id,\n            memory_type=row.memory_type,\n            summary=row.summary,\n            embedding=embedding,\n            extra=row.extra,\n            created_at=row.created_at,\n            updated_at=row.updated_at,\n            **user_data,\n        )\n        self.items[row.id] = item\n        return item\n\n    def create_item_reinforce(\n        self,\n        *,\n        resource_id: str,\n        memory_type: MemoryType,\n        summary: str,\n        embedding: list[float],\n        user_data: dict[str, Any],\n    ) -> MemoryItem:\n        \"\"\"Create or reinforce a memory item with deduplication.\n\n        If an item with the same content hash exists in the same scope,\n        reinforce it instead of creating a duplicate.\n\n        Args:\n            resource_id: Associated resource ID.\n            memory_type: Type of memory.\n            summary: Memory summary text.\n            embedding: Embedding vector.\n            user_data: User scope data.\n\n        Returns:\n            Created or reinforced MemoryItem object.\n        \"\"\"\n        from sqlalchemy import func\n\n        content_hash = compute_content_hash(summary, memory_type)\n\n        with self._sessions.session() as session:\n            # Check for existing item with same hash in same scope (deduplication)\n            # Use json_extract(extra, '$.content_hash') for query\n            content_hash_col = func.json_extract(self._memory_item_model.extra, \"$.content_hash\")\n            filters = [content_hash_col == content_hash]\n            filters.extend(self._build_filters(self._memory_item_model, user_data))\n\n            existing = session.exec(select(self._memory_item_model).where(*filters)).first()\n\n            if existing:\n                # Reinforce existing memory instead of creating duplicate\n                current_extra = existing.extra or {}\n                current_count = current_extra.get(\"reinforcement_count\", 1)\n                existing.extra = {\n                    **current_extra,\n                    \"reinforcement_count\": current_count + 1,\n                    \"last_reinforced_at\": self._now().isoformat(),\n                }\n                existing.updated_at = self._now()\n                session.add(existing)\n                session.commit()\n                session.refresh(existing)\n\n                item = MemoryItem(\n                    id=existing.id,\n                    resource_id=existing.resource_id,\n                    memory_type=existing.memory_type,\n                    summary=existing.summary,\n                    embedding=self._normalize_embedding(existing.embedding_json),\n                    created_at=existing.created_at,\n                    updated_at=existing.updated_at,\n                    extra=existing.extra,\n                    **self._scope_kwargs_from(existing),\n                )\n                self.items[existing.id] = item\n                return item\n\n            # Create new item with salience tracking in extra\n            now = self._now()\n            item_extra = user_data.pop(\"extra\", {}) if \"extra\" in user_data else {}\n            item_extra.update({\n                \"content_hash\": content_hash,\n                \"reinforcement_count\": 1,\n                \"last_reinforced_at\": now.isoformat(),\n            })\n\n            row = self._memory_item_model(\n                resource_id=resource_id,\n                memory_type=memory_type,\n                summary=summary,\n                embedding_json=self._prepare_embedding(embedding),\n                extra=item_extra,\n                created_at=now,\n                updated_at=now,\n                **user_data,\n            )\n\n            session.add(row)\n            session.commit()\n            session.refresh(row)\n\n        item = MemoryItem(\n            id=row.id,\n            resource_id=row.resource_id,\n            memory_type=row.memory_type,\n            summary=row.summary,\n            embedding=embedding,\n            created_at=row.created_at,\n            updated_at=row.updated_at,\n            extra=row.extra,\n            **self._scope_kwargs_from(row),\n        )\n        self.items[row.id] = item\n        return item\n\n    def update_item(\n        self,\n        *,\n        item_id: str,\n        memory_type: MemoryType | None = None,\n        summary: str | None = None,\n        embedding: list[float] | None = None,\n        extra: dict[str, Any] | None = None,\n        tool_record: dict[str, Any] | None = None,\n    ) -> MemoryItem:\n        \"\"\"Update an existing memory item.\n\n        Args:\n            item_id: ID of item to update.\n            memory_type: New memory type (optional).\n            summary: New summary text (optional).\n            embedding: New embedding vector (optional).\n            extra: Extra data to merge into existing extra dict (optional).\n            tool_record: Tool-related fields (when_to_use, metadata, tool_calls) to merge into extra.\n\n        Returns:\n            Updated MemoryItem object.\n\n        Raises:\n            KeyError: If item not found.\n        \"\"\"\n        with self._sessions.session() as session:\n            stmt = select(self._memory_item_model).where(self._memory_item_model.id == item_id)\n            row = session.exec(stmt).first()\n\n            if row is None:\n                msg = f\"Item with id {item_id} not found\"\n                raise KeyError(msg)\n\n            if memory_type is not None:\n                row.memory_type = memory_type\n            if summary is not None:\n                row.summary = summary\n            if embedding is not None:\n                row.embedding_json = self._prepare_embedding(embedding)\n\n            # Merge extra and tool_record into existing extra dict\n            current_extra = row.extra or {}\n            if extra is not None:\n                current_extra = {**current_extra, **extra}\n            if tool_record is not None:\n                # Merge tool_record fields at top level\n                for key in (\"when_to_use\", \"metadata\", \"tool_calls\"):\n                    if tool_record.get(key) is not None:\n                        current_extra[key] = tool_record[key]\n            if extra is not None or tool_record is not None:\n                row.extra = current_extra\n\n            row.updated_at = self._now()\n\n            session.add(row)\n            session.commit()\n            session.refresh(row)\n\n        item = MemoryItem(\n            id=row.id,\n            resource_id=row.resource_id,\n            memory_type=row.memory_type,\n            summary=row.summary,\n            embedding=self._normalize_embedding(row.embedding_json),\n            extra=row.extra,\n            created_at=row.created_at,\n            updated_at=row.updated_at,\n            **self._scope_kwargs_from(row),\n        )\n        self.items[row.id] = item\n        return item\n\n    def delete_item(self, item_id: str) -> None:\n        \"\"\"Delete a memory item.\n\n        Args:\n            item_id: ID of item to delete.\n        \"\"\"\n        with self._sessions.session() as session:\n            stmt = select(self._memory_item_model).where(self._memory_item_model.id == item_id)\n            row = session.exec(stmt).first()\n            if row:\n                session.delete(row)\n                session.commit()\n\n        if item_id in self.items:\n            del self.items[item_id]\n\n    def vector_search_items(\n        self,\n        query_vec: list[float],\n        top_k: int,\n        where: Mapping[str, Any] | None = None,\n        *,\n        ranking: str = \"similarity\",\n        recency_decay_days: float = 30.0,\n    ) -> list[tuple[str, float]]:\n        \"\"\"Perform vector similarity search on memory items.\n\n        Uses brute-force cosine similarity since SQLite doesn't have native vector support.\n\n        Args:\n            query_vec: Query embedding vector.\n            top_k: Maximum number of results to return.\n            where: Optional filter conditions.\n            ranking: Ranking strategy - \"similarity\" (default) or \"salience\".\n            recency_decay_days: Half-life for recency decay in salience ranking.\n\n        Returns:\n            List of (item_id, similarity_score) tuples.\n        \"\"\"\n        # Load items from database with filters\n        pool = self.list_items(where)\n\n        if ranking == \"salience\":\n            # Salience-aware ranking: similarity x reinforcement x recency\n            # Read values from extra dict\n            corpus = [\n                (\n                    i.id,\n                    i.embedding,\n                    (i.extra or {}).get(\"reinforcement_count\", 1),\n                    self._parse_datetime((i.extra or {}).get(\"last_reinforced_at\")),\n                )\n                for i in pool.values()\n            ]\n            return cosine_topk_salience(query_vec, corpus, k=top_k, recency_decay_days=recency_decay_days)\n\n        # Default: pure cosine similarity (backward compatible)\n        hits = cosine_topk(query_vec, [(i.id, i.embedding) for i in pool.values()], k=top_k)\n        return hits\n\n    @staticmethod\n    def _parse_datetime(dt_str: str | None) -> pendulum.DateTime | None:\n        \"\"\"Parse ISO datetime string from extra dict.\"\"\"\n        if dt_str is None:\n            return None\n        try:\n            parsed = pendulum.parse(dt_str)\n        except (ValueError, TypeError):\n            return None\n        else:\n            if isinstance(parsed, pendulum.DateTime):\n                return parsed\n            return None\n\n    def load_existing(self) -> None:\n        \"\"\"Load all existing items from database into cache.\"\"\"\n        self.list_items()\n\n\n__all__ = [\"SQLiteMemoryItemRepo\"]\n"
  },
  {
    "path": "src/memu/database/sqlite/repositories/resource_repo.py",
    "content": "\"\"\"SQLite resource repository implementation.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import Mapping\nfrom typing import Any\n\nfrom sqlmodel import delete, select\n\nfrom memu.database.models import Resource\nfrom memu.database.repositories.resource import ResourceRepo\nfrom memu.database.sqlite.repositories.base import SQLiteRepoBase\nfrom memu.database.sqlite.schema import SQLiteSQLAModels\nfrom memu.database.sqlite.session import SQLiteSessionManager\nfrom memu.database.state import DatabaseState\n\nlogger = logging.getLogger(__name__)\n\n\nclass SQLiteResourceRepo(SQLiteRepoBase, ResourceRepo):\n    \"\"\"SQLite implementation of resource repository.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        state: DatabaseState,\n        resource_model: type[Any],\n        sqla_models: SQLiteSQLAModels,\n        sessions: SQLiteSessionManager,\n        scope_fields: list[str],\n    ) -> None:\n        \"\"\"Initialize resource repository.\n\n        Args:\n            state: Shared database state for caching.\n            resource_model: SQLModel class for resources.\n            sqla_models: SQLAlchemy model container.\n            sessions: Session manager for database connections.\n            scope_fields: List of user scope field names.\n        \"\"\"\n        super().__init__(\n            state=state,\n            sqla_models=sqla_models,\n            sessions=sessions,\n            scope_fields=scope_fields,\n        )\n        self._resource_model = resource_model\n        self.resources = self._state.resources\n\n    def list_resources(self, where: Mapping[str, Any] | None = None) -> dict[str, Resource]:\n        \"\"\"List resources matching the where clause.\n\n        Args:\n            where: Optional filter conditions.\n\n        Returns:\n            Dictionary of resource ID to Resource mapping.\n        \"\"\"\n        # Prefer cached data if available and no filter\n        if not where and self.resources:\n            return dict(self.resources)\n\n        with self._sessions.session() as session:\n            stmt = select(self._resource_model)\n            filters = self._build_filters(self._resource_model, where)\n            if filters:\n                stmt = stmt.where(*filters)\n            rows = session.exec(stmt).all()\n\n        result: dict[str, Resource] = {}\n        for row in rows:\n            res = Resource(\n                id=row.id,\n                url=row.url,\n                modality=row.modality,\n                local_path=row.local_path,\n                caption=row.caption,\n                embedding=self._normalize_embedding(row.embedding_json),\n                created_at=row.created_at,\n                updated_at=row.updated_at,\n                **self._scope_kwargs_from(row),\n            )\n            result[row.id] = res\n            self.resources[row.id] = res\n\n        return result\n\n    def clear_resources(self, where: Mapping[str, Any] | None = None) -> dict[str, Resource]:\n        \"\"\"Clear resources matching the where clause.\n\n        Args:\n            where: Optional filter conditions.\n\n        Returns:\n            Dictionary of deleted resource ID to Resource mapping.\n        \"\"\"\n        filters = self._build_filters(self._resource_model, where)\n        with self._sessions.session() as session:\n            # First get the objects to delete\n            stmt = select(self._resource_model)\n            if filters:\n                stmt = stmt.where(*filters)\n            rows = session.exec(stmt).all()\n\n            deleted: dict[str, Resource] = {}\n            for row in rows:\n                res = Resource(\n                    id=row.id,\n                    url=row.url,\n                    modality=row.modality,\n                    local_path=row.local_path,\n                    caption=row.caption,\n                    embedding=self._normalize_embedding(row.embedding_json),\n                    created_at=row.created_at,\n                    updated_at=row.updated_at,\n                    **self._scope_kwargs_from(row),\n                )\n                deleted[row.id] = res\n\n            if not deleted:\n                return {}\n\n            # Delete from database\n            del_stmt = delete(self._resource_model)\n            if filters:\n                del_stmt = del_stmt.where(*filters)\n            session.exec(del_stmt)\n            session.commit()\n\n            # Clean up cache\n            for res_id in deleted:\n                self.resources.pop(res_id, None)\n\n        return deleted\n\n    def create_resource(\n        self,\n        *,\n        url: str,\n        modality: str,\n        local_path: str,\n        caption: str | None,\n        embedding: list[float] | None,\n        user_data: dict[str, Any],\n    ) -> Resource:\n        \"\"\"Create a new resource record.\n\n        Args:\n            url: Resource URL.\n            modality: Resource modality type.\n            local_path: Local file path.\n            caption: Optional caption text.\n            embedding: Optional embedding vector.\n            user_data: User scope data.\n\n        Returns:\n            Created Resource object.\n        \"\"\"\n        now = self._now()\n        row = self._resource_model(\n            url=url,\n            modality=modality,\n            local_path=local_path,\n            caption=caption,\n            embedding_json=self._prepare_embedding(embedding),\n            created_at=now,\n            updated_at=now,\n            **user_data,\n        )\n        with self._sessions.session() as session:\n            session.add(row)\n            session.commit()\n            session.refresh(row)\n\n        res = Resource(\n            id=row.id,\n            url=row.url,\n            modality=row.modality,\n            local_path=row.local_path,\n            caption=row.caption,\n            embedding=embedding,\n            created_at=row.created_at,\n            updated_at=row.updated_at,\n            **user_data,\n        )\n        self.resources[row.id] = res\n        return res\n\n    def load_existing(self) -> None:\n        \"\"\"Load all existing resources from database into cache.\"\"\"\n        self.list_resources()\n\n\n__all__ = [\"SQLiteResourceRepo\"]\n"
  },
  {
    "path": "src/memu/database/sqlite/schema.py",
    "content": "\"\"\"SQLAlchemy schema definitions for SQLite backend.\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom pydantic import BaseModel\nfrom sqlalchemy import MetaData\nfrom sqlmodel import SQLModel\n\nfrom memu.database.sqlite.models import (\n    SQLiteCategoryItemModel,\n    SQLiteMemoryCategoryModel,\n    SQLiteMemoryItemModel,\n    SQLiteResourceModel,\n    build_sqlite_table_model,\n)\n\n\n@dataclass\nclass SQLiteSQLAModels:\n    \"\"\"Container for SQLite SQLAlchemy/SQLModel models.\"\"\"\n\n    Base: type[Any]\n    Resource: type[Any]\n    MemoryCategory: type[Any]\n    MemoryItem: type[Any]\n    CategoryItem: type[Any]\n\n\n_MODEL_CACHE: dict[type[Any], SQLiteSQLAModels] = {}\n\n\ndef get_sqlite_sqlalchemy_models(*, scope_model: type[BaseModel] | None = None) -> SQLiteSQLAModels:\n    \"\"\"Build (and cache) SQLModel ORM models for SQLite storage.\n\n    Args:\n        scope_model: Optional Pydantic model defining user scope fields.\n\n    Returns:\n        SQLiteSQLAModels containing all table models.\n    \"\"\"\n    scope = scope_model or BaseModel\n    cache_key = scope\n    cached = _MODEL_CACHE.get(cache_key)\n    if cached:\n        return cached\n\n    metadata_obj = MetaData()\n\n    resource_model = build_sqlite_table_model(\n        scope,\n        SQLiteResourceModel,\n        tablename=\"sqlite_resources\",\n        metadata=metadata_obj,\n    )\n    memory_category_model = build_sqlite_table_model(\n        scope,\n        SQLiteMemoryCategoryModel,\n        tablename=\"sqlite_memory_categories\",\n        metadata=metadata_obj,\n    )\n    memory_item_model = build_sqlite_table_model(\n        scope,\n        SQLiteMemoryItemModel,\n        tablename=\"sqlite_memory_items\",\n        metadata=metadata_obj,\n    )\n    category_item_model = build_sqlite_table_model(\n        scope,\n        SQLiteCategoryItemModel,\n        tablename=\"sqlite_category_items\",\n        metadata=metadata_obj,\n    )\n\n    class SQLiteBase(SQLModel):\n        __abstract__ = True\n        metadata = metadata_obj\n\n    models = SQLiteSQLAModels(\n        Base=SQLiteBase,\n        Resource=resource_model,\n        MemoryCategory=memory_category_model,\n        MemoryItem=memory_item_model,\n        CategoryItem=category_item_model,\n    )\n    _MODEL_CACHE[cache_key] = models\n    return models\n\n\ndef get_sqlite_metadata(scope_model: type[BaseModel] | None = None) -> MetaData:\n    \"\"\"Get SQLAlchemy metadata for SQLite tables.\n\n    Args:\n        scope_model: Optional Pydantic model defining user scope fields.\n\n    Returns:\n        SQLAlchemy MetaData object.\n    \"\"\"\n    from typing import cast\n\n    return cast(MetaData, get_sqlite_sqlalchemy_models(scope_model=scope_model).Base.metadata)\n\n\n__all__ = [\"SQLiteSQLAModels\", \"get_sqlite_metadata\", \"get_sqlite_sqlalchemy_models\"]\n"
  },
  {
    "path": "src/memu/database/sqlite/session.py",
    "content": "\"\"\"SQLite session manager for database connections.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom sqlmodel import Session, create_engine\n\nlogger = logging.getLogger(__name__)\n\n\nclass SQLiteSessionManager:\n    \"\"\"Handle engine lifecycle and session creation for SQLite store.\"\"\"\n\n    def __init__(self, *, dsn: str, engine_kwargs: dict[str, Any] | None = None) -> None:\n        \"\"\"Initialize SQLite session manager.\n\n        Args:\n            dsn: SQLite connection string (e.g., \"sqlite:///path/to/db.sqlite\").\n            engine_kwargs: Optional keyword arguments for create_engine.\n        \"\"\"\n        kw: dict[str, Any] = {\n            \"connect_args\": {\"check_same_thread\": False},  # Allow multi-threaded access\n        }\n        if engine_kwargs:\n            kw.update(engine_kwargs)\n        self._engine = create_engine(dsn, **kw)\n\n    def session(self) -> Session:\n        \"\"\"Create a new database session.\"\"\"\n        return Session(self._engine, expire_on_commit=False)\n\n    def close(self) -> None:\n        \"\"\"Close the database engine and release resources.\"\"\"\n        try:\n            self._engine.dispose()\n        except SQLAlchemyError:\n            logger.exception(\"Failed to close SQLite engine\")\n\n    @property\n    def engine(self) -> Any:\n        \"\"\"Return the underlying SQLAlchemy engine.\"\"\"\n        return self._engine\n\n\n__all__ = [\"SQLiteSessionManager\"]\n"
  },
  {
    "path": "src/memu/database/sqlite/sqlite.py",
    "content": "\"\"\"SQLite database store implementation for MemU.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom typing import Any\n\nfrom pydantic import BaseModel\nfrom sqlmodel import SQLModel\n\nfrom memu.database.interfaces import Database\nfrom memu.database.models import CategoryItem, MemoryCategory, MemoryItem, Resource\nfrom memu.database.repositories import CategoryItemRepo, MemoryCategoryRepo, MemoryItemRepo, ResourceRepo\nfrom memu.database.sqlite.repositories.category_item_repo import SQLiteCategoryItemRepo\nfrom memu.database.sqlite.repositories.memory_category_repo import SQLiteMemoryCategoryRepo\nfrom memu.database.sqlite.repositories.memory_item_repo import SQLiteMemoryItemRepo\nfrom memu.database.sqlite.repositories.resource_repo import SQLiteResourceRepo\nfrom memu.database.sqlite.schema import SQLiteSQLAModels, get_sqlite_sqlalchemy_models\nfrom memu.database.sqlite.session import SQLiteSessionManager\nfrom memu.database.state import DatabaseState\n\nlogger = logging.getLogger(__name__)\n\n\nclass SQLiteStore(Database):\n    \"\"\"SQLite database store implementation.\n\n    This store provides a lightweight, file-based database backend for MemU.\n    It uses SQLite for metadata storage and brute-force cosine similarity\n    for vector search (native vector support is not available in SQLite).\n\n    Attributes:\n        resource_repo: Repository for resource records.\n        memory_category_repo: Repository for memory categories.\n        memory_item_repo: Repository for memory items.\n        category_item_repo: Repository for category-item relations.\n        resources: Dict cache of resource records.\n        items: Dict cache of memory item records.\n        categories: Dict cache of memory category records.\n        relations: List cache of category-item relations.\n    \"\"\"\n\n    resource_repo: ResourceRepo\n    memory_category_repo: MemoryCategoryRepo\n    memory_item_repo: MemoryItemRepo\n    category_item_repo: CategoryItemRepo\n    resources: dict[str, Resource]\n    items: dict[str, MemoryItem]\n    categories: dict[str, MemoryCategory]\n    relations: list[CategoryItem]\n\n    def __init__(\n        self,\n        *,\n        dsn: str,\n        scope_model: type[BaseModel] | None = None,\n        resource_model: type[Any] | None = None,\n        memory_category_model: type[Any] | None = None,\n        memory_item_model: type[Any] | None = None,\n        category_item_model: type[Any] | None = None,\n        sqla_models: SQLiteSQLAModels | None = None,\n    ) -> None:\n        \"\"\"Initialize SQLite database store.\n\n        Args:\n            dsn: SQLite connection string (e.g., \"sqlite:///path/to/db.sqlite\").\n            scope_model: Pydantic model defining user scope fields.\n            resource_model: Optional custom resource model.\n            memory_category_model: Optional custom memory category model.\n            memory_item_model: Optional custom memory item model.\n            category_item_model: Optional custom category-item model.\n            sqla_models: Pre-built SQLAlchemy models container.\n        \"\"\"\n        self.dsn = dsn\n        self._scope_model: type[BaseModel] = scope_model or BaseModel\n        self._scope_fields = list(getattr(self._scope_model, \"model_fields\", {}).keys())\n        self._state = DatabaseState()\n        self._sessions = SQLiteSessionManager(dsn=self.dsn)\n        self._sqla_models: SQLiteSQLAModels = sqla_models or get_sqlite_sqlalchemy_models(scope_model=self._scope_model)\n\n        # Create tables\n        self._create_tables()\n\n        # Use provided models or defaults from sqla_models\n        resource_model = resource_model or self._sqla_models.Resource\n        memory_category_model = memory_category_model or self._sqla_models.MemoryCategory\n        memory_item_model = memory_item_model or self._sqla_models.MemoryItem\n        category_item_model = category_item_model or self._sqla_models.CategoryItem\n\n        # Initialize repositories\n        self.resource_repo = SQLiteResourceRepo(\n            state=self._state,\n            resource_model=resource_model,\n            sqla_models=self._sqla_models,\n            sessions=self._sessions,\n            scope_fields=self._scope_fields,\n        )\n        self.memory_category_repo = SQLiteMemoryCategoryRepo(\n            state=self._state,\n            memory_category_model=memory_category_model,\n            sqla_models=self._sqla_models,\n            sessions=self._sessions,\n            scope_fields=self._scope_fields,\n        )\n        self.memory_item_repo = SQLiteMemoryItemRepo(\n            state=self._state,\n            memory_item_model=memory_item_model,\n            sqla_models=self._sqla_models,\n            sessions=self._sessions,\n            scope_fields=self._scope_fields,\n        )\n        self.category_item_repo = SQLiteCategoryItemRepo(\n            state=self._state,\n            category_item_model=category_item_model,\n            sqla_models=self._sqla_models,\n            sessions=self._sessions,\n            scope_fields=self._scope_fields,\n        )\n\n        # Set up cache references\n        self.resources = self._state.resources\n        self.items = self._state.items\n        self.categories = self._state.categories\n        self.relations = self._state.relations\n\n    def _create_tables(self) -> None:\n        \"\"\"Create SQLite tables if they don't exist.\"\"\"\n        SQLModel.metadata.create_all(self._sessions.engine)\n        # Also create tables from our custom metadata\n        self._sqla_models.Base.metadata.create_all(self._sessions.engine)\n        logger.debug(\"SQLite tables created/verified\")\n\n    def close(self) -> None:\n        \"\"\"Close the database connection and release resources.\"\"\"\n        self._sessions.close()\n\n    def load_existing(self) -> None:\n        \"\"\"Load all existing data from database into cache.\"\"\"\n        self.resource_repo.load_existing()\n        self.memory_category_repo.load_existing()\n        self.memory_item_repo.load_existing()\n        self.category_item_repo.load_existing()\n\n\n__all__ = [\"SQLiteStore\"]\n"
  },
  {
    "path": "src/memu/database/state.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass, field\n\nfrom memu.database.models import CategoryItem, MemoryCategory, MemoryItem, Resource\n\n\n@dataclass\nclass DatabaseState:\n    resources: dict[str, Resource] = field(default_factory=dict)\n    items: dict[str, MemoryItem] = field(default_factory=dict)\n    categories: dict[str, MemoryCategory] = field(default_factory=dict)\n    relations: list[CategoryItem] = field(default_factory=list)\n\n\n__all__ = [\"DatabaseState\"]\n"
  },
  {
    "path": "src/memu/embedding/__init__.py",
    "content": "from memu.embedding.http_client import HTTPEmbeddingClient\nfrom memu.embedding.openai_sdk import OpenAIEmbeddingSDKClient\n\n__all__ = [\"HTTPEmbeddingClient\", \"OpenAIEmbeddingSDKClient\"]\n"
  },
  {
    "path": "src/memu/embedding/backends/__init__.py",
    "content": "from memu.embedding.backends.base import EmbeddingBackend\nfrom memu.embedding.backends.doubao import DoubaoEmbeddingBackend, DoubaoMultimodalEmbeddingInput\nfrom memu.embedding.backends.openai import OpenAIEmbeddingBackend\n\n__all__ = [\n    \"DoubaoEmbeddingBackend\",\n    \"DoubaoMultimodalEmbeddingInput\",\n    \"EmbeddingBackend\",\n    \"OpenAIEmbeddingBackend\",\n]\n"
  },
  {
    "path": "src/memu/embedding/backends/base.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any\n\n\nclass EmbeddingBackend:\n    \"\"\"Defines how to talk to a specific embedding provider.\"\"\"\n\n    name: str = \"base\"\n    embedding_endpoint: str = \"/embeddings\"\n\n    def build_embedding_payload(self, *, inputs: list[str], embed_model: str) -> dict[str, Any]:\n        raise NotImplementedError\n\n    def parse_embedding_response(self, data: dict[str, Any]) -> list[list[float]]:\n        raise NotImplementedError\n"
  },
  {
    "path": "src/memu/embedding/backends/doubao.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, Literal, cast\n\nfrom memu.embedding.backends.base import EmbeddingBackend\n\n\nclass DoubaoMultimodalEmbeddingInput:\n    \"\"\"Represents a single input item for multimodal embedding.\"\"\"\n\n    def __init__(\n        self,\n        input_type: Literal[\"text\", \"image_url\", \"video_url\"],\n        content: str,\n    ):\n        self.input_type = input_type\n        self.content = content\n\n    def to_dict(self) -> dict[str, Any]:\n        if self.input_type == \"text\":\n            return {\"type\": \"text\", \"text\": self.content}\n        elif self.input_type == \"image_url\":\n            return {\"type\": \"image_url\", \"image_url\": {\"url\": self.content}}\n        elif self.input_type == \"video_url\":\n            return {\"type\": \"video_url\", \"video_url\": {\"url\": self.content}}\n        else:\n            msg = f\"Unsupported input type: {self.input_type}\"\n            raise ValueError(msg)\n\n\nclass DoubaoEmbeddingBackend(EmbeddingBackend):\n    \"\"\"Backend for Doubao embedding API (including multimodal embedding).\"\"\"\n\n    name = \"doubao\"\n    embedding_endpoint = \"/api/v3/embeddings\"\n    multimodal_embedding_endpoint = \"/api/v3/embeddings/multimodal\"\n\n    def build_embedding_payload(self, *, inputs: list[str], embed_model: str) -> dict[str, Any]:\n        \"\"\"Build payload for standard text embeddings.\"\"\"\n        return {\"model\": embed_model, \"input\": inputs, \"encoding_format\": \"float\"}\n\n    def parse_embedding_response(self, data: dict[str, Any]) -> list[list[float]]:\n        \"\"\"Parse embedding response.\"\"\"\n        return [cast(list[float], d[\"embedding\"]) for d in data[\"data\"]]\n\n    def build_multimodal_embedding_payload(\n        self,\n        *,\n        inputs: list[DoubaoMultimodalEmbeddingInput],\n        embed_model: str,\n        encoding_format: str = \"float\",\n    ) -> dict[str, Any]:\n        \"\"\"\n        Build payload for multimodal embedding API.\n\n        Args:\n            inputs: List of multimodal inputs (text, image_url, video_url)\n            embed_model: Model name (e.g., 'doubao-embedding-vision-250615')\n            encoding_format: Encoding format ('float' or 'base64')\n\n        Returns:\n            Request payload dict\n        \"\"\"\n        return {\n            \"model\": embed_model,\n            \"encoding_format\": encoding_format,\n            \"input\": [inp.to_dict() for inp in inputs],\n        }\n\n    def parse_multimodal_embedding_response(self, data: dict[str, Any]) -> list[list[float]]:\n        \"\"\"Parse multimodal embedding response.\"\"\"\n        return [cast(list[float], d[\"embedding\"]) for d in data[\"data\"]]\n"
  },
  {
    "path": "src/memu/embedding/backends/openai.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, cast\n\nfrom memu.embedding.backends.base import EmbeddingBackend\n\n\nclass OpenAIEmbeddingBackend(EmbeddingBackend):\n    \"\"\"Backend for OpenAI-compatible embedding API.\"\"\"\n\n    name = \"openai\"\n    embedding_endpoint = \"/embeddings\"\n\n    def build_embedding_payload(self, *, inputs: list[str], embed_model: str) -> dict[str, Any]:\n        return {\"model\": embed_model, \"input\": inputs}\n\n    def parse_embedding_response(self, data: dict[str, Any]) -> list[list[float]]:\n        return [cast(list[float], d[\"embedding\"]) for d in data[\"data\"]]\n"
  },
  {
    "path": "src/memu/embedding/http_client.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\nfrom collections.abc import Callable\nfrom typing import Literal\n\nimport httpx\n\nfrom memu.embedding.backends.base import EmbeddingBackend\nfrom memu.embedding.backends.doubao import DoubaoEmbeddingBackend, DoubaoMultimodalEmbeddingInput\nfrom memu.embedding.backends.openai import OpenAIEmbeddingBackend\n\n\ndef _load_proxy() -> str | None:\n    return os.getenv(\"MEMU_HTTP_PROXY\") or os.getenv(\"HTTP_PROXY\") or os.getenv(\"HTTPS_PROXY\") or None\n\n\nlogger = logging.getLogger(__name__)\n\nEMBEDDING_BACKENDS: dict[str, Callable[[], EmbeddingBackend]] = {\n    OpenAIEmbeddingBackend.name: OpenAIEmbeddingBackend,\n    DoubaoEmbeddingBackend.name: DoubaoEmbeddingBackend,\n}\n\n\nclass HTTPEmbeddingClient:\n    \"\"\"HTTP client for embedding APIs.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        base_url: str,\n        api_key: str,\n        embed_model: str,\n        provider: str = \"openai\",\n        endpoint_overrides: dict[str, str] | None = None,\n        timeout: int = 60,\n    ):\n        # Ensure base_url ends with \"/\" so httpx doesn't discard the path\n        # component when joining with endpoint paths.\n        # See: https://github.com/NevaMind-AI/memU/issues/328\n        self.base_url = base_url.rstrip(\"/\") + \"/\"\n        self.api_key = api_key or \"\"\n        self.embed_model = embed_model\n        self.provider = provider.lower()\n        self.backend = self._load_backend(self.provider)\n        overrides = endpoint_overrides or {}\n        raw_embedding_ep = (\n            overrides.get(\"embeddings\")\n            or overrides.get(\"embedding\")\n            or overrides.get(\"embed\")\n            or self.backend.embedding_endpoint\n        )\n        # Strip leading \"/\" so httpx resolves relative to base_url\n        self.embedding_endpoint = raw_embedding_ep.lstrip(\"/\")\n        self.timeout = timeout\n        self.proxy = _load_proxy()\n\n    async def embed(self, inputs: list[str]) -> list[list[float]]:\n        \"\"\"\n        Create text embeddings.\n\n        Args:\n            inputs: List of text strings to embed\n\n        Returns:\n            List of embedding vectors\n        \"\"\"\n        payload = self.backend.build_embedding_payload(inputs=inputs, embed_model=self.embed_model)\n        async with httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout, proxy=self.proxy) as client:\n            resp = await client.post(self.embedding_endpoint, json=payload, headers=self._headers())\n            resp.raise_for_status()\n            data = resp.json()\n        logger.debug(\"HTTP embedding response: %s\", data)\n        return self.backend.parse_embedding_response(data)\n\n    async def embed_multimodal(\n        self,\n        inputs: list[tuple[Literal[\"text\", \"image_url\", \"video_url\"], str]],\n        *,\n        encoding_format: str = \"float\",\n    ) -> list[list[float]]:\n        \"\"\"\n        Create multimodal embeddings using Doubao embedding vision API.\n\n        This method supports embedding a mix of text, images, and videos in a single request.\n        Only available when using the 'doubao' provider.\n\n        Args:\n            inputs: List of tuples where each tuple contains:\n                - input_type: One of 'text', 'image_url', or 'video_url'\n                - content: The text content or URL to the image/video\n            encoding_format: Encoding format for the embeddings ('float' or 'base64')\n\n        Returns:\n            List of embedding vectors\n\n        Example:\n            >>> client = HTTPEmbeddingClient(\n            ...     base_url=\"https://ark.cn-beijing.volces.com\",\n            ...     api_key=\"your-api-key\",\n            ...     embed_model=\"doubao-embedding-vision-250615\",\n            ...     provider=\"doubao\",\n            ... )\n            >>> embeddings = await client.embed_multimodal([\n            ...     (\"text\", \"What is in the image and video?\"),\n            ...     (\"image_url\", \"https://example.com/image.png\"),\n            ...     (\"video_url\", \"https://example.com/video.mp4\"),\n            ... ])\n\n        Raises:\n            TypeError: If the backend does not support multimodal embeddings\n        \"\"\"\n        if not isinstance(self.backend, DoubaoEmbeddingBackend):\n            msg = (\n                f\"Multimodal embedding is only supported by 'doubao' provider, \"\n                f\"but current provider is '{self.provider}'\"\n            )\n            raise TypeError(msg)\n\n        multimodal_inputs = [\n            DoubaoMultimodalEmbeddingInput(input_type=input_type, content=content) for input_type, content in inputs\n        ]\n\n        payload = self.backend.build_multimodal_embedding_payload(\n            inputs=multimodal_inputs,\n            embed_model=self.embed_model,\n            encoding_format=encoding_format,\n        )\n\n        endpoint = self.backend.multimodal_embedding_endpoint.lstrip(\"/\")\n        async with httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout, proxy=self.proxy) as client:\n            resp = await client.post(endpoint, json=payload, headers=self._headers())\n            resp.raise_for_status()\n            data = resp.json()\n\n        logger.debug(\"HTTP multimodal embedding response: %s\", data)\n        return self.backend.parse_multimodal_embedding_response(data)\n\n    def _headers(self) -> dict[str, str]:\n        return {\"Authorization\": f\"Bearer {self.api_key}\"}\n\n    def _load_backend(self, provider: str) -> EmbeddingBackend:\n        factory = EMBEDDING_BACKENDS.get(provider)\n        if not factory:\n            msg = f\"Unsupported embedding provider '{provider}'. Available: {', '.join(EMBEDDING_BACKENDS.keys())}\"\n            raise ValueError(msg)\n        return factory()\n"
  },
  {
    "path": "src/memu/embedding/openai_sdk.py",
    "content": "import logging\nfrom typing import cast\n\nfrom openai import AsyncOpenAI\n\nlogger = logging.getLogger(__name__)\n\n\nclass OpenAIEmbeddingSDKClient:\n    \"\"\"OpenAI embedding client that relies on the official Python SDK.\"\"\"\n\n    def __init__(self, *, base_url: str, api_key: str, embed_model: str, batch_size: int = 25):\n        self.base_url = base_url.rstrip(\"/\")\n        self.api_key = api_key or \"\"\n        self.embed_model = embed_model\n        self.batch_size = batch_size\n        self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)\n\n    async def embed(self, inputs: list[str]) -> list[list[float]]:\n        \"\"\"\n        Create text embeddings.\n\n        Args:\n            inputs: List of text strings to embed\n\n        Returns:\n            List of embedding vectors\n        \"\"\"\n        # Process in batches to handle API limits (e.g., some providers limit batch size)\n        if len(inputs) <= self.batch_size:\n            # Single batch - direct call\n            response = await self.client.embeddings.create(model=self.embed_model, input=inputs)\n            return [cast(list[float], d.embedding) for d in response.data]\n\n        # Multiple batches - split and merge\n        all_embeddings = []\n        for i in range(0, len(inputs), self.batch_size):\n            batch = inputs[i : i + self.batch_size]\n            response = await self.client.embeddings.create(model=self.embed_model, input=batch)\n            batch_embeddings = [cast(list[float], d.embedding) for d in response.data]\n            all_embeddings.extend(batch_embeddings)\n\n        return all_embeddings\n"
  },
  {
    "path": "src/memu/integrations/__init__.py",
    "content": "from .langgraph import MemULangGraphTools\n\n__all__ = [\"MemULangGraphTools\"]\n"
  },
  {
    "path": "src/memu/integrations/langgraph.py",
    "content": "\"\"\"LangGraph integration for MemU.\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport logging\nimport os\nimport tempfile\nimport uuid\nfrom typing import Any\n\n# MUST explicitly import langgraph to satisfy DEP002\nimport langgraph\nfrom pydantic import BaseModel, Field\n\nfrom memu.app.service import MemoryService\n\ntry:\n    from langchain_core.tools import BaseTool, StructuredTool\nexcept ImportError as e:\n    msg = \"Please install 'langchain-core' (and 'langgraph') to use the LangGraph integration.\"\n    raise ImportError(msg) from e\n\n\n# Setup logger\nlogger = logging.getLogger(\"memu.integrations.langgraph\")\n\n\nclass MemUIntegrationError(Exception):\n    \"\"\"Base exception for MemU integration issues.\"\"\"\n\n\nclass SaveRecallInput(BaseModel):\n    \"\"\"Input schema for the save_memory tool.\"\"\"\n\n    content: str = Field(description=\"The text content or information to save/remember.\")\n    user_id: str = Field(description=\"The unique identifier of the user.\")\n    metadata: dict[str, Any] | None = Field(default=None, description=\"Additional metadata related to the memory.\")\n\n\nclass SearchRecallInput(BaseModel):\n    \"\"\"Input schema for the search_memory tool.\"\"\"\n\n    query: str = Field(description=\"The search query to retrieve relevant memories.\")\n    user_id: str = Field(description=\"The unique identifier of the user.\")\n    limit: int = Field(default=5, description=\"Number of memories to retrieve.\")\n    metadata_filter: dict[str, Any] | None = Field(\n        default=None, description=\"Optional filter for memory metadata (e.g., {'category': 'work'}).\"\n    )\n    min_relevance_score: float = Field(default=0.0, description=\"Minimum relevance score (0.0 to 1.0) for results.\")\n\n\nclass MemULangGraphTools:\n    \"\"\"Adapter to expose MemU as a set of Tools for LangGraph/LangChain agents.\n\n    This class provides a bridge between the MemU MemoryService and LangChain's\n    tooling ecosystem.\n    \"\"\"\n\n    def __init__(self, memory_service: MemoryService):\n        \"\"\"Initializes the MemULangGraphTools with a memory service.\"\"\"\n        self.memory_service = memory_service\n        # Expose the langgraph module to ensure it's \"used\" even if just by reference in this class\n        self._graph_backend = langgraph\n\n    def tools(self) -> list[BaseTool]:\n        \"\"\"Return a list of tools compatible with LangGraph.\"\"\"\n        return [\n            self.save_memory_tool(),\n            self.search_memory_tool(),\n        ]\n\n    def save_memory_tool(self) -> StructuredTool:\n        \"\"\"Creates a tool to save information into MemU.\"\"\"\n\n        async def _save(content: str, user_id: str, metadata: dict | None = None) -> str:\n            logger.info(\"Entering save_memory_tool for user_id: %s\", user_id)\n            filename = f\"memu_input_{uuid.uuid4()}.txt\"\n            temp_dir = tempfile.gettempdir()\n            file_path = os.path.join(temp_dir, filename)\n\n            try:\n                with open(file_path, \"w\", encoding=\"utf-8\") as f:\n                    f.write(content)\n\n                logger.debug(\"Calling memory_service.memorize with temporary file: %s\", file_path)\n                await self.memory_service.memorize(\n                    resource_url=file_path,\n                    modality=\"conversation\",\n                    user={\"user_id\": user_id, **(metadata or {})},\n                )\n                logger.info(\"Successfully saved memory for user_id: %s\", user_id)\n            except Exception as e:\n                error_msg = f\"Failed to save memory for user {user_id}: {e!s}\"\n                logger.exception(error_msg)\n                return str(MemUIntegrationError(error_msg))\n            finally:\n                if os.path.exists(file_path):\n                    with contextlib.suppress(OSError):\n                        os.remove(file_path)\n                        logger.debug(\"Cleaned up temporary file: %s\", file_path)\n\n            return \"Memory saved successfully.\"\n\n        return StructuredTool.from_function(\n            func=None,\n            coroutine=_save,\n            name=\"save_memory\",\n            description=\"Save a piece of information, conversation snippet, or memory for a user.\",\n            args_schema=SaveRecallInput,\n        )\n\n    def search_memory_tool(self) -> StructuredTool:\n        \"\"\"Creates a tool to search for information in MemU.\"\"\"\n\n        async def _search(\n            query: str,\n            user_id: str,\n            limit: int = 5,\n            metadata_filter: dict | None = None,\n            min_relevance_score: float = 0.0,\n        ) -> str:\n            logger.info(\"Entering search_memory_tool for user_id: %s, query: '%s'\", user_id, query)\n            try:\n                queries = [{\"role\": \"user\", \"content\": query}]\n                where_filter = {\"user_id\": user_id}\n                if metadata_filter:\n                    where_filter.update(metadata_filter)\n\n                logger.debug(\"Calling memory_service.retrieve with where_filter: %s\", where_filter)\n                result = await self.memory_service.retrieve(\n                    queries=queries,\n                    where=where_filter,\n                )\n                logger.info(\"Successfully retrieved memories for user_id: %s\", user_id)\n            except Exception as e:\n                error_msg = f\"Failed to search memory for user {user_id}: {e!s}\"\n                logger.exception(error_msg)\n                return str(MemUIntegrationError(error_msg))\n\n            items = result.get(\"items\", [])\n            if min_relevance_score > 0:\n                items = [item for item in items if item.get(\"score\", 1.0) >= min_relevance_score]\n\n            if not items:\n                logger.info(\"No memories found for user_id: %s\", user_id)\n                return \"No relevant memories found.\"\n\n            response_text = \"Retrieved Memories:\\n\"\n            for idx, item in enumerate(items[:limit]):\n                summary = item.get(\"summary\", \"\")\n                score = item.get(\"score\", \"N/A\")\n                response_text += f\"{idx + 1}. [Score: {score}] {summary}\\n\"\n\n            return response_text\n\n        return StructuredTool.from_function(\n            func=None,\n            coroutine=_search,\n            name=\"search_memory\",\n            description=\"Search for relevant memories or information for a user based on a query.\",\n            args_schema=SearchRecallInput,\n        )\n"
  },
  {
    "path": "src/memu/llm/backends/__init__.py",
    "content": "from memu.llm.backends.base import LLMBackend\nfrom memu.llm.backends.doubao import DoubaoLLMBackend\nfrom memu.llm.backends.grok import GrokBackend\nfrom memu.llm.backends.openai import OpenAILLMBackend\nfrom memu.llm.backends.openrouter import OpenRouterLLMBackend\n\n__all__ = [\"DoubaoLLMBackend\", \"GrokBackend\", \"LLMBackend\", \"OpenAILLMBackend\", \"OpenRouterLLMBackend\"]\n"
  },
  {
    "path": "src/memu/llm/backends/base.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any\n\n\nclass LLMBackend:\n    \"\"\"Defines how to talk to a specific HTTP LLM provider.\"\"\"\n\n    name: str = \"base\"\n    summary_endpoint: str = \"/chat/completions\"\n\n    def build_summary_payload(\n        self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int | None\n    ) -> dict[str, Any]:\n        raise NotImplementedError\n\n    def parse_summary_response(self, data: dict[str, Any]) -> str:\n        raise NotImplementedError\n\n    def build_vision_payload(\n        self,\n        *,\n        prompt: str,\n        base64_image: str,\n        mime_type: str,\n        system_prompt: str | None,\n        chat_model: str,\n        max_tokens: int | None,\n    ) -> dict[str, Any]:\n        raise NotImplementedError\n"
  },
  {
    "path": "src/memu/llm/backends/doubao.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, cast\n\nfrom memu.llm.backends.base import LLMBackend\n\n\nclass DoubaoLLMBackend(LLMBackend):\n    \"\"\"Backend for Doubao LLM API.\"\"\"\n\n    name = \"doubao\"\n    summary_endpoint = \"/api/v3/chat/completions\"\n\n    def build_summary_payload(\n        self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int | None\n    ) -> dict[str, Any]:\n        \"\"\"Build payload for Doubao chat completions (OpenAI-compatible).\"\"\"\n        prompt = system_prompt or \"Summarize the text in one short paragraph.\"\n        payload: dict[str, Any] = {\n            \"model\": chat_model,\n            \"messages\": [\n                {\"role\": \"system\", \"content\": prompt},\n                {\"role\": \"user\", \"content\": text},\n            ],\n            \"temperature\": 0.2,\n        }\n        if max_tokens is not None:\n            payload[\"max_tokens\"] = max_tokens\n        return payload\n\n    def parse_summary_response(self, data: dict[str, Any]) -> str:\n        return cast(str, data[\"choices\"][0][\"message\"][\"content\"])\n\n    def build_vision_payload(\n        self,\n        *,\n        prompt: str,\n        base64_image: str,\n        mime_type: str,\n        system_prompt: str | None,\n        chat_model: str,\n        max_tokens: int | None,\n    ) -> dict[str, Any]:\n        \"\"\"Build payload for Doubao Vision API (OpenAI-compatible).\"\"\"\n        messages: list[dict[str, Any]] = []\n        if system_prompt:\n            messages.append({\"role\": \"system\", \"content\": system_prompt})\n\n        messages.append({\n            \"role\": \"user\",\n            \"content\": [\n                {\"type\": \"text\", \"text\": prompt},\n                {\n                    \"type\": \"image_url\",\n                    \"image_url\": {\n                        \"url\": f\"data:{mime_type};base64,{base64_image}\",\n                    },\n                },\n            ],\n        })\n\n        payload: dict[str, Any] = {\n            \"model\": chat_model,\n            \"messages\": messages,\n            \"temperature\": 0.2,\n        }\n        if max_tokens is not None:\n            payload[\"max_tokens\"] = max_tokens\n        return payload\n"
  },
  {
    "path": "src/memu/llm/backends/grok.py",
    "content": "from __future__ import annotations\n\nfrom memu.llm.backends.openai import OpenAILLMBackend\n\n\nclass GrokBackend(OpenAILLMBackend):\n    \"\"\"Backend for Grok (xAI) LLM API.\"\"\"\n\n    name = \"grok\"\n    # Grok uses the same payload structure as OpenAI\n    # We inherits build_summary_payload, parse_summary_response, etc.\n"
  },
  {
    "path": "src/memu/llm/backends/openai.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, cast\n\nfrom memu.llm.backends.base import LLMBackend\n\n\nclass OpenAILLMBackend(LLMBackend):\n    \"\"\"Backend for OpenAI-compatible LLM API.\"\"\"\n\n    name = \"openai\"\n    summary_endpoint = \"/chat/completions\"\n\n    def build_summary_payload(\n        self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int | None\n    ) -> dict[str, Any]:\n        prompt = system_prompt or \"Summarize the text in one short paragraph.\"\n        return {\n            \"model\": chat_model,\n            \"messages\": [\n                {\"role\": \"system\", \"content\": prompt},\n                {\"role\": \"user\", \"content\": text},\n            ],\n            \"temperature\": 0.2,\n            \"max_tokens\": max_tokens,\n        }\n\n    def parse_summary_response(self, data: dict[str, Any]) -> str:\n        return cast(str, data[\"choices\"][0][\"message\"][\"content\"])\n\n    def build_vision_payload(\n        self,\n        *,\n        prompt: str,\n        base64_image: str,\n        mime_type: str,\n        system_prompt: str | None,\n        chat_model: str,\n        max_tokens: int | None,\n    ) -> dict[str, Any]:\n        \"\"\"Build payload for OpenAI Vision API.\"\"\"\n        messages: list[dict[str, Any]] = []\n        if system_prompt:\n            messages.append({\"role\": \"system\", \"content\": system_prompt})\n\n        messages.append({\n            \"role\": \"user\",\n            \"content\": [\n                {\"type\": \"text\", \"text\": prompt},\n                {\n                    \"type\": \"image_url\",\n                    \"image_url\": {\n                        \"url\": f\"data:{mime_type};base64,{base64_image}\",\n                    },\n                },\n            ],\n        })\n\n        return {\n            \"model\": chat_model,\n            \"messages\": messages,\n            \"temperature\": 0.2,\n            \"max_tokens\": max_tokens,\n        }\n"
  },
  {
    "path": "src/memu/llm/backends/openrouter.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, cast\n\nfrom memu.llm.backends.base import LLMBackend\n\n\nclass OpenRouterLLMBackend(LLMBackend):\n    \"\"\"Backend for OpenRouter LLM API (OpenAI-compatible).\"\"\"\n\n    name = \"openrouter\"\n    summary_endpoint = \"/api/v1/chat/completions\"\n\n    def build_summary_payload(\n        self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int | None\n    ) -> dict[str, Any]:\n        \"\"\"Build payload for OpenRouter chat completions (OpenAI-compatible).\"\"\"\n        prompt = system_prompt or \"Summarize the text in one short paragraph.\"\n        payload: dict[str, Any] = {\n            \"model\": chat_model,\n            \"messages\": [\n                {\"role\": \"system\", \"content\": prompt},\n                {\"role\": \"user\", \"content\": text},\n            ],\n            \"temperature\": 0.2,\n        }\n        if max_tokens is not None:\n            payload[\"max_tokens\"] = max_tokens\n        return payload\n\n    def parse_summary_response(self, data: dict[str, Any]) -> str:\n        \"\"\"Parse OpenRouter response (OpenAI-compatible format).\"\"\"\n        return cast(str, data[\"choices\"][0][\"message\"][\"content\"])\n\n    def build_vision_payload(\n        self,\n        *,\n        prompt: str,\n        base64_image: str,\n        mime_type: str,\n        system_prompt: str | None,\n        chat_model: str,\n        max_tokens: int | None,\n    ) -> dict[str, Any]:\n        \"\"\"Build payload for OpenRouter Vision API (OpenAI-compatible).\"\"\"\n        messages: list[dict[str, Any]] = []\n        if system_prompt:\n            messages.append({\"role\": \"system\", \"content\": system_prompt})\n\n        messages.append({\n            \"role\": \"user\",\n            \"content\": [\n                {\"type\": \"text\", \"text\": prompt},\n                {\n                    \"type\": \"image_url\",\n                    \"image_url\": {\n                        \"url\": f\"data:{mime_type};base64,{base64_image}\",\n                    },\n                },\n            ],\n        })\n\n        payload: dict[str, Any] = {\n            \"model\": chat_model,\n            \"messages\": messages,\n            \"temperature\": 0.2,\n        }\n        if max_tokens is not None:\n            payload[\"max_tokens\"] = max_tokens\n        return payload\n"
  },
  {
    "path": "src/memu/llm/http_client.py",
    "content": "from __future__ import annotations\n\nimport base64\nimport logging\nimport os\nfrom collections.abc import Callable\nfrom pathlib import Path\nfrom typing import Any, cast\n\nimport httpx\n\nfrom memu.llm.backends.base import LLMBackend\nfrom memu.llm.backends.doubao import DoubaoLLMBackend\nfrom memu.llm.backends.grok import GrokBackend\nfrom memu.llm.backends.openai import OpenAILLMBackend\nfrom memu.llm.backends.openrouter import OpenRouterLLMBackend\n\n\ndef _load_proxy() -> str | None:\n    return os.getenv(\"MEMU_HTTP_PROXY\") or os.getenv(\"HTTP_PROXY\") or os.getenv(\"HTTPS_PROXY\") or None\n\n\n# Minimal embedding backend support (moved from embedding module)\nclass _EmbeddingBackend:\n    name: str\n    embedding_endpoint: str\n\n    def build_embedding_payload(self, *, inputs: list[str], embed_model: str) -> dict[str, Any]:\n        raise NotImplementedError\n\n    def parse_embedding_response(self, data: dict[str, Any]) -> list[list[float]]:\n        raise NotImplementedError\n\n\nclass _OpenAIEmbeddingBackend(_EmbeddingBackend):\n    name = \"openai\"\n    embedding_endpoint = \"/embeddings\"\n\n    def build_embedding_payload(self, *, inputs: list[str], embed_model: str) -> dict[str, Any]:\n        return {\"model\": embed_model, \"input\": inputs}\n\n    def parse_embedding_response(self, data: dict[str, Any]) -> list[list[float]]:\n        return [cast(list[float], d[\"embedding\"]) for d in data[\"data\"]]\n\n\nclass _DoubaoEmbeddingBackend(_EmbeddingBackend):\n    name = \"doubao\"\n    embedding_endpoint = \"/api/v3/embeddings\"\n\n    def build_embedding_payload(self, *, inputs: list[str], embed_model: str) -> dict[str, Any]:\n        return {\"model\": embed_model, \"input\": inputs, \"encoding_format\": \"float\"}\n\n    def parse_embedding_response(self, data: dict[str, Any]) -> list[list[float]]:\n        return [cast(list[float], d[\"embedding\"]) for d in data[\"data\"]]\n\n\nclass _OpenRouterEmbeddingBackend(_EmbeddingBackend):\n    \"\"\"OpenRouter uses OpenAI-compatible embedding API.\"\"\"\n\n    name = \"openrouter\"\n    embedding_endpoint = \"/api/v1/embeddings\"\n\n    def build_embedding_payload(self, *, inputs: list[str], embed_model: str) -> dict[str, Any]:\n        return {\"model\": embed_model, \"input\": inputs}\n\n    def parse_embedding_response(self, data: dict[str, Any]) -> list[list[float]]:\n        return [cast(list[float], d[\"embedding\"]) for d in data[\"data\"]]\n\n\nlogger = logging.getLogger(__name__)\n\nLLM_BACKENDS: dict[str, Callable[[], LLMBackend]] = {\n    OpenAILLMBackend.name: OpenAILLMBackend,\n    DoubaoLLMBackend.name: DoubaoLLMBackend,\n    GrokBackend.name: GrokBackend,\n    OpenRouterLLMBackend.name: OpenRouterLLMBackend,\n}\n\n\nclass HTTPLLMClient:\n    \"\"\"HTTP client for LLM APIs (chat, vision, transcription) and embeddings.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        base_url: str,\n        api_key: str,\n        chat_model: str,\n        provider: str = \"openai\",\n        endpoint_overrides: dict[str, str] | None = None,\n        timeout: int = 60,\n        embed_model: str | None = None,\n    ):\n        # Ensure base_url ends with \"/\" so httpx doesn't discard the path\n        # component when joining with endpoint paths.\n        # See: https://github.com/NevaMind-AI/memU/issues/328\n        self.base_url = base_url.rstrip(\"/\") + \"/\"\n        self.api_key = api_key or \"\"\n        self.chat_model = chat_model\n        self.provider = provider.lower()\n        self.backend = self._load_backend(self.provider)\n        self.embedding_backend = self._load_embedding_backend(self.provider)\n        overrides = endpoint_overrides or {}\n        raw_summary_ep = overrides.get(\"chat\") or overrides.get(\"summary\") or self.backend.summary_endpoint\n        raw_embedding_ep = (\n            overrides.get(\"embeddings\")\n            or overrides.get(\"embedding\")\n            or overrides.get(\"embed\")\n            or self.embedding_backend.embedding_endpoint\n        )\n        # Strip leading \"/\" from endpoints so httpx resolves them relative to\n        # base_url instead of treating them as absolute paths.\n        self.summary_endpoint = raw_summary_ep.lstrip(\"/\")\n        self.embedding_endpoint = raw_embedding_ep.lstrip(\"/\")\n        self.timeout = timeout\n        self.embed_model = embed_model or chat_model\n        self.proxy = _load_proxy()\n\n    async def chat(\n        self,\n        prompt: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n        temperature: float = 0.2,\n    ) -> tuple[str, dict[str, Any]]:\n        \"\"\"Generic chat completion.\"\"\"\n        messages: list[dict[str, Any]] = []\n        if system_prompt is not None:\n            messages.append({\"role\": \"system\", \"content\": system_prompt})\n        messages.append({\"role\": \"user\", \"content\": prompt})\n\n        payload: dict[str, Any] = {\n            \"model\": self.chat_model,\n            \"messages\": messages,\n            \"temperature\": temperature,\n        }\n        if max_tokens is not None:\n            payload[\"max_tokens\"] = max_tokens\n\n        async with httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout) as client:\n            resp = await client.post(self.summary_endpoint, json=payload, headers=self._headers())\n            resp.raise_for_status()\n            data = resp.json()\n        logger.debug(\"HTTP LLM chat response: %s\", data)\n        return self.backend.parse_summary_response(data), data\n\n    async def summarize(\n        self, text: str, max_tokens: int | None = None, system_prompt: str | None = None\n    ) -> tuple[str, dict[str, Any]]:\n        payload = self.backend.build_summary_payload(\n            text=text, system_prompt=system_prompt, chat_model=self.chat_model, max_tokens=max_tokens\n        )\n        async with httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout, proxy=self.proxy) as client:\n            resp = await client.post(self.summary_endpoint, json=payload, headers=self._headers())\n            resp.raise_for_status()\n            data = resp.json()\n        logger.debug(\"HTTP LLM summarize response: %s\", data)\n        return self.backend.parse_summary_response(data), data\n\n    async def vision(\n        self,\n        prompt: str,\n        image_path: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n    ) -> tuple[str, dict[str, Any]]:\n        \"\"\"\n        Call Vision API with an image.\n\n        Args:\n            prompt: Text prompt to send with the image\n            image_path: Path to the image file\n            max_tokens: Maximum tokens in response\n            system_prompt: Optional system prompt\n\n        Returns:\n            Tuple of (LLM response text, raw response dict)\n        \"\"\"\n        # Read and encode image as base64\n        image_data = Path(image_path).read_bytes()\n        base64_image = base64.b64encode(image_data).decode(\"utf-8\")\n\n        # Detect image format\n        suffix = Path(image_path).suffix.lower()\n        mime_type = {\n            \".jpg\": \"image/jpeg\",\n            \".jpeg\": \"image/jpeg\",\n            \".png\": \"image/png\",\n            \".gif\": \"image/gif\",\n            \".webp\": \"image/webp\",\n        }.get(suffix, \"image/jpeg\")\n\n        payload = self.backend.build_vision_payload(\n            prompt=prompt,\n            base64_image=base64_image,\n            mime_type=mime_type,\n            system_prompt=system_prompt,\n            chat_model=self.chat_model,\n            max_tokens=max_tokens,\n        )\n\n        async with httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout, proxy=self.proxy) as client:\n            resp = await client.post(self.summary_endpoint, json=payload, headers=self._headers())\n            resp.raise_for_status()\n            data = resp.json()\n        logger.debug(\"HTTP LLM vision response: %s\", data)\n        return self.backend.parse_summary_response(data), data\n\n    async def embed(self, inputs: list[str]) -> tuple[list[list[float]], dict[str, Any]]:\n        \"\"\"Create text embeddings using the provider-specific embedding API.\"\"\"\n        payload = self.embedding_backend.build_embedding_payload(inputs=inputs, embed_model=self.embed_model)\n        async with httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout, proxy=self.proxy) as client:\n            resp = await client.post(self.embedding_endpoint, json=payload, headers=self._headers())\n            resp.raise_for_status()\n            data = resp.json()\n        logger.debug(\"HTTP embedding response: %s\", data)\n        return self.embedding_backend.parse_embedding_response(data), data\n\n    async def transcribe(\n        self,\n        audio_path: str,\n        *,\n        prompt: str | None = None,\n        language: str | None = None,\n        response_format: str = \"text\",\n    ) -> tuple[str, dict[str, Any] | None]:\n        \"\"\"\n        Transcribe audio file using OpenAI Audio API.\n\n        Args:\n            audio_path: Path to the audio file\n            prompt: Optional prompt to guide the transcription\n            language: Optional language code (e.g., 'en', 'zh')\n            response_format: Response format ('text', 'json', 'verbose_json')\n\n        Returns:\n            Tuple of (transcribed text, raw response dict or None for text format)\n        \"\"\"\n        try:\n            raw_response: dict[str, Any] | None = None\n            # Prepare multipart form data\n            with open(audio_path, \"rb\") as audio_file:\n                files = {\"file\": (Path(audio_path).name, audio_file, \"application/octet-stream\")}\n                data = {\n                    \"model\": \"gpt-4o-mini-transcribe\",\n                    \"response_format\": response_format,\n                }\n                if prompt:\n                    data[\"prompt\"] = prompt\n                if language:\n                    data[\"language\"] = language\n\n                async with httpx.AsyncClient(\n                    base_url=self.base_url, timeout=self.timeout * 3, proxy=self.proxy\n                ) as client:\n                    resp = await client.post(\n                        \"/v1/audio/transcriptions\",\n                        files=files,\n                        data=data,\n                        headers=self._headers(),\n                    )\n                    resp.raise_for_status()\n\n                    if response_format == \"text\":\n                        result = resp.text\n                    else:\n                        raw_response = resp.json()\n                        result = raw_response.get(\"text\", \"\")\n\n            logger.debug(\"HTTP audio transcribe response for %s: %s chars\", audio_path, len(result))\n        except Exception:\n            logger.exception(\"Audio transcription failed for %s\", audio_path)\n            raise\n        else:\n            return result or \"\", raw_response\n\n    def _headers(self) -> dict[str, str]:\n        return {\"Authorization\": f\"Bearer {self.api_key}\"}\n\n    def _load_backend(self, provider: str) -> LLMBackend:\n        factory = LLM_BACKENDS.get(provider)\n        if not factory:\n            msg = f\"Unsupported LLM provider '{provider}'. Available: {', '.join(LLM_BACKENDS.keys())}\"\n            raise ValueError(msg)\n        return factory()\n\n    def _load_embedding_backend(self, provider: str) -> _EmbeddingBackend:\n        backends: dict[str, type[_EmbeddingBackend]] = {\n            _OpenAIEmbeddingBackend.name: _OpenAIEmbeddingBackend,\n            _DoubaoEmbeddingBackend.name: _DoubaoEmbeddingBackend,\n            \"grok\": _OpenAIEmbeddingBackend,\n            _OpenRouterEmbeddingBackend.name: _OpenRouterEmbeddingBackend,\n        }\n        factory = backends.get(provider)\n        if not factory:\n            msg = f\"Unsupported embedding provider '{provider}'. Available: {', '.join(backends.keys())}\"\n            raise ValueError(msg)\n        return factory()\n"
  },
  {
    "path": "src/memu/llm/lazyllm_client.py",
    "content": "import asyncio\nimport functools\nfrom typing import Any, cast\n\nimport lazyllm\nfrom lazyllm import LOG\n\n\nclass LazyLLMClient:\n    \"\"\"LAZYLLM client that relies on the LazyLLM framework.\"\"\"\n\n    DEFAULT_SOURCE = \"qwen\"\n\n    def __init__(\n        self,\n        *,\n        llm_source: str | None = None,\n        vlm_source: str | None = None,\n        embed_source: str | None = None,\n        stt_source: str | None = None,\n        chat_model: str | None = None,\n        vlm_model: str | None = None,\n        embed_model: str | None = None,\n        stt_model: str | None = None,\n    ):\n        self.llm_source = llm_source or self.DEFAULT_SOURCE\n        self.vlm_source = vlm_source or self.DEFAULT_SOURCE\n        self.embed_source = embed_source or self.DEFAULT_SOURCE\n        self.stt_source = stt_source or self.DEFAULT_SOURCE\n        self.chat_model = chat_model\n        self.vlm_model = vlm_model\n        self.embed_model = embed_model\n        self.stt_model = stt_model\n\n    async def _call_async(self, client: Any, *args: Any, **kwargs: Any) -> Any:\n        \"\"\"\n        Asynchronously call a LazyLLM client with given arguments and keyword arguments.\n        \"\"\"\n        if kwargs:\n            return await asyncio.to_thread(functools.partial(client, *args, **kwargs))\n        else:\n            return await asyncio.to_thread(client, *args)\n\n    async def chat(\n        self,\n        text: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n        temperature: float = 0.2,\n    ) -> str:\n        \"\"\"\n        Generate a summary or response for the input text using the configured LLM backend.\n\n        Args:\n            text: The input text to summarize or process.\n            max_tokens: (Optional) Maximum number of tokens to generate.\n            system_prompt: (Optional) System instruction to guide the LLM behavior.\n        Return:\n            The generated summary text as a string.\n        \"\"\"\n        client = lazyllm.namespace(\"MEMU\").OnlineModule(source=self.llm_source, model=self.chat_model, type=\"llm\")\n        prompt = f\"{system_prompt}\\n\\n\" if system_prompt else \"\"\n        full_prompt = f\"{prompt}text:\\n{text}\"\n        LOG.debug(f\"Summarizing text with {self.llm_source}/{self.chat_model}\")\n        response = await self._call_async(client, full_prompt)\n        return cast(str, response)\n\n    async def summarize(\n        self,\n        text: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n    ) -> str:\n        \"\"\"\n        Generate a summary or response for the input text using the configured LLM backend.\n\n        Args:\n            text: The input text to summarize or process.\n            max_tokens: (Optional) Maximum number of tokens to generate.\n            system_prompt: (Optional) System instruction to guide the LLM behavior.\n        Return:\n            The generated summary text as a string.\n        \"\"\"\n        client = lazyllm.namespace(\"MEMU\").OnlineModule(source=self.llm_source, model=self.chat_model, type=\"llm\")\n        prompt = system_prompt or \"Summarize the text in one short paragraph.\"\n        full_prompt = f\"{prompt}\\n\\ntext:\\n{text}\"\n        LOG.debug(f\"Summarizing text with {self.llm_source}/{self.chat_model}\")\n        response = await self._call_async(client, full_prompt)\n        return cast(str, response)\n\n    async def vision(\n        self,\n        prompt: str,\n        image_path: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n    ) -> tuple[str, Any]:\n        \"\"\"\n        Process an image with a text prompt using the configured VLM (Vision-Language Model).\n\n\n        Args:\n            prompt: Text prompt describing the request or question about the image.\n            image_path: Path to the image file to be analyzed.\n            max_tokens: (Optional) Maximum number of tokens to generate.\n            system_prompt: (Optional) System instruction to guide the VLM behavior.\n        Return:\n            A tuple containing the generated text response and None (reserved for metadata).\n        \"\"\"\n        client = lazyllm.namespace(\"MEMU\").OnlineModule(source=self.vlm_source, model=self.vlm_model, type=\"vlm\")\n        LOG.debug(f\"Processing image with {self.vlm_source}/{self.vlm_model}: {image_path}\")\n        # LazyLLM VLM accepts prompt as first positional argument and image_path as keyword argument\n        response = await self._call_async(client, prompt, lazyllm_files=image_path)\n        return response, None\n\n    async def embed(\n        self,\n        texts: list[str],\n        batch_size: int = 10,\n    ) -> list[list[float]]:\n        \"\"\"\n        Generate vector embeddings for a list of text strings.\n\n        Args:\n            texts: List of text strings to embed.\n            batch_size: (Optional) Batch size for processing embeddings (default: 10).\n        Return:\n            A list of embedding vectors (list of floats), one for each input text.\n        \"\"\"\n        client = lazyllm.namespace(\"MEMU\").OnlineModule(\n            source=self.embed_source, model=self.embed_model, type=\"embed\", batch_size=batch_size\n        )\n        LOG.debug(f\"embed {len(texts)} texts with {self.embed_source}/{self.embed_model}\")\n        response = await self._call_async(client, texts)\n        return cast(list[list[float]], response)\n\n    async def transcribe(\n        self,\n        audio_path: str,\n        language: str | None = None,\n        prompt: str | None = None,\n    ) -> str:\n        \"\"\"\n        Transcribe audio content to text using the configured STT (Speech-to-Text) backend.\n\n        Args:\n            audio_path: Path to the audio file to transcribe.\n            language: (Optional) Language code of the audio content.\n            prompt: (Optional) Text prompt to guide the transcription or translation.\n        Return:\n            The transcribed text as a string.\n        \"\"\"\n        client = lazyllm.namespace(\"MEMU\").OnlineModule(source=self.stt_source, model=self.stt_model, type=\"stt\")\n        LOG.debug(f\"Transcribing audio with {self.stt_source}/{self.stt_model}: {audio_path}\")\n        response = await self._call_async(client, audio_path)\n        return cast(str, response)\n"
  },
  {
    "path": "src/memu/llm/openai_sdk.py",
    "content": "import base64\nimport logging\nfrom pathlib import Path\nfrom typing import Any, Literal, cast\n\nfrom openai import AsyncOpenAI\nfrom openai.types import CreateEmbeddingResponse\nfrom openai.types.chat import (\n    ChatCompletion,\n    ChatCompletionContentPartImageParam,\n    ChatCompletionContentPartTextParam,\n    ChatCompletionMessageParam,\n    ChatCompletionSystemMessageParam,\n    ChatCompletionUserMessageParam,\n)\n\nlogger = logging.getLogger(__name__)\n\n\nclass OpenAISDKClient:\n    \"\"\"OpenAI LLM client that relies on the official Python SDK.\"\"\"\n\n    def __init__(\n        self,\n        *,\n        base_url: str,\n        api_key: str,\n        chat_model: str,\n        embed_model: str,\n        embed_batch_size: int = 1,\n    ):\n        self.base_url = base_url.rstrip(\"/\")\n        self.api_key = api_key or \"\"\n        self.chat_model = chat_model\n        self.embed_model = embed_model\n        self.embed_batch_size = embed_batch_size\n        self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)\n\n    async def chat(\n        self,\n        prompt: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n        temperature: float = 0.2,\n    ) -> tuple[str, ChatCompletion]:\n        \"\"\"Generic chat completion.\"\"\"\n        messages: list[ChatCompletionMessageParam] = []\n        if system_prompt is not None:\n            system_message: ChatCompletionSystemMessageParam = {\"role\": \"system\", \"content\": system_prompt}\n            messages.append(system_message)\n\n        user_message: ChatCompletionUserMessageParam = {\"role\": \"user\", \"content\": prompt}\n        messages.append(user_message)\n\n        response = await self.client.chat.completions.create(\n            model=self.chat_model,\n            messages=messages,\n            temperature=temperature,\n            max_tokens=max_tokens,\n        )\n        content = response.choices[0].message.content\n        logger.debug(\"OpenAI chat response: %s\", response)\n        return content or \"\", response\n\n    async def summarize(\n        self,\n        text: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n    ) -> tuple[str, ChatCompletion]:\n        prompt = system_prompt or \"Summarize the text in one short paragraph.\"\n\n        system_message: ChatCompletionSystemMessageParam = {\"role\": \"system\", \"content\": prompt}\n        user_message: ChatCompletionUserMessageParam = {\"role\": \"user\", \"content\": text}\n        messages: list[ChatCompletionMessageParam] = [system_message, user_message]\n\n        response = await self.client.chat.completions.create(\n            model=self.chat_model,\n            messages=messages,\n            temperature=1,\n            max_tokens=max_tokens,\n        )\n        content = response.choices[0].message.content\n        logger.debug(\"OpenAI summarize response: %s\", response)\n        return content or \"\", response\n\n    async def vision(\n        self,\n        prompt: str,\n        image_path: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n    ) -> tuple[str, ChatCompletion]:\n        \"\"\"\n        Call OpenAI Vision API with an image.\n\n        Args:\n            prompt: Text prompt to send with the image\n            image_path: Path to the image file\n            max_tokens: Maximum tokens in response\n            system_prompt: Optional system prompt\n\n        Returns:\n            Tuple of (LLM response text, raw ChatCompletion response)\n        \"\"\"\n        # Read and encode image as base64\n        image_data = Path(image_path).read_bytes()\n        base64_image = base64.b64encode(image_data).decode(\"utf-8\")\n\n        # Detect image format\n        suffix = Path(image_path).suffix.lower()\n        mime_type = {\n            \".jpg\": \"image/jpeg\",\n            \".jpeg\": \"image/jpeg\",\n            \".png\": \"image/png\",\n            \".gif\": \"image/gif\",\n            \".webp\": \"image/webp\",\n        }.get(suffix, \"image/jpeg\")\n\n        # Build messages with image\n        messages: list[ChatCompletionMessageParam] = []\n        if system_prompt:\n            system_message: ChatCompletionSystemMessageParam = {\n                \"role\": \"system\",\n                \"content\": system_prompt,\n            }\n            messages.append(system_message)\n\n        text_part: ChatCompletionContentPartTextParam = {\"type\": \"text\", \"text\": prompt}\n        image_part: ChatCompletionContentPartImageParam = {\n            \"type\": \"image_url\",\n            \"image_url\": {\n                \"url\": f\"data:{mime_type};base64,{base64_image}\",\n            },\n        }\n        user_message: ChatCompletionUserMessageParam = {\n            \"role\": \"user\",\n            \"content\": [text_part, image_part],\n        }\n        messages.append(user_message)\n\n        response = await self.client.chat.completions.create(\n            model=self.chat_model,\n            messages=messages,\n            temperature=1,\n            max_tokens=max_tokens,\n        )\n        content = response.choices[0].message.content\n        logger.debug(\"OpenAI vision response: %s\", response)\n        return content or \"\", response\n\n    async def embed(self, inputs: list[str]) -> tuple[list[list[float]], CreateEmbeddingResponse | None]:\n        \"\"\"Create text embeddings via the official SDK.\"\"\"\n        if len(inputs) <= self.embed_batch_size:\n            response = await self.client.embeddings.create(model=self.embed_model, input=inputs)\n            return [cast(list[float], d.embedding) for d in response.data], response\n\n        # For batched requests, we aggregate embeddings but only return the last response for usage\n        all_embeddings: list[list[float]] = []\n        last_response: CreateEmbeddingResponse | None = None\n        for idx in range(0, len(inputs), self.embed_batch_size):\n            batch = inputs[idx : idx + self.embed_batch_size]\n            response = await self.client.embeddings.create(model=self.embed_model, input=batch)\n            all_embeddings.extend([cast(list[float], d.embedding) for d in response.data])\n            last_response = response\n\n        return all_embeddings, last_response\n\n    async def transcribe(\n        self,\n        audio_path: str,\n        *,\n        prompt: str | None = None,\n        language: str | None = None,\n        response_format: Literal[\"text\", \"json\", \"verbose_json\"] = \"text\",\n    ) -> tuple[str, Any]:\n        \"\"\"\n        Transcribe audio file using OpenAI Audio API.\n\n        Args:\n            audio_path: Path to the audio file\n            prompt: Optional prompt to guide the transcription\n            language: Optional language code (e.g., 'en', 'zh')\n            response_format: Response format ('text', 'json', 'verbose_json')\n\n        Returns:\n            Tuple of (transcribed text, raw transcription response)\n        \"\"\"\n        try:\n            # Use gpt-4o-mini-transcribe for better performance and cost\n            kwargs: dict[str, Any] = {}\n            if prompt is not None:\n                kwargs[\"prompt\"] = prompt\n            if language is not None:\n                kwargs[\"language\"] = language\n            with open(audio_path, \"rb\") as audio_stream:\n                transcription = await self.client.audio.transcriptions.create(\n                    file=audio_stream,\n                    model=\"gpt-4o-mini-transcribe\",\n                    response_format=response_format,\n                    **kwargs,\n                )\n\n            # Handle different response formats\n            if response_format == \"text\":\n                result = transcription if isinstance(transcription, str) else transcription.text\n            else:\n                result = transcription.text if hasattr(transcription, \"text\") else str(transcription)\n\n            logger.debug(\"OpenAI transcribe response for %s: %s chars\", audio_path, len(result))\n        except Exception:\n            logger.exception(\"Audio transcription failed for %s\", audio_path)\n            raise\n        else:\n            return result or \"\", transcription\n"
  },
  {
    "path": "src/memu/llm/wrapper.py",
    "content": "from __future__ import annotations\n\nimport hashlib\nimport inspect\nimport logging\nimport threading\nimport time\nimport uuid\nfrom collections.abc import Callable, Mapping, Sequence\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Any\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass(frozen=True)\nclass LLMCallContext:\n    profile: str\n    request_id: str\n    trace_id: str | None\n    operation: str | None\n    step_id: str | None\n    provider: str | None\n    model: str | None\n    tags: Mapping[str, Any] | None\n\n\n@dataclass(frozen=True)\nclass LLMRequestView:\n    kind: str\n    input_items: int | None = None\n    input_chars: int | None = None\n    content: str | list[str] | None = None\n    content_hash: str | None = None\n    metadata: dict[str, Any] = field(default_factory=dict)\n\n\n@dataclass(frozen=True)\nclass LLMResponseView:\n    output_items: int | None = None\n    output_chars: int | None = None\n    content: str | None = None\n    content_hash: str | None = None\n    metadata: dict[str, Any] = field(default_factory=dict)\n\n\n@dataclass(frozen=True)\nclass LLMUsage:\n    input_tokens: int | None = None\n    output_tokens: int | None = None\n    total_tokens: int | None = None\n    cached_input_tokens: int | None = None\n    reasoning_tokens: int | None = None\n    latency_ms: float | None = None\n    finish_reason: str | None = None\n    status: str | None = None\n    tokens_breakdown: dict[str, Any] | None = None\n\n\n@dataclass(frozen=True)\nclass LLMCallFilter:\n    operations: set[str] | None = None\n    step_ids: set[str] | None = None\n    providers: set[str] | None = None\n    models: set[str] | None = None\n    statuses: set[str] | None = None\n\n    def __post_init__(self) -> None:\n        object.__setattr__(self, \"operations\", _normalize_set(self.operations))\n        object.__setattr__(self, \"providers\", _normalize_set(self.providers))\n        object.__setattr__(self, \"models\", _normalize_set(self.models))\n        object.__setattr__(self, \"statuses\", _normalize_set(self.statuses))\n\n    def matches(self, ctx: LLMCallContext, status: str | None) -> bool:\n        if self.operations and (ctx.operation or \"\").lower() not in self.operations:\n            return False\n        if self.step_ids and (ctx.step_id or \"\") not in self.step_ids:\n            return False\n        if self.providers and (ctx.provider or \"\").lower() not in self.providers:\n            return False\n        if self.models and (ctx.model or \"\").lower() not in self.models:\n            return False\n        if self.statuses:\n            return status is not None and status.lower() in self.statuses\n        return True\n\n\n@dataclass(frozen=True)\nclass LLMCallMetadata:\n    profile: str | None = None\n    operation: str | None = None\n    step_id: str | None = None\n    trace_id: str | None = None\n    tags: Mapping[str, Any] | None = None\n\n\n@dataclass(frozen=True)\nclass _LLMInterceptor:\n    interceptor_id: int\n    fn: Callable[..., Any]\n    name: str | None\n    priority: int\n    order: int\n    filter: LLMCallFilter | Callable[[LLMCallContext, str | None], bool] | None\n\n\n@dataclass(frozen=True)\nclass _LLMInterceptorSnapshot:\n    before: tuple[_LLMInterceptor, ...]\n    after: tuple[_LLMInterceptor, ...]\n    on_error: tuple[_LLMInterceptor, ...]\n\n\nclass LLMInterceptorHandle:\n    def __init__(self, registry: LLMInterceptorRegistry, interceptor_id: int) -> None:\n        self._registry = registry\n        self._interceptor_id = interceptor_id\n        self._disposed = False\n\n    def dispose(self) -> bool:\n        if self._disposed:\n            return False\n        self._disposed = True\n        return self._registry.remove(self._interceptor_id)\n\n\nclass LLMInterceptorRegistry:\n    def __init__(self, *, strict: bool = False) -> None:\n        self._before: tuple[_LLMInterceptor, ...] = ()\n        self._after: tuple[_LLMInterceptor, ...] = ()\n        self._on_error: tuple[_LLMInterceptor, ...] = ()\n        self._lock = threading.Lock()\n        self._seq = 0\n        self._strict = strict\n\n    @property\n    def strict(self) -> bool:\n        return self._strict\n\n    def register_before(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        priority: int = 0,\n        where: LLMCallFilter | Callable[[LLMCallContext, str | None], bool] | Mapping[str, Any] | None = None,\n    ) -> LLMInterceptorHandle:\n        return self._register(\"before\", fn, name=name, priority=priority, where=where)\n\n    def register_after(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        priority: int = 0,\n        where: LLMCallFilter | Callable[[LLMCallContext, str | None], bool] | Mapping[str, Any] | None = None,\n    ) -> LLMInterceptorHandle:\n        return self._register(\"after\", fn, name=name, priority=priority, where=where)\n\n    def register_on_error(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n        priority: int = 0,\n        where: LLMCallFilter | Callable[[LLMCallContext, str | None], bool] | Mapping[str, Any] | None = None,\n    ) -> LLMInterceptorHandle:\n        return self._register(\"on_error\", fn, name=name, priority=priority, where=where)\n\n    def _register(\n        self,\n        kind: str,\n        fn: Callable[..., Any],\n        *,\n        name: str | None,\n        priority: int,\n        where: LLMCallFilter | Callable[[LLMCallContext, str | None], bool] | Mapping[str, Any] | None,\n    ) -> LLMInterceptorHandle:\n        if not callable(fn):\n            msg = \"Interceptor must be callable\"\n            raise TypeError(msg)\n        where = _coerce_filter(where)\n        with self._lock:\n            self._seq += 1\n            interceptor = _LLMInterceptor(\n                interceptor_id=self._seq,\n                fn=fn,\n                name=name,\n                priority=priority,\n                order=self._seq,\n                filter=where,\n            )\n            if kind == \"before\":\n                self._before = _sorted_interceptors(self._before, interceptor)\n            elif kind == \"after\":\n                self._after = _sorted_interceptors(self._after, interceptor)\n            elif kind == \"on_error\":\n                self._on_error = _sorted_interceptors(self._on_error, interceptor)\n            else:\n                msg = f\"Unknown interceptor kind '{kind}'\"\n                raise ValueError(msg)\n        return LLMInterceptorHandle(self, interceptor.interceptor_id)\n\n    def remove(self, interceptor_id: int) -> bool:\n        with self._lock:\n            removed = False\n            before = tuple(i for i in self._before if i.interceptor_id != interceptor_id)\n            after = tuple(i for i in self._after if i.interceptor_id != interceptor_id)\n            on_error = tuple(i for i in self._on_error if i.interceptor_id != interceptor_id)\n            if len(before) != len(self._before):\n                removed = True\n                self._before = before\n            if len(after) != len(self._after):\n                removed = True\n                self._after = after\n            if len(on_error) != len(self._on_error):\n                removed = True\n                self._on_error = on_error\n        return removed\n\n    def snapshot(self) -> _LLMInterceptorSnapshot:\n        return _LLMInterceptorSnapshot(self._before, self._after, self._on_error)\n\n\nclass LLMClientWrapper:\n    def __init__(\n        self,\n        client: Any,\n        *,\n        registry: LLMInterceptorRegistry,\n        metadata: LLMCallMetadata | None = None,\n        provider: str | None = None,\n        chat_model: str | None = None,\n        embed_model: str | None = None,\n    ) -> None:\n        self._client = client\n        self._registry = registry\n        self._metadata = metadata or LLMCallMetadata()\n        self._provider = provider\n        self._chat_model = chat_model or getattr(client, \"chat_model\", None)\n        self._embed_model = embed_model or getattr(client, \"embed_model\", None)\n\n    def __getattr__(self, name: str) -> Any:\n        return getattr(self._client, name)\n\n    async def summarize(\n        self,\n        text: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n    ) -> Any:\n        request_view = _build_text_request_view(\n            \"summarize\",\n            text,\n            metadata={\n                \"system_prompt_chars\": len(system_prompt or \"\"),\n                \"max_tokens\": max_tokens,\n            },\n        )\n\n        async def _call() -> Any:\n            return await self._client.summarize(text, max_tokens=max_tokens, system_prompt=system_prompt)\n\n        return await self._invoke(\n            kind=\"summarize\",\n            call_fn=_call,\n            request_view=request_view,\n            model=self._chat_model,\n            response_builder=_build_text_response_view,\n        )\n\n    async def chat(\n        self,\n        prompt: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n        temperature: float = 0.2,\n    ) -> Any:\n        request_view = _build_text_request_view(\n            \"chat\",\n            prompt,\n            metadata={\n                \"system_prompt_chars\": len(system_prompt or \"\"),\n                \"max_tokens\": max_tokens,\n                \"temperature\": temperature,\n            },\n        )\n\n        async def _call() -> Any:\n            return await self._client.chat(\n                prompt,\n                max_tokens=max_tokens,\n                system_prompt=system_prompt,\n                temperature=temperature,\n            )\n\n        return await self._invoke(\n            kind=\"chat\",\n            call_fn=_call,\n            request_view=request_view,\n            model=self._chat_model,\n            response_builder=_build_text_response_view,\n        )\n\n    async def vision(\n        self,\n        prompt: str,\n        image_path: str,\n        *,\n        max_tokens: int | None = None,\n        system_prompt: str | None = None,\n    ) -> Any:\n        metadata = {\n            \"image_path\": Path(image_path).name,\n            \"image_bytes\": _safe_file_size(image_path),\n            \"system_prompt_chars\": len(system_prompt or \"\"),\n            \"max_tokens\": max_tokens,\n        }\n        request_view = _build_text_request_view(\"vision\", prompt, metadata=metadata)\n\n        async def _call() -> Any:\n            return await self._client.vision(\n                prompt,\n                image_path,\n                max_tokens=max_tokens,\n                system_prompt=system_prompt,\n            )\n\n        return await self._invoke(\n            kind=\"vision\",\n            call_fn=_call,\n            request_view=request_view,\n            model=self._chat_model,\n            response_builder=_build_text_response_view,\n        )\n\n    async def embed(self, inputs: list[str]) -> Any:\n        request_view = _build_embedding_request_view(inputs)\n\n        async def _call() -> Any:\n            return await self._client.embed(inputs)\n\n        return await self._invoke(\n            kind=\"embed\",\n            call_fn=_call,\n            request_view=request_view,\n            model=self._embed_model,\n            response_builder=_build_embedding_response_view,\n        )\n\n    async def transcribe(\n        self,\n        audio_path: str,\n        *,\n        prompt: str | None = None,\n        language: str | None = None,\n        response_format: str = \"text\",\n    ) -> Any:\n        metadata = {\n            \"audio_path\": Path(audio_path).name,\n            \"audio_bytes\": _safe_file_size(audio_path),\n            \"prompt_chars\": len(prompt or \"\"),\n            \"language\": language,\n            \"response_format\": response_format,\n        }\n        request_view = _build_text_request_view(\"transcribe\", prompt or \"\", metadata=metadata)\n\n        async def _call() -> Any:\n            return await self._client.transcribe(\n                audio_path,\n                prompt=prompt,\n                language=language,\n                response_format=response_format,\n            )\n\n        return await self._invoke(\n            kind=\"transcribe\",\n            call_fn=_call,\n            request_view=request_view,\n            model=None,\n            response_builder=_build_text_response_view,\n        )\n\n    async def _invoke(\n        self,\n        *,\n        kind: str,\n        call_fn: Callable[[], Any],\n        request_view: LLMRequestView,\n        model: str | None,\n        response_builder: Callable[[Any], LLMResponseView],\n    ) -> Any:\n        call_ctx = self._build_call_context(model)\n        snapshot = self._registry.snapshot()\n        await self._run_before(snapshot.before, call_ctx, request_view)\n        start_time = time.perf_counter()\n        try:\n            result = call_fn()\n            if inspect.isawaitable(result):\n                result = await result\n        except Exception as exc:\n            latency_ms = (time.perf_counter() - start_time) * 1000\n            usage = LLMUsage(latency_ms=latency_ms, status=\"error\")\n            await self._run_on_error(snapshot.on_error, call_ctx, request_view, exc, usage)\n            raise\n        else:\n            latency_ms = (time.perf_counter() - start_time) * 1000\n\n            # Handle tuple response: (pure_response, raw_response)\n            pure_result = result\n            raw_response = None\n            if isinstance(result, tuple) and len(result) == 2:\n                pure_result, raw_response = result\n\n            response_view = response_builder(pure_result)\n\n            # Extract token usage from raw response (best-effort)\n            extracted_usage = _extract_usage_from_raw_response(kind=kind, raw_response=raw_response)\n            usage = LLMUsage(\n                input_tokens=extracted_usage.get(\"input_tokens\"),\n                output_tokens=extracted_usage.get(\"output_tokens\"),\n                total_tokens=extracted_usage.get(\"total_tokens\"),\n                cached_input_tokens=extracted_usage.get(\"cached_input_tokens\"),\n                reasoning_tokens=extracted_usage.get(\"reasoning_tokens\"),\n                latency_ms=latency_ms,\n                finish_reason=extracted_usage.get(\"finish_reason\"),\n                status=\"success\",\n                tokens_breakdown=extracted_usage.get(\"tokens_breakdown\"),\n            )\n\n            await self._run_after(snapshot.after, call_ctx, request_view, response_view, usage)\n            return pure_result\n\n    def _build_call_context(self, model: str | None) -> LLMCallContext:\n        request_id = uuid.uuid4().hex\n        return LLMCallContext(\n            profile=self._metadata.profile or \"\",\n            request_id=request_id,\n            trace_id=self._metadata.trace_id,\n            operation=self._metadata.operation,\n            step_id=self._metadata.step_id,\n            provider=self._provider,\n            model=model,\n            tags=self._metadata.tags,\n        )\n\n    async def _run_before(\n        self,\n        interceptors: Sequence[_LLMInterceptor],\n        ctx: LLMCallContext,\n        request_view: LLMRequestView,\n    ) -> None:\n        for interceptor in interceptors:\n            if not _should_run_interceptor(interceptor, ctx, None):\n                continue\n            await _safe_invoke_interceptor(\n                interceptor,\n                self._registry.strict,\n                ctx,\n                request_view,\n            )\n\n    async def _run_after(\n        self,\n        interceptors: Sequence[_LLMInterceptor],\n        ctx: LLMCallContext,\n        request_view: LLMRequestView,\n        response_view: LLMResponseView,\n        usage: LLMUsage,\n    ) -> None:\n        for interceptor in reversed(interceptors):\n            if not _should_run_interceptor(interceptor, ctx, \"success\"):\n                continue\n            await _safe_invoke_interceptor(\n                interceptor,\n                self._registry.strict,\n                ctx,\n                request_view,\n                response_view,\n                usage,\n            )\n\n    async def _run_on_error(\n        self,\n        interceptors: Sequence[_LLMInterceptor],\n        ctx: LLMCallContext,\n        request_view: LLMRequestView,\n        error: Exception,\n        usage: LLMUsage,\n    ) -> None:\n        for interceptor in reversed(interceptors):\n            if not _should_run_interceptor(interceptor, ctx, \"error\"):\n                continue\n            await _safe_invoke_interceptor(\n                interceptor,\n                self._registry.strict,\n                ctx,\n                request_view,\n                error,\n                usage,\n            )\n\n\ndef _normalize_set(values: set[str] | None) -> set[str] | None:\n    if not values:\n        return None\n    return {str(value).lower() for value in values}\n\n\ndef _sorted_interceptors(\n    existing: tuple[_LLMInterceptor, ...],\n    interceptor: _LLMInterceptor,\n) -> tuple[_LLMInterceptor, ...]:\n    items = list(existing)\n    items.append(interceptor)\n    items.sort(key=lambda item: (item.priority, item.order))\n    return tuple(items)\n\n\ndef _hash_text(value: str | None) -> str | None:\n    if not value:\n        return None\n    return hashlib.sha256(value.encode(\"utf-8\")).hexdigest()\n\n\ndef _hash_texts(values: Sequence[str]) -> str | None:\n    if not values:\n        return None\n    sha = hashlib.sha256()\n    for value in values:\n        sha.update(value.encode(\"utf-8\"))\n        sha.update(b\"\\0\")\n    return sha.hexdigest()\n\n\ndef _safe_file_size(path: str) -> int | None:\n    try:\n        return Path(path).stat().st_size\n    except OSError:\n        return None\n\n\ndef _build_text_request_view(\n    kind: str,\n    text: str,\n    *,\n    metadata: dict[str, Any] | None = None,\n) -> LLMRequestView:\n    return LLMRequestView(\n        kind=kind,\n        input_items=1,\n        input_chars=len(text),\n        content=text,\n        content_hash=_hash_text(text),\n        metadata=metadata or {},\n    )\n\n\ndef _build_text_response_view(response: str) -> LLMResponseView:\n    return LLMResponseView(\n        output_items=1,\n        output_chars=len(response),\n        content=response,\n        content_hash=_hash_text(response),\n        metadata={},\n    )\n\n\ndef _build_embedding_request_view(inputs: Sequence[str]) -> LLMRequestView:\n    total_chars = sum(len(text) for text in inputs)\n    return LLMRequestView(\n        kind=\"embed\",\n        input_items=len(inputs),\n        input_chars=total_chars,\n        content=list(inputs),\n        content_hash=_hash_texts(inputs),\n        metadata={},\n    )\n\n\ndef _build_embedding_response_view(response: Sequence[Sequence[float]]) -> LLMResponseView:\n    vector_dim = len(response[0]) if response else 0\n    return LLMResponseView(\n        output_items=len(response),\n        output_chars=None,\n        content_hash=None,\n        metadata={\"vector_dim\": vector_dim},\n    )\n\n\ndef _get_attr_or_key(obj: Any, key: str) -> Any:\n    \"\"\"Extract value from object attribute or dict key.\"\"\"\n    if obj is None:\n        return None\n    if hasattr(obj, key):\n        return getattr(obj, key)\n    if isinstance(obj, dict):\n        return obj.get(key)\n    return None\n\n\ndef _extract_finish_reason(raw_response: Any) -> str | None:\n    \"\"\"Extract finish_reason from choices[0] if available.\"\"\"\n    choices = _get_attr_or_key(raw_response, \"choices\")\n    if choices and len(choices) > 0:\n        result = _get_attr_or_key(choices[0], \"finish_reason\")\n        return str(result) if result is not None else None\n    return None\n\n\ndef _get_usage_object(raw_response: Any) -> Any:\n    \"\"\"Get usage object/dict from response.\"\"\"\n    if hasattr(raw_response, \"usage\") and raw_response.usage is not None:\n        return raw_response.usage\n    if isinstance(raw_response, dict) and \"usage\" in raw_response:\n        return raw_response[\"usage\"]\n    return None\n\n\ndef _convert_to_dict(obj: Any) -> dict[str, Any] | None:\n    \"\"\"Convert object to dict using available methods.\"\"\"\n    if hasattr(obj, \"model_dump\"):\n        result: dict[str, Any] = obj.model_dump()\n        return result\n    if hasattr(obj, \"__dict__\"):\n        return dict(obj.__dict__)\n    if isinstance(obj, dict):\n        return obj\n    return None\n\n\ndef _extract_token_details(usage_obj: Any, usage_data: dict[str, Any]) -> None:\n    \"\"\"Extract token breakdown and cached tokens from usage object.\"\"\"\n    completion_tokens_details = _get_attr_or_key(usage_obj, \"completion_tokens_details\")\n    if completion_tokens_details is not None:\n        breakdown = _convert_to_dict(completion_tokens_details)\n        if breakdown is not None:\n            usage_data[\"tokens_breakdown\"] = breakdown\n        reasoning_tokens = _get_attr_or_key(completion_tokens_details, \"reasoning_tokens\")\n        if reasoning_tokens is not None:\n            usage_data[\"reasoning_tokens\"] = reasoning_tokens\n\n    prompt_tokens_details = _get_attr_or_key(usage_obj, \"prompt_tokens_details\")\n    if prompt_tokens_details is not None:\n        cached_tokens = _get_attr_or_key(prompt_tokens_details, \"cached_tokens\")\n        if cached_tokens is not None:\n            usage_data[\"cached_input_tokens\"] = cached_tokens\n\n\ndef _extract_usage_from_raw_response(kind: str, raw_response: Any) -> dict[str, Any]:\n    \"\"\"\n    Best-effort extraction of token usage from raw LLM response.\n\n    Supports OpenAI SDK response objects and JSON dict responses.\n    Mapping:\n        input_tokens <- prompt_tokens\n        output_tokens <- completion_tokens\n        total_tokens <- total_tokens\n        tokens_breakdown <- completion_tokens_details\n    \"\"\"\n    usage_data: dict[str, Any] = {}\n\n    if raw_response is None:\n        return usage_data\n\n    try:\n        finish_reason = _extract_finish_reason(raw_response)\n        if finish_reason is not None:\n            usage_data[\"finish_reason\"] = finish_reason\n\n        usage_obj = _get_usage_object(raw_response)\n        if usage_obj is None:\n            return usage_data\n\n        # Map prompt_tokens -> input_tokens\n        prompt_tokens = _get_attr_or_key(usage_obj, \"prompt_tokens\")\n        if prompt_tokens is not None:\n            usage_data[\"input_tokens\"] = prompt_tokens\n\n        # Map completion_tokens -> output_tokens\n        completion_tokens = _get_attr_or_key(usage_obj, \"completion_tokens\")\n        if completion_tokens is not None:\n            usage_data[\"output_tokens\"] = completion_tokens\n\n        # total_tokens stays the same\n        total_tokens = _get_attr_or_key(usage_obj, \"total_tokens\")\n        if total_tokens is not None:\n            usage_data[\"total_tokens\"] = total_tokens\n\n        # Some providers does not explicitly return input_tokens for embedding calls\n        if kind == \"embed\" and usage_data.get(\"total_tokens\") and not usage_data.get(\"input_tokens\"):\n            usage_data[\"input_tokens\"] = usage_data[\"total_tokens\"]\n\n        _extract_token_details(usage_obj, usage_data)\n\n    except Exception:\n        # Best-effort: silently ignore extraction errors\n        logger.debug(\"Failed to extract usage from raw response\", exc_info=True)\n\n    return usage_data\n\n\ndef _coerce_filter(\n    where: LLMCallFilter | Callable[[LLMCallContext, str | None], bool] | Mapping[str, Any] | None,\n) -> LLMCallFilter | Callable[[LLMCallContext, str | None], bool] | None:\n    if where is None or callable(where) or isinstance(where, LLMCallFilter):\n        return where\n    if isinstance(where, Mapping):\n        return LLMCallFilter(\n            operations=_ensure_set(where.get(\"operations\") or where.get(\"operation\")),\n            step_ids=_ensure_set(where.get(\"step_ids\") or where.get(\"step_id\")),\n            providers=_ensure_set(where.get(\"providers\") or where.get(\"provider\")),\n            models=_ensure_set(where.get(\"models\") or where.get(\"model\")),\n            statuses=_ensure_set(where.get(\"statuses\") or where.get(\"status\")),\n        )\n    msg = \"Filter must be a callable, mapping, or LLMCallFilter\"\n    raise TypeError(msg)\n\n\ndef _ensure_set(value: Any) -> set[str] | None:\n    if value is None:\n        return None\n    if isinstance(value, set):\n        return {str(item) for item in value}\n    if isinstance(value, (list, tuple)):\n        return {str(item) for item in value}\n    return {str(value)}\n\n\ndef _should_run_interceptor(\n    interceptor: _LLMInterceptor,\n    ctx: LLMCallContext,\n    status: str | None,\n) -> bool:\n    filt = interceptor.filter\n    if filt is None:\n        return True\n    if isinstance(filt, LLMCallFilter):\n        try:\n            return filt.matches(ctx, status)\n        except Exception:\n            logger.exception(\"LLM interceptor filter failed: %s\", interceptor.name or interceptor.interceptor_id)\n            return False\n    try:\n        return bool(filt(ctx, status))\n    except TypeError:\n        try:\n            return bool(filt(ctx, None))\n        except Exception:\n            logger.exception(\"LLM interceptor filter failed: %s\", interceptor.name or interceptor.interceptor_id)\n            return False\n    except Exception:\n        logger.exception(\"LLM interceptor filter failed: %s\", interceptor.name or interceptor.interceptor_id)\n        return False\n\n\nasync def _safe_invoke_interceptor(\n    interceptor: _LLMInterceptor,\n    strict: bool,\n    *args: Any,\n) -> None:\n    try:\n        result = interceptor.fn(*args)\n        if inspect.isawaitable(result):\n            await result\n    except Exception:\n        if strict:\n            raise\n        logger.exception(\"LLM interceptor failed: %s\", interceptor.name or interceptor.interceptor_id)\n"
  },
  {
    "path": "src/memu/prompts/__init__.py",
    "content": "from memu.prompts.category_summary import PROMPT as CATEGORY_SUMMARY_PROMPT\nfrom memu.prompts.memory_type import DEFAULT_MEMORY_TYPES\nfrom memu.prompts.memory_type import PROMPTS as MEMORY_TYPE_PROMPTS\nfrom memu.prompts.preprocess import PROMPTS as PREPROCESS_PROMPTS\nfrom memu.prompts.retrieve.judger import PROMPT as RETRIEVE_JUDGER_PROMPT\n\n__all__ = [\n    \"CATEGORY_SUMMARY_PROMPT\",\n    \"DEFAULT_MEMORY_TYPES\",\n    \"MEMORY_TYPE_PROMPTS\",\n    \"PREPROCESS_PROMPTS\",\n    \"RETRIEVE_JUDGER_PROMPT\",\n]\n"
  },
  {
    "path": "src/memu/prompts/category_patch/__init__.py",
    "content": "from __future__ import annotations\n\nfrom memu.prompts.category_patch.category import PROMPT\n\nCATEGORY_PATCH_PROMPT = PROMPT.strip()\n\n__all__ = [\"CATEGORY_PATCH_PROMPT\"]\n"
  },
  {
    "path": "src/memu/prompts/category_patch/category.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nYour task is to read an existing user profile and an update related to a specific memory topic, then determine whether the profile needs to be updated.\nIf an update is required, you must generate the updated version of the profile based on the rules below.\n\n# Workflow\n1. Understand the Topic\nFocus only on memories relevant to the specified Topic.\n\n2. Analyze Original Content\nReview the existing profile content enclosed in <content>...</content>.\n\n3. Analyze Update\nDetermine whether the update represents:\n- A new memory\n- A variation of an existing memory\n- A discarded (invalidated) memory\n\n4. Decision Making\nJudge whether the profile requires modification based on relevance and importance.\n\n5. Generate Output\n- If an update is required, produce the revised profile content.\n- If not, explicitly indicate that no update is needed.\n\n\n# Response Format (JSON):\n{{\n    \"need_update\": [bool, whether the profile needs to be updated]\n    \"updated_content\": [str, the updated content of the profile if need_update is true, otherwise empty]\n}}\n\n\n# Input\nTopic:\n{category}\n\nOriginal content:\n<content>\n{original_content}\n</content>\n\nUpdate:\n{update_content}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/category_summary/__init__.py",
    "content": "from __future__ import annotations\n\nfrom memu.prompts.category_summary.category import CUSTOM_PROMPT, PROMPT\nfrom memu.prompts.category_summary.category_with_refs import CUSTOM_PROMPT as CUSTOM_PROMPT_WITH_REFS\nfrom memu.prompts.category_summary.category_with_refs import PROMPT as PROMPT_WITH_REFS\n\nDEFAULT_CATEGORY_SUMMARY_PROMPT_ORDINAL: dict[str, int] = {\n    \"objective\": 10,\n    \"workflow\": 20,\n    \"rules\": 30,\n    \"output\": 40,\n    \"examples\": 50,\n    \"input\": 90,\n}\n\n__all__ = [\n    \"CUSTOM_PROMPT\",\n    \"CUSTOM_PROMPT_WITH_REFS\",\n    \"DEFAULT_CATEGORY_SUMMARY_PROMPT_ORDINAL\",\n    \"PROMPT\",\n    \"PROMPT_WITH_REFS\",\n]\n"
  },
  {
    "path": "src/memu/prompts/category_summary/category.py",
    "content": "PROMPT_LEGACY = \"\"\"\n# Task Objective\nYou are a professional User Profile Synchronization Specialist. Your core objective is to accurately merge newly extracted user information items into the user's initial profile using only two operations: add and update.\nBecause no original conversation text is provided, active deletion is not allowed; only implicit replacement through newer items is permitted. The final output must be the updated, complete user profile.\n\n# Workflow\n## Step 1: Preprocessing & Parsing\n- Input sources\nUser Initial Profile: structured, categorized, confirmed long-term user information.\nNewly Extracted User Information Items.\n- Structure parsing\nInitial profile: extract categories and core content; preserve original wording style and format; build a category-content mapping.\nNew items: validate completeness and category correctness; mark each as Add or Update; distinguish stable facts from event-type information; extract dates/times (events only).\n- Pre-validation\nVerify subject accuracy: clearly distinguish the user from related persons (family, friends, etc.).\nRemove invalid items: vague, miscategorized, or non-user-information items.\nRemove one-off events: temporary actions without long-term relevance (e.g., what the user ate today).\n\n## Step 2: Core Operations (Update / Add)\nA. Update\nConflict detection: compare new items with existing ones in the same category for semantic overlap (e.g., age update).\nValidity priority: retain information that is more specific, clearer, and more certain.\nOverwrite / supplement: replace outdated entries with new ones, ensuring no loss of core information.\nTime integration (events only): retain dates/times and integrate them naturally; multiple events at the same time may be layered, but each entry must remain independently understandable.\nB. Add\nDeduplication check: ensure the new item is not identical or semantically similar to existing or updated items.\nCategory matching: place the item into the correct predefined category.\nInsertion: add the item following the original profile's language and formatting style, concise and clear.\n\n## Step 3: Merge & Formatting\nStructured ordering: present content by category order; omit empty categories.\nFormatting rules: strictly use Markdown (# for main title, ## for category titles).\nFinal validation\nConsistency: no contradictions or duplicates.\nCompliance: correct categories only; no explanatory or operational text.\nAccuracy: subject clarity; natural time embedding; proper format.\n\n\n## Step 4: Summarize\nTarget length: {target_length}\nSummarize the updated user markdown profile to the target length.\nUse Markdown hierarchy.\nDo not include explanations, operation traces, or meta text.\nControl item length strictly; prioritize core information if needed.\n\n## Step 5: Output\nOutput only the updated user markdown profile.\nUse Markdown hierarchy.\nDo not include explanations, operation traces, or meta text.\nControl item length strictly; prioritize core information if needed.\n\n\n\n# Output Format (Markdown)\n```markdown\n# {category}\n## <category name>\n- User information item\n- User information item\n...\n## <category name>\n- User information item\n- User information item\n...\n```\n\n# Examples (Input / Output / Explanation)\n- Example 1: Basic Add & Update\n\n\nTopic:\nPersonal Basic Information\n\nOriginal content:\n<content>\n# Personal Basic Information\n## Basic Information\n- The user is 28 years old\n- The user currently lives in Beijing\n## Basic Preferences\n- The user likes spicy food\n## Core Traits\n- The user is extroverted\n</content>\n\nNew memory items:\n<item>\n- The user is 30 years old\n- The user currently lives in Shanghai\n- The user prefers Sichuan-style spicy food and dislikes sweet-spicy flavors\n- The user enjoys hiking on weekends\n- The user is meticulous\n- The user ate Malatang today\n</item>\n\nOutput\n# Personal Basic Information\n## Basic Information\n- The user is 30 years old\n- The user currently lives in Shanghai\n## Basic Preferences\n- The user prefers Sichuan-style spicy food and dislikes sweet-spicy flavors\n- The user enjoys hiking on weekends\n## Core Traits\n- The user is extroverted\n- The user is meticulous\n\nExplanation\nThe \"The user ate Malatang today\" is a one-time daily action without long-term value and is therefore excluded.\n\n\nYour task is to read and analyze existing content and some new memory items, and then selectively update the content to reflect both the existing and new information.\n\n\n# Input\n\nTopic:\n{category}\n\nOriginal content:\n<content>\n{original_content}\n</content>\n\nNew memory items:\n<item>\n{new_memory_items_text}\n</item>\n\n\n# Output format (Markdown)\n```markdown\n# {category}\n## <category name>\n- User information item\n- User information item\n...\n## <category name>\n- User information item\n- User information item\n...\n```\n\"\"\"\n\n\nPROMPT_BLOCK_OBJECTIVE = \"\"\"\n# Task Objective\nYou are a professional User Profile Synchronization Specialist. Your core objective is to accurately merge newly extracted user information items into the user's initial profile using only two operations: add and update.\nBecause no original conversation text is provided, active deletion is not allowed; only implicit replacement through newer items is permitted. The final output must be the updated, complete user profile.\n\"\"\"\n\nPROMPT_BLOCK_WORKFLOW = \"\"\"\n# Workflow\n## Step 1: Preprocessing & Parsing\n- Input sources\nUser Initial Profile: structured, categorized, confirmed long-term user information.\nNewly Extracted User Information Items.\n- Structure parsing\nInitial profile: extract categories and core content; preserve original wording style and format; build a category-content mapping.\nNew items: validate completeness and category correctness; mark each as Add or Update; distinguish stable facts from event-type information; extract dates/times (events only).\n- Pre-validation\nVerify subject accuracy: clearly distinguish the user from related persons (family, friends, etc.).\nRemove invalid items: vague, miscategorized, or non-user-information items.\nRemove one-off events: temporary actions without long-term relevance (e.g., what the user ate today).\n\n## Step 2: Core Operations (Update / Add)\nA. Update\nConflict detection: compare new items with existing ones in the same category for semantic overlap (e.g., age update).\nValidity priority: retain information that is more specific, clearer, and more certain.\nOverwrite / supplement: replace outdated entries with new ones, ensuring no loss of core information.\nTime integration (events only): retain dates/times and integrate them naturally; multiple events at the same time may be layered, but each entry must remain independently understandable.\nB. Add\nDeduplication check: ensure the new item is not identical or semantically similar to existing or updated items.\nCategory matching: place the item into the correct predefined category.\nInsertion: add the item following the original profile's language and formatting style, concise and clear.\n\n## Step 3: Merge & Formatting\nStructured ordering: present content by category order; omit empty categories.\nFormatting rules: strictly use Markdown (# for main title, ## for category titles).\nFinal validation\nConsistency: no contradictions or duplicates.\nCompliance: correct categories only; no explanatory or operational text.\nAccuracy: subject clarity; natural time embedding; proper format.\n\n\n## Step 4: Summarize\nTarget length: {target_length}\nSummarize the updated user markdown profile to the target length.\nUse Markdown hierarchy.\nCluster the memory items and update the <category name>.\n\n## Step 5: Output\nOutput only the updated user markdown profile.\nUse Markdown hierarchy.\nDo not include explanations, operation traces, or meta text.\nControl item length strictly; prioritize core information if needed.\n\"\"\"\n\nPROMPT_BLOCK_RULES = \"\"\"\n\"\"\"\n\nPROMPT_BLOCK_OUTPUT = \"\"\"\n# Output Format (Markdown)\n```markdown\n# {category}\n## <category name>\n- User information item\n- User information item\n## <category name>\n- User information item\n- User information item\n```\n\n# Critical\nAlways ensure that your output does not exceed {target_length} tokens.\nYou may merge or omit unimportant information to meet this limit.\n\"\"\"\n\nPROMPT_BLOCK_EXAMPLES = \"\"\"\n# Examples (Input / Output / Explanation)\n- Example 1: Basic Add & Update\n\nTopic:\nPersonal Basic Information\n\nOriginal content:\n<content>\n# Personal Basic Information\n## Basic Information\n- The user is 28 years old\n- The user currently lives in Beijing\n## Basic Preferences\n- The user likes spicy food\n## Core Traits\n- The user is extroverted\n</content>\n\nNew memory items:\n<item>\n- The user is 30 years old\n- The user currently lives in Shanghai\n- The user prefers Sichuan-style spicy food and dislikes sweet-spicy flavors\n- The user enjoys hiking on weekends\n- The user is meticulous\n- The user ate Malatang today\n</item>\n\nOutput\n# Personal Basic Information\n## Basic Information\n- The user is 30 years old\n- The user currently lives in Shanghai\n## Basic Preferences\n- The user prefers Sichuan-style spicy food and dislikes sweet-spicy flavors\n- The user enjoys hiking on weekends\n## Core Traits\n- The user is extroverted\n- The user is meticulous\n\nExplanation\nThe \"The user ate Malatang today\" is a one-time daily action without long-term value and is therefore excluded.\n\"\"\"\n\nPROMPT_BLOCK_INPUT = \"\"\"\n# Input\nTopic:\n{category}\n\nOriginal content:\n<content>\n{original_content}\n</content>\n\nNew memory items:\n<item>\n{new_memory_items_text}\n</item>\n\"\"\"\n\nPROMPT = \"\\n\\n\".join([\n    PROMPT_BLOCK_OBJECTIVE.strip(),\n    PROMPT_BLOCK_WORKFLOW.strip(),\n    PROMPT_BLOCK_RULES.strip(),\n    PROMPT_BLOCK_OUTPUT.strip(),\n    PROMPT_BLOCK_EXAMPLES.strip(),\n    PROMPT_BLOCK_INPUT.strip(),\n])\n\nCUSTOM_PROMPT = {\n    \"objective\": PROMPT_BLOCK_OBJECTIVE.strip(),\n    \"workflow\": PROMPT_BLOCK_WORKFLOW.strip(),\n    \"rules\": PROMPT_BLOCK_RULES.strip(),\n    \"output\": PROMPT_BLOCK_OUTPUT.strip(),\n    \"examples\": PROMPT_BLOCK_EXAMPLES.strip(),\n    \"input\": PROMPT_BLOCK_INPUT.strip(),\n}\n"
  },
  {
    "path": "src/memu/prompts/category_summary/category_with_refs.py",
    "content": "\"\"\"\nCategory summary prompt with inline references to memory items.\n\nThis prompt instructs the LLM to include [ref:ITEM_ID] citations\nwhen summarizing category content, linking statements to their\nsource memory items.\n\"\"\"\n\nPROMPT_BLOCK_OBJECTIVE = \"\"\"\n# Task Objective\nYou are a professional User Profile Synchronization Specialist. Your core objective is to accurately merge newly extracted user information items into the user's initial profile using only two operations: add and update.\n\nIMPORTANT: You must include inline references to source memory items using the format [ref:ITEM_ID] when incorporating information from the provided memory items. This creates a traceable link between summary statements and their sources.\n\"\"\"\n\nPROMPT_BLOCK_WORKFLOW = \"\"\"\n# Workflow\n## Step 1: Preprocessing & Parsing\n- Input sources:\n  - User Initial Profile: structured, categorized, confirmed long-term user information.\n  - Newly Extracted User Information Items: each item has an ID that MUST be referenced.\n- Structure parsing:\n  - Initial profile: extract categories and core content; preserve original wording style and format.\n  - New items: note the item ID for each piece of information to include as [ref:ID].\n\n## Step 2: Core Operations (Update / Add)\nA. Update\n- When updating existing information with new data, add the reference: \"User is 30 years old [ref:item_abc123]\"\n- If multiple items support the same fact, include multiple refs: [ref:id1,id2]\n\nB. Add\n- When adding new information, always include the source reference\n- Format: \"User enjoys hiking on weekends [ref:item_xyz789]\"\n\n## Step 3: Merge & Formatting\n- Structured ordering: present content by category order; omit empty categories.\n- Formatting rules: strictly use Markdown (# for main title, ## for category titles).\n- References: ensure every new or updated fact has at least one [ref:ITEM_ID] citation.\n\n## Step 4: Summarize\nTarget length: {target_length}\n- Summarize the updated user markdown profile to the target length.\n- PRESERVE all [ref:ITEM_ID] citations in the summary.\n- Use Markdown hierarchy.\n\n## Step 5: Output\n- Output only the updated user markdown profile with inline references.\n- Use Markdown hierarchy.\n- Do not include explanations, operation traces, or meta text.\n\"\"\"\n\nPROMPT_BLOCK_RULES = \"\"\"\n# Reference Rules\n1. Every piece of information from new memory items MUST have a [ref:ITEM_ID] citation\n2. Use the exact item ID provided in the input\n3. Place references immediately after the relevant statement\n4. Multiple sources can be cited: [ref:id1,id2]\n5. Existing information without new updates does not need references\n\"\"\"\n\nPROMPT_BLOCK_OUTPUT = \"\"\"\n# Output Format (Markdown with References)\n```markdown\n# {category}\n## <category name>\n- User information item [ref:ITEM_ID]\n- User information item [ref:ITEM_ID]\n## <category name>\n- User information item [ref:ITEM_ID,ITEM_ID2]\n```\n\n# Critical\n- Always ensure that your output does not exceed {target_length} tokens.\n- ALWAYS include [ref:ITEM_ID] for information from new memory items.\n- You may merge or omit unimportant information to meet the length limit.\n\"\"\"\n\nPROMPT_BLOCK_EXAMPLES = \"\"\"\n# Examples (Input / Output)\n\nTopic:\nPersonal Basic Information\n\nOriginal content:\n<content>\n# Personal Basic Information\n## Basic Information\n- The user is 28 years old\n- The user currently lives in Beijing\n</content>\n\nNew memory items with IDs:\n<items>\n- [item_a1b2c3] The user is 30 years old\n- [item_d4e5f6] The user currently lives in Shanghai\n- [item_g7h8i9] The user prefers Sichuan-style spicy food\n</items>\n\nOutput:\n# Personal Basic Information\n## Basic Information\n- The user is 30 years old [ref:item_a1b2c3]\n- The user currently lives in Shanghai [ref:item_d4e5f6]\n## Basic Preferences\n- The user prefers Sichuan-style spicy food [ref:item_g7h8i9]\n\"\"\"\n\nPROMPT_BLOCK_INPUT = \"\"\"\n# Input\nTopic:\n{category}\n\nOriginal content:\n<content>\n{original_content}\n</content>\n\nNew memory items with IDs:\n<items>\n{new_memory_items_text}\n</items>\n\"\"\"\n\nPROMPT = \"\\n\\n\".join([\n    PROMPT_BLOCK_OBJECTIVE.strip(),\n    PROMPT_BLOCK_WORKFLOW.strip(),\n    PROMPT_BLOCK_RULES.strip(),\n    PROMPT_BLOCK_OUTPUT.strip(),\n    PROMPT_BLOCK_EXAMPLES.strip(),\n    PROMPT_BLOCK_INPUT.strip(),\n])\n\nCUSTOM_PROMPT = {\n    \"objective\": PROMPT_BLOCK_OBJECTIVE.strip(),\n    \"workflow\": PROMPT_BLOCK_WORKFLOW.strip(),\n    \"rules\": PROMPT_BLOCK_RULES.strip(),\n    \"output\": PROMPT_BLOCK_OUTPUT.strip(),\n    \"examples\": PROMPT_BLOCK_EXAMPLES.strip(),\n    \"input\": PROMPT_BLOCK_INPUT.strip(),\n}\n"
  },
  {
    "path": "src/memu/prompts/memory_type/__init__.py",
    "content": "from memu.prompts.memory_type import behavior, event, knowledge, profile, skill, tool\n\n# DEFAULT_MEMORY_TYPES: list[str] = [\"profile\", \"event\", \"knowledge\", \"behavior\"]\nDEFAULT_MEMORY_TYPES: list[str] = [\"profile\", \"event\"]\n\nPROMPTS: dict[str, str] = {\n    \"profile\": profile.PROMPT.strip(),\n    \"event\": event.PROMPT.strip(),\n    \"knowledge\": knowledge.PROMPT.strip(),\n    \"behavior\": behavior.PROMPT.strip(),\n    \"skill\": skill.PROMPT.strip(),\n    \"tool\": tool.PROMPT.strip(),\n}\n\nCUSTOM_PROMPTS: dict[str, dict[str, str]] = {\n    \"profile\": profile.CUSTOM_PROMPT,\n    \"event\": event.CUSTOM_PROMPT,\n    \"knowledge\": knowledge.CUSTOM_PROMPT,\n    \"behavior\": behavior.CUSTOM_PROMPT,\n    \"skill\": skill.CUSTOM_PROMPT,\n    \"tool\": tool.CUSTOM_PROMPT,\n}\n\nCUSTOM_TYPE_CUSTOM_PROMPTS: dict[str, str] = {\n    \"category\": profile.CUSTOM_PROMPT[\"category\"],\n    \"output\": profile.CUSTOM_PROMPT[\"output\"],\n    \"input\": profile.CUSTOM_PROMPT[\"input\"],\n}\n\nDEFAULT_MEMORY_CUSTOM_PROMPT_ORDINAL: dict[str, int] = {\n    \"objective\": 10,\n    \"workflow\": 20,\n    \"rules\": 30,\n    \"category\": 40,\n    \"output\": 50,\n    \"examples\": 60,\n    \"input\": 90,\n}\n\n__all__ = [\n    \"CUSTOM_PROMPTS\",\n    \"CUSTOM_TYPE_CUSTOM_PROMPTS\",\n    \"DEFAULT_MEMORY_CUSTOM_PROMPT_ORDINAL\",\n    \"DEFAULT_MEMORY_TYPES\",\n    \"PROMPTS\",\n]\n"
  },
  {
    "path": "src/memu/prompts/memory_type/behavior.py",
    "content": "PROMPT_LEGACY = \"\"\"\nYour task is to read and understand the resource content between the user and the assistant, and, based on the given memory categories, extract behavioral patterns, routines, and solutions about the user.\n\n## Original Resource:\n<resource>\n{resource}\n</resource>\n\n## Memory Categories:\n{categories_str}\n\n## Critical Requirements:\nThe core extraction target is behavioral memory items that record patterns, routines, and solutions characterizing how the user acts or behaves to solve specific problems.\n\n## Memory Item Requirements:\n- Use the same language as the resource in <resource></resource>.\n- Extract patterns of behavior, routines, and solutions\n- Focus on how the user typically acts, their preferences, and regular activities\n- Each item can be either a single sentence concisely describing the pattern, routine, or solution, or a multi-line record with each line recording a specific step of the pattern, routine, or solution.\n- Only extract meaningful behaviors, skip one-time actions unless significant\n- Return empty array if no meaningful behaviors found\n\n## About Memory Categories:\n- You can put identical or similar memory items into multiple memory categories.\n- Do not create new memory categories. Please only generate in the given memory categories.\n- The given memory categories may only cover part of the resource's topic and content. You don't need to summarize resource's content unrelated to the given memory categories.\n- If the resource does not contain information relevant to a particular memory category, You can ignore that category and avoid forcing weakly related memory items into it. Simply skip that memory category and DO NOT output contents like \"no relevant memory item\".\n\n## Memory Item Content Requirements:\n- Single line plain text, no format, index, or Markdown.\n- If the original resource contains emojis or other special characters, ignore them and output in plain text.\n- *ALWAYS* use the same language as the resource.\n\n# Response Format (JSON):\n{{\n    \"memories_items\": [\n        {{\n            \"content\": \"the content of the memory item\",\n            \"categories\": [list of memory categories that this memory item should belongs to, can be empty]\n        }}\n    ]\n}}\n\"\"\"\n\nPROMPT_BLOCK_OBJECTIVE = \"\"\"\n# Task Objective\nYou are a professional User Memory Extractor. Your core task is to extract behavioral patterns, routines, and solutions that characterize how the user acts or behaves to solve specific problems.\n\"\"\"\n\nPROMPT_BLOCK_WORKFLOW = \"\"\"\n# Workflow\nRead the full conversation to understand topics and meanings.\n## Extract memories\nSelect turns that contain valuable Behavior Information and extract behavioral memory items.\n## Review & validate\nMerge semantically similar items.\nResolve contradictions by keeping the latest / most certain item.\n## Final output\nOutput Behavior Information.\n\"\"\"\n\nPROMPT_BLOCK_RULES = \"\"\"\n# Rules\n## General requirements (must satisfy all)\n- Use \"user\" to refer to the user consistently.\n- Each memory item must be complete and self-contained, written as a declarative descriptive sentence.\n- Each memory item must express one single complete piece of information and be understandable without context.\n- Similar/redundant items must be merged into one, and assigned to only one category.\n- Each memory item must be < 50 words worth of length (keep it concise but include relevant details).\n- Focus on patterns of behavior, routines, and solutions.\n- Focus on how the user typically acts, their preferences, and regular activities.\n- Can include multi-line records with each line describing a specific step of the pattern, routine, or solution.\nImportant: Extract only behaviors directly stated or confirmed by the user. No guesses, no suggestions, and no content introduced only by the assistant.\nImportant: Accurately reflect whether the subject is the user or someone around the user.\n\n## Special rules for Behavior Information\n- One-time actions or specific events are forbidden in Behavior Information unless they demonstrate a significant pattern.\n- Focus on recurring patterns, typical approaches, and established routines.\n- Do not extract content that was obtained only through the model's follow-up questions unless the user shows strong proactive intent.\n\n## Forbidden content\n- Knowledge Q&A without a clear user behavior pattern.\n- One-time events that do not reflect recurring behavior.\n- Turns where the user did not respond and only the assistant spoke.\n- Illegal / harmful sensitive topics (violence, politics, drugs, etc.).\n- Private financial accounts, IDs, addresses, military/defense/government job details, precise street addresses—unless explicitly requested by the user (still avoid if not necessary).\n- Any content mentioned only by the assistant and not explicitly confirmed by the user.\n\n## Review & validation rules\n- Merge similar items: keep only one and assign a single category.\n- Resolve conflicts: keep the latest / most certain item.\n- Final check: every item must comply with all extraction rules.\n\"\"\"\n\nPROMPT_BLOCK_CATEGORY = \"\"\"\n## Memory Categories:\n{categories_str}\n\"\"\"\n\nPROMPT_BLOCK_OUTPUT = \"\"\"\n# Output Format (XML)\nReturn all memories wrapped in a single <item> element:\n<item>\n    <memory>\n        <content>Behavior memory item content 1</content>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>Behavior memory item content 2</content>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n</item>\n\"\"\"\n\nPROMPT_BLOCK_EXAMPLES = \"\"\"\n# Examples (Input / Output / Explanation)\nExample 1: Behavior Information Extraction\n## Input\nuser: Hi, are you busy? I just got off work and I'm going to the supermarket to buy some groceries.\nassistant: Not busy. Are you cooking for yourself?\nuser: Yes. It's healthier. I work as a product manager in an internet company. I'm 30 this year. After work I like experimenting with cooking, I often figure out dishes by myself.\nassistant: Being a PM is tough. You're so disciplined to cook at 30!\nuser: It's fine. Cooking relaxes me. It's better than takeout. Also I'm traveling next weekend.\nassistant: You can check the weather ahead. Your sunscreen can finally be used.\nuser: I haven't started packing yet. It's annoying.\n## Output\n<item>\n    <memory>\n        <content>The user typically cooks for themselves after work instead of ordering takeout</content>\n        <categories>\n            <category>Daily Routine</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>The user often experiments with cooking and figures out dishes by themselves</content>\n        <categories>\n            <category>Daily Routine</category>\n        </categories>\n    </memory>\n</item>\n## Explanation\nOnly behavioral patterns explicitly stated by the user are extracted.\nCooking after work and experimenting with dishes are recurring behaviors/routines.\nUser's job, age are stable traits (not behaviors). The travel plan is a one-time event, not a behavioral pattern.\n\"\"\"\n\nPROMPT_BLOCK_INPUT = \"\"\"\n# Original Resource:\n<resource>\n{resource}\n</resource>\n\"\"\"\n\nPROMPT = \"\\n\\n\".join([\n    PROMPT_BLOCK_OBJECTIVE.strip(),\n    PROMPT_BLOCK_WORKFLOW.strip(),\n    PROMPT_BLOCK_RULES.strip(),\n    PROMPT_BLOCK_CATEGORY.strip(),\n    PROMPT_BLOCK_OUTPUT.strip(),\n    PROMPT_BLOCK_EXAMPLES.strip(),\n    PROMPT_BLOCK_INPUT.strip(),\n])\n\nCUSTOM_PROMPT = {\n    \"objective\": PROMPT_BLOCK_OBJECTIVE.strip(),\n    \"workflow\": PROMPT_BLOCK_WORKFLOW.strip(),\n    \"rules\": PROMPT_BLOCK_RULES.strip(),\n    \"category\": PROMPT_BLOCK_CATEGORY.strip(),\n    \"output\": PROMPT_BLOCK_OUTPUT.strip(),\n    \"examples\": PROMPT_BLOCK_EXAMPLES.strip(),\n    \"input\": PROMPT_BLOCK_INPUT.strip(),\n}\n"
  },
  {
    "path": "src/memu/prompts/memory_type/event.py",
    "content": "PROMPT_LEGACY = \"\"\"\nYour task is to read and understand the resource content between the user and the assistant, and, based on the given memory categories, extract specific events and experiences that happened to or involved the user.\n\n## Original Resource:\n<resource>\n{resource}\n</resource>\n\n## Memory Categories:\n{categories_str}\n\n## Critical Requirements:\nThe core extraction target is eventful memory items about specific events, experiences, and occurrences that happened at a particular time and involve the user.\n\n## Memory Item Requirements:\n- Use the same language as the resource in <resource></resource>.\n- Each memory item should be complete and standalone.\n- Each memory item should express a complete piece of information, and is understandable without context and reading other memory items.\n- Always use declarative and descriptive sentences.\n- Use \"the user\" (or that in the target language, e.g., \"用户\") to refer to the user.\n- Focus on specific events that happened at a particular time or period.\n- Extract concrete happenings, activities, and experiences.\n- Include relevant details such as time, location, and participants where available.\n- Carefully judge whether an event is narrated by the user or the assistant. You should only extract memory items for events directly narrated or confirmed by the user.\n- DO NOT include behavioral patterns, habits, or factual knowledge.\n- DO NOT record temporary, ephemeral situations or trivial daily activities unless significant.\n\n## Example (good):\n- The user and his family went on a hike at a nature park outside the city last weekend. They had a picnic there, and had a great time.\n\n## Example (bad):\n- The user went on a hike. (The time, place, and people are missing.)\n- They had a great time. (The reference to \"they\" is unclear and does not constitute a self-contained memory item.)\n\n## About Memory Categories:\n- You can put identical or similar memory items into multiple memory categories.\n- Do not create new memory categories. Please only generate in the given memory categories.\n- The given memory categories may only cover part of the resource's topic and content. You don't need to summarize resource's content unrelated to the given memory categories.\n- If the resource does not contain information relevant to a particular memory category, You can ignore that category and avoid forcing weakly related memory items into it. Simply skip that memory category and DO NOT output contents like \"no relevant memory item\".\n\n## Memory Item Content Requirements:\n- Single line plain text, no format, index, or Markdown.\n- If the original resource contains emojis or other special characters, ignore them and output in plain text.\n- *ALWAYS* use the same language as the resource.\n\n# Response Format (JSON):\n{{\n    \"memories_items\": [\n        {{\n            \"content\": \"the content of the memory item\",\n            \"categories\": [list of memory categories that this memory item should belongs to, can be empty]\n        }}\n    ]\n}}\n\"\"\"\n\nPROMPT_BLOCK_OBJECTIVE = \"\"\"\n# Task Objective\nYou are a professional User Memory Extractor. Your core task is to extract specific events and experiences that happened to or involved the user (e.g., activities, occurrences, experiences at particular times).\n\"\"\"\n\nPROMPT_BLOCK_WORKFLOW = \"\"\"\n# Workflow\nRead the full conversation to understand topics and meanings.\n## Extract memories\nSelect turns that contain valuable Event Information and extract event memory items.\n## Review & validate\nMerge semantically similar items.\nResolve contradictions by keeping the latest / most certain item.\n## Final output\nOutput Event Information.\n\"\"\"\n\nPROMPT_BLOCK_RULES = \"\"\"\n# Rules\n## General requirements (must satisfy all)\n- Use \"user\" to refer to the user consistently.\n- Each memory item must be complete and self-contained, written as a declarative descriptive sentence.\n- Each memory item must express one single complete piece of information and be understandable without context.\n- Similar/redundant items must be merged into one, and assigned to only one category.\n- Each memory item must be < 50 words worth of length (keep it concise but include relevant details).\n- Focus on specific events that happened at a particular time or period.\n- Include relevant details such as time, location, and participants where available.\nImportant: Extract only events directly stated or confirmed by the user. No guesses, no suggestions, and no content introduced only by the assistant.\nImportant: Accurately reflect whether the subject is the user or someone around the user.\n\n## Special rules for Event Information\n- Behavioral patterns, habits, preferences, or factual knowledge are forbidden in Event Information.\n- Focus on concrete happenings, activities, and experiences.\n- Do not extract content that was obtained only through the model's follow-up questions unless the user shows strong proactive intent.\n\n## Forbidden content\n- Knowledge Q&A without a clear user event.\n- Trivial daily activities unless significant (e.g., routine meals, commuting).\n- Temporary, ephemeral situations that lack meaningful significance.\n- Turns where the user did not respond and only the assistant spoke.\n- Illegal / harmful sensitive topics (violence, politics, drugs, etc.).\n- Private financial accounts, IDs, addresses, military/defense/government job details, precise street addresses—unless explicitly requested by the user (still avoid if not necessary).\n- Any content mentioned only by the assistant and not explicitly confirmed by the user.\n\n## Review & validation rules\n- Merge similar items: keep only one and assign a single category.\n- Resolve conflicts: keep the latest / most certain item.\n- Final check: every item must comply with all extraction rules.\n\"\"\"\n\nPROMPT_BLOCK_CATEGORY = \"\"\"\n## Memory Categories:\n{categories_str}\n\"\"\"\n\nPROMPT_BLOCK_OUTPUT = \"\"\"\n# Output Format (XML)\nReturn all memories wrapped in a single <item> element:\n<item>\n    <memory>\n        <content>Event memory item content 1</content>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>Event memory item content 2</content>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n</item>\n\"\"\"\n\nPROMPT_BLOCK_EXAMPLES = \"\"\"\n# Examples (Input / Output / Explanation)\nExample 1: Event Information Extraction\n## Input\nuser: Hi, are you busy? I just got off work and I'm going to the supermarket to buy some groceries.\nassistant: Not busy. Are you cooking for yourself?\nuser: Yes. It's healthier. I work as a product manager in an internet company. I'm 30 this year. After work I like experimenting with cooking, I often figure out dishes by myself.\nassistant: Being a PM is tough. You're so disciplined to cook at 30!\nuser: It's fine. Cooking relaxes me. It's better than takeout. Also I'm traveling next weekend.\nassistant: You can check the weather ahead. Your sunscreen can finally be used.\nuser: I haven't started packing yet. It's annoying.\n## Output\n<item>\n    <memory>\n        <content>The user is planning a trip next weekend and hasn't started packing yet</content>\n        <categories>\n            <category>Travel</category>\n        </categories>\n    </memory>\n</item>\n## Explanation\nOnly specific events explicitly stated by the user are extracted.\nThe travel plan is an event with a specific time reference (next weekend).\nUser's job, age, and cooking habits are stable user traits, so they are not extracted as Event Information.\n\"\"\"\n\nPROMPT_BLOCK_INPUT = \"\"\"\n# Original Resource:\n<resource>\n{resource}\n</resource>\n\"\"\"\n\nPROMPT = \"\\n\\n\".join([\n    PROMPT_BLOCK_OBJECTIVE.strip(),\n    PROMPT_BLOCK_WORKFLOW.strip(),\n    PROMPT_BLOCK_RULES.strip(),\n    PROMPT_BLOCK_CATEGORY.strip(),\n    PROMPT_BLOCK_OUTPUT.strip(),\n    PROMPT_BLOCK_EXAMPLES.strip(),\n    PROMPT_BLOCK_INPUT.strip(),\n])\n\nCUSTOM_PROMPT = {\n    \"objective\": PROMPT_BLOCK_OBJECTIVE.strip(),\n    \"workflow\": PROMPT_BLOCK_WORKFLOW.strip(),\n    \"rules\": PROMPT_BLOCK_RULES.strip(),\n    \"category\": PROMPT_BLOCK_CATEGORY.strip(),\n    \"output\": PROMPT_BLOCK_OUTPUT.strip(),\n    \"examples\": PROMPT_BLOCK_EXAMPLES.strip(),\n    \"input\": PROMPT_BLOCK_INPUT.strip(),\n}\n"
  },
  {
    "path": "src/memu/prompts/memory_type/knowledge.py",
    "content": "PROMPT_LEGACY = \"\"\"\nYour task is to read and understand the resource content between the user and the assistant, and, based on the given memory categories, extract knowledge and information that the user learned or discussed.\n\n## Original Resource:\n<resource>\n{resource}\n</resource>\n\n## Memory Categories:\n{categories_str}\n\n## Critical Requirements:\nThe core extraction target is factual memory items that reflect knowledge, concepts, definitions, and factual information that the resource content suggests.\n\n## Memory Item Requirements:\n- Use the same language as the resource in <resource></resource>.\n- Each memory item should be complete and standalone.\n- Each memory item should express a complete piece of information, and is understandable without context and reading other memory items.\n- Extract factual knowledge, concepts, definitions, and explanations\n- Focus on objective information that can be learned or referenced\n- Each item should be a descriptive sentence.\n- Only extract meaningful knowledge, skip opinions or personal experiences\n- Return empty array if no meaningful knowledge found\n\n## About Memory Categories:\n- You can put identical or similar memory items into multiple memory categories.\n- Do not create new memory categories. Please only generate in the given memory categories.\n- The given memory categories may only cover part of the resource's topic and content. You don't need to summarize resource's content unrelated to the given memory categories.\n- If the resource does not contain information relevant to a particular memory category, You can ignore that category and avoid forcing weakly related memory items into it. Simply skip that memory category and DO NOT output contents like \"no relevant memory item\".\n\n## Memory Item Content Requirements:\n- Single line plain text, no format, index, or Markdown.\n- If the original resource contains emojis or other special characters, ignore them and output in plain text.\n- *ALWAYS* use the same language as the resource.\n\n# Response Format (JSON):\n{{\n    \"memories_items\": [\n        {{\n            \"content\": \"the content of the memory item\",\n            \"categories\": [list of memory categories that this memory item should belongs to, can be empty]\n        }}\n    ]\n}}\n\"\"\"\n\nPROMPT_BLOCK_OBJECTIVE = \"\"\"\n# Task Objective\nYou are a professional User Memory Extractor. Your core task is to extract factual knowledge, concepts, definitions, and information that the user learned or discussed in the conversation.\n\"\"\"\n\nPROMPT_BLOCK_WORKFLOW = \"\"\"\n# Workflow\nRead the full conversation to understand topics and meanings.\n## Extract memories\nSelect turns that contain valuable Knowledge Information and extract knowledge memory items.\n## Review & validate\nMerge semantically similar items.\nResolve contradictions by keeping the latest / most certain item.\n## Final output\nOutput Knowledge Information.\n\"\"\"\n\nPROMPT_BLOCK_RULES = \"\"\"\n# Rules\n## General requirements (must satisfy all)\n- Each memory item must be complete and self-contained, written as a declarative descriptive sentence.\n- Each memory item must express one single complete piece of information and be understandable without context.\n- Similar/redundant items must be merged into one, and assigned to only one category.\n- Each memory item must be < 50 words worth of length (keep it concise but include relevant details).\n- Focus on factual knowledge, concepts, definitions, and explanations.\n- Focus on objective information that can be learned or referenced.\nImportant: Extract only knowledge directly stated or discussed in the conversation. No guesses or unsupported extensions.\n\n## Special rules for Knowledge Information\n- Personal opinions, subjective preferences, or personal experiences are forbidden in Knowledge Information.\n- Focus on objective facts, concepts, and explanations.\n- User-specific traits, events, or behaviors are not knowledge items.\n\n## Forbidden content\n- Opinions or subjective statements without factual basis.\n- Personal experiences or events (these belong to event type).\n- User preferences or behavioral patterns (these belong to profile/behavior type).\n- Trivial or commonly known facts that add no value.\n- Illegal / harmful sensitive topics (violence, politics, drugs, etc.).\n- Any content that is speculative or not clearly established in the conversation.\n\n## Review & validation rules\n- Merge similar items: keep only one and assign a single category.\n- Resolve conflicts: keep the latest / most certain item.\n- Final check: every item must comply with all extraction rules.\n\"\"\"\n\nPROMPT_BLOCK_CATEGORY = \"\"\"\n## Memory Categories:\n{categories_str}\n\"\"\"\n\nPROMPT_BLOCK_OUTPUT = \"\"\"\n# Output Format (XML)\nReturn all memories wrapped in a single <item> element:\n<item>\n    <memory>\n        <content>Knowledge memory item content 1</content>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>Knowledge memory item content 2</content>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n</item>\n\"\"\"\n\nPROMPT_BLOCK_EXAMPLES = \"\"\"\n# Examples (Input / Output / Explanation)\nExample 1: Knowledge Information Extraction\n## Input\nuser: I'm trying to understand how Python decorators work. Can you explain?\nassistant: A decorator is a function that takes another function and extends its behavior without modifying it. It's a form of metaprogramming.\nuser: Oh I see, so it's like wrapping a function. I heard that the @ symbol is syntactic sugar for applying decorators.\nassistant: Exactly! When you write @decorator above a function, it's equivalent to function = decorator(function).\nuser: That makes sense. By the way, I'm working on a project at my company using this.\n## Output\n<item>\n    <memory>\n        <content>In Python, a decorator is a function that takes another function and extends its behavior without modifying it</content>\n        <categories>\n            <category>Programming</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>The @ symbol in Python is syntactic sugar for applying decorators, equivalent to function = decorator(function)</content>\n        <categories>\n            <category>Programming</category>\n        </categories>\n    </memory>\n</item>\n## Explanation\nOnly factual knowledge discussed and confirmed in the conversation is extracted.\nThe user's work project is a personal event/situation, not knowledge, so it is not extracted.\n\"\"\"\n\nPROMPT_BLOCK_INPUT = \"\"\"\n# Original Resource:\n<resource>\n{resource}\n</resource>\n\"\"\"\n\nPROMPT = \"\\n\\n\".join([\n    PROMPT_BLOCK_OBJECTIVE.strip(),\n    PROMPT_BLOCK_WORKFLOW.strip(),\n    PROMPT_BLOCK_RULES.strip(),\n    PROMPT_BLOCK_CATEGORY.strip(),\n    PROMPT_BLOCK_OUTPUT.strip(),\n    PROMPT_BLOCK_EXAMPLES.strip(),\n    PROMPT_BLOCK_INPUT.strip(),\n])\n\nCUSTOM_PROMPT = {\n    \"objective\": PROMPT_BLOCK_OBJECTIVE.strip(),\n    \"workflow\": PROMPT_BLOCK_WORKFLOW.strip(),\n    \"rules\": PROMPT_BLOCK_RULES.strip(),\n    \"category\": PROMPT_BLOCK_CATEGORY.strip(),\n    \"output\": PROMPT_BLOCK_OUTPUT.strip(),\n    \"examples\": PROMPT_BLOCK_EXAMPLES.strip(),\n    \"input\": PROMPT_BLOCK_INPUT.strip(),\n}\n"
  },
  {
    "path": "src/memu/prompts/memory_type/profile.py",
    "content": "PROMPT_LEGACY = \"\"\"\nYour task is to read and understand the resource content between the user and the assistant, and, based on the given memory categories, extract memory items about the user.\n\n## Original Resource:\n<resource>\n{resource}\n</resource>\n\n## Memory Categories:\n{categories_str}\n\n## Critical Requirements:\nThe core extraction target is self-contained memory items about the user.\n\n## Memory Item Requirements:\n- Use the same language as the resource in <resource></resource>.\n- Each memory item should be complete and standalone.\n- Each memory item should express a complete piece of information, and is understandable without context and reading other memory items.\n- Always use declarative and descriptive sentences.\n- Use \"the user\" (or that in the target language, e.g., \"用户\") to refer to the user.\n- You can cluster multiple events that are closely related or under a single topic into a single memory item, but avoid making each single memory item too long (over 100 words).\n- **Important** Carefully judge whether an event/fact/information is narrated by the user or the assistant. You should only extract memory items for the event/fact/information directly narrated or confirmed by the user. DO NOT include any groundless conjectures, advice, suggestions, or any content provided by the assistant.\n- **Important** Carefully judge whether the subject of an event/fact/information is the user themselves or some person around the user (e.g., the user's family, friend, or the assistant), and reflect the subject correctly in the memory items.\n- **Important** DO NOT record temporary, ephemeral, or one-time situational information such as weather conditions (e.g., \"today is raining\"), current mood states, temporary technical issues, or any short-lived circumstances that are unlikely to be relevant for the user profile. Focus on meaningful, persistent information about the user's characteristics, preferences, relationships, ongoing situations, and significant events.\n\n## Example (good):\n- The user and his family went on a hike at a nature park outside the city last weekend. They had a picnic there, and had a great time.\n\n## Example (bad):\n- The user went on a hike. (The time, place, and people are missing.)\n- They had a great time. (The reference to \"they\" is unclear and does not constitute a self-contained memory item.)\n- The user and his family went on a hike at a nature park outside the city last weekend. The user and his family had a picnic at a nature park outside the city last weekend. (Should be merged.)\n\n## About Memory Categories:\n- You can put identical or similar memory items into multiple memory categories. For example, \"The user and his family went on a hike at a nature park outside the city last weekend.\" can be put into all of \"hiking\", \"weekend activities\", and \"family activities\" categories (if they exist). Nevertheless, Memory items put to each category can have different focuses.\n- Do not create new memory categories. Please only generate in the given memory categories.\n- The given memory categories may only cover part of the resource's topic and content. You don't need to summarize resource's content unrelated to the given memory categories.\n- If the resource does not contain information relevant to a particular memory category, You can ignore that category and avoid forcing weakly related memory items into it. Simply skip that memory category and DO NOT output contents like \"no relevant memory item\".\n\n## Memory Item Content Requirements:\n- Single line plain text, no format, index, or Markdown.\n- If the original resource contains emojis or other special characters, ignore them and output in plain text.\n- *ALWAYS* use the same language as the resource.\n\n# Response Format (JSON):\n{{\n    \"memories_items\": [\n        {{\n            \"content\": \"the content of the memory item\",\n            \"categories\": [list of memory categories that this memory item should belongs to, can be empty]\n        }}\n    ]\n}}\n\"\"\"\n\nPROMPT_BLOCK_OBJECTIVE = \"\"\"\n# Task Objective\nYou are a professional User Memory Extractor. Your core task is to extract independent user memory items about the user (e.g., basic info, preferences, habits, other long-term stable traits).\n\"\"\"\n\nPROMPT_BLOCK_WORKFLOW = \"\"\"\n# Workflow\nRead the full conversation to understand topics and meanings.\n## Extract memories\nSelect turns that contain valuable User Information and extract user info memory items.\n## Review & validate\nMerge semantically similar items.\nResolve contradictions by keeping the latest / most certain item.\n## Final output\nOutput User Information.\n\"\"\"\n\nPROMPT_BLOCK_RULES = \"\"\"\n# Rules\n## General requirements (must satisfy all)\n- Use \"user\" to refer to the user consistently.\n- Each memory item must be complete and self-contained, written as a declarative descriptive sentence.\n- Each memory item must express one single complete piece of information and be understandable without context.\n- Similar/redundant items must be merged into one, and assigned to only one category.\n- Each memory item must be < 30 words worth of length (keep it as concise as possible).\n- A single memory item must NOT contain timestamps.\nImportant: Extract only facts directly stated or confirmed by the user. No guesses, no suggestions, and no content introduced only by the assistant.\nImportant: Accurately reflect whether the subject is the user or someone around the user.\nImportant: Do not record temporary/one-off situational information; focus on meaningful, persistent information.\n\n## Special rules for User Information\n- Any event-related item is forbidden in User Information.\n- Do not extract content that was obtained only through the model's follow-up questions unless the user shows strong proactive intent.\n\n## Forbidden content\n- Knowledge Q&A without a clear user fact.\n- Trivial updates that do not add meaningful value (e.g., “full → too full”).\n- Turns where the user did not respond and only the assistant spoke.\n- Illegal / harmful sensitive topics (violence, politics, drugs, etc.).\n- Private financial accounts, IDs, addresses, military/defense/government job details, precise street addresses—unless explicitly requested by the user (still avoid if not necessary).\n- Any content mentioned only by the assistant and not explicitly confirmed by the user.\n\n## Review & validation rules\n- Merge similar items: keep only one and assign a single category.\n- Resolve conflicts: keep the latest / most certain item.\n- Final check: every item must comply with all extraction rules.\n\"\"\"\n\nPROMPT_BLOCK_CATEGORY = \"\"\"\n## Memory Categories:\n{categories_str}\n\"\"\"\n\nPROMPT_BLOCK_OUTPUT = \"\"\"\n# Output Format (XML)\nReturn all memories wrapped in a single <item> element:\n<item>\n    <memory>\n        <content>User memory item content 1</content>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>User memory item content 2</content>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n</item>\n\"\"\"\n\nPROMPT_BLOCK_EXAMPLES = \"\"\"\n# Examples (Input / Output / Explanation)\nExample 1: User Information Extraction\n## Input\nuser: Hi, are you busy? I just got off work and I'm going to the supermarket to buy some groceries.\nassistant: Not busy. Are you cooking for yourself?\nuser: Yes. It's healthier. I work as a product manager in an internet company. I'm 30 this year. After work I like experimenting with cooking, I often figure out dishes by myself.\nassistant: Being a PM is tough. You're so disciplined to cook at 30!\nuser: It's fine. Cooking relaxes me. It's better than takeout. Also I'm traveling next weekend.\nassistant: You can check the weather ahead. Your sunscreen can finally be used.\nuser: I haven't started packing yet. It's annoying.\n## Output\n<item>\n    <memory>\n        <content>The user works as a product manager at an internet company</content>\n        <categories>\n            <category>Basic Information</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>The user is 30 years old</content>\n        <categories>\n            <category>Basic Information</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>The user likes experimenting with cooking after work</content>\n        <categories>\n            <category>Basic Information</category>\n        </categories>\n    </memory>\n</item>\n## Explanation\nOnly stable user facts explicitly stated by the user are extracted.\nThe travel plan and packing annoyance are events/temporary states, so they are not extracted as User Information.\n\"\"\"\n\nPROMPT_BLOCK_INPUT = \"\"\"\n# Original Resource:\n<resource>\n{resource}\n</resource>\n\"\"\"\n\nPROMPT = \"\\n\\n\".join([\n    PROMPT_BLOCK_OBJECTIVE.strip(),\n    PROMPT_BLOCK_WORKFLOW.strip(),\n    PROMPT_BLOCK_RULES.strip(),\n    PROMPT_BLOCK_CATEGORY.strip(),\n    PROMPT_BLOCK_OUTPUT.strip(),\n    PROMPT_BLOCK_EXAMPLES.strip(),\n    PROMPT_BLOCK_INPUT.strip(),\n])\n\nCUSTOM_PROMPT = {\n    \"objective\": PROMPT_BLOCK_OBJECTIVE.strip(),\n    \"workflow\": PROMPT_BLOCK_WORKFLOW.strip(),\n    \"rules\": PROMPT_BLOCK_RULES.strip(),\n    \"category\": PROMPT_BLOCK_CATEGORY.strip(),\n    \"output\": PROMPT_BLOCK_OUTPUT.strip(),\n    \"examples\": PROMPT_BLOCK_EXAMPLES.strip(),\n    \"input\": PROMPT_BLOCK_INPUT.strip(),\n}\n"
  },
  {
    "path": "src/memu/prompts/memory_type/skill.py",
    "content": "PROMPT_LEGACY = \"\"\"\nYour task is to read and understand the resource content (agent logs, workflow documentation, execution traces, or technical documents), and extract skills, capabilities, and technical competencies demonstrated or described in the content. Format each skill as a comprehensive, production-ready skill profile that can be referenced and applied.\n\n## Original Resource:\n<resource>\n{resource}\n</resource>\n\n## Memory Categories:\n{categories_str}\n\n## Critical Requirements:\nExtract skill-based memory items as comprehensive skill profiles that include:\n1. **Skill Name**: Clear, memorable name for the skill\n2. **Description**: What the skill enables and when to use it\n3. **Context**: Situations where this skill was demonstrated\n4. **Core Principles**: Fundamental guidelines and best practices\n5. **Implementation Details**: Specific techniques, tools, and approaches\n6. **Success Patterns**: What works well\n7. **Common Pitfalls**: What to avoid\n\nThe core extraction target is actionable skill profiles that capture not just WHAT was done, but HOW and WHY it works.\n\n## Skill Profile Structure:\n\nFor each extracted skill, create a comprehensive profile following this template:\n\n```\n---\nname: skill-name-in-kebab-case\ndescription: One-line description of what this skill enables and when to use it\ncategory: primary-category\ndemonstrated-in: [list of contexts where this was shown]\n---\n\n[Brief introduction explaining the skill and its importance]\n\n## Core Principles\n\n[Key concepts and fundamental approaches that make this skill effective]\n\n## When to Use This Skill\n\n- Situation 1: [specific context]\n- Situation 2: [specific context]\n- [More situations as applicable]\n\n## Implementation Guide\n\n### Prerequisites\n- [Required knowledge or setup]\n\n### Techniques and Approaches\n[Detailed explanation of how to apply this skill, including:]\n- Specific methods used\n- Tools and technologies involved\n- Step-by-step process when applicable\n- Metrics to track (error rates, response times, etc.)\n\n### Example from Resource\n[Concrete example from the source material showing this skill in action, including outcomes and metrics]\n\n## Success Patterns\n\nWhat works well when applying this skill:\n- [Pattern 1 with explanation]\n- [Pattern 2 with explanation]\n- [More patterns]\n\n## Common Pitfalls\n\nWhat to avoid:\n- **[Pitfall 1]**: [Why it's a problem and how to avoid it]\n- **[Pitfall 2]**: [Why it's a problem and how to avoid it]\n- [More pitfalls based on failures or lessons learned]\n\n## Key Takeaways\n\n- [Critical insight 1]\n- [Critical insight 2]\n- [Critical insight 3]\n```\n\n## Example Skill Profiles:\n\n### Example 1: Canary Deployment\n\n```\n---\nname: canary-deployment-with-monitoring\ndescription: Implement gradual traffic shifting deployment strategy with real-time monitoring and automatic rollback capabilities\ncategory: deployment\ndemonstrated-in: [Payment Service v2.3.1 deployment]\n---\n\nCanary deployment is a risk-mitigation strategy that gradually shifts production traffic from an old version to a new version while continuously monitoring key metrics. This approach enables early detection of issues with minimal user impact.\n\n## Core Principles\n\n- **Gradual exposure**: Start with a small percentage of traffic (typically 5-10%) to limit blast radius\n- **Continuous monitoring**: Track error rates, response times, and business metrics in real-time\n- **Automated decision-making**: Use predefined thresholds to trigger automatic rollbacks\n- **Quick recovery**: Maintain ability to instantly route traffic back to stable version\n\n## When to Use This Skill\n\n- Deploying critical services where downtime is unacceptable\n- Rolling out changes with uncertain production behavior\n- High-traffic services where A/B testing production performance is valuable\n- Services with complex dependencies where integration issues may emerge gradually\n\n## Implementation Guide\n\n### Prerequisites\n- Load balancer with traffic splitting capabilities\n- Monitoring system with real-time metrics (Prometheus, Grafana)\n- Automated deployment pipeline (Jenkins, GitLab CI)\n- Health check endpoints on both versions\n\n### Techniques and Approaches\n\n1. **Initial Deployment** (10% traffic):\n   - Deploy new version alongside existing version\n   - Configure load balancer to route 10% of traffic to new version\n   - Monitor for 5-10 minutes\n\n2. **Monitoring Checkpoints**:\n   - Error rate comparison: New version should not exceed baseline by >2%\n   - Response time (p95): Should remain within 20% of baseline\n   - Business metrics: Transaction success rate, API call patterns\n\n3. **Gradual Rollout**:\n   - If metrics stable: 10% → 25% → 50% → 75% → 100%\n   - Pause 5-10 minutes between each increment\n   - Automated progression based on metric thresholds\n\n4. **Rollback Triggers**:\n   - Error rate >5%: Immediate rollback\n   - Response time degradation >50%: Investigation required\n   - Health check failures: Automatic rollback\n\n### Example from Resource\n\nPayment Service v2.3.1 deployment achieved:\n- Zero downtime during 12-minute deployment\n- Traffic progression: 10% → 50% → 100% with 2-minute pauses\n- Response time improved 15% (320ms → 270ms p95)\n- Error rate remained stable at 0.1% throughout\n- New fraud detection algorithm safely rolled out to all users\n\n## Success Patterns\n\nWhat works well:\n- **Small initial percentage**: 5-10% catches most issues while limiting impact\n- **Metric-driven automation**: Removes human error from rollback decisions\n- **Business metric monitoring**: Technical metrics alone miss some issues\n- **Communication**: Notify stakeholders about canary status\n\n## Common Pitfalls\n\nWhat to avoid:\n- **Too aggressive progression**: Rushing from 10% to 100% defeats the purpose\n- **Insufficient monitoring window**: Need 5+ minutes at each stage to detect issues\n- **Ignoring business metrics**: Technical health doesn't guarantee business success\n- **Manual rollback only**: Human reaction time too slow for critical failures\n\n## Key Takeaways\n\n- Canary deployments trade deployment speed for safety\n- Automation is critical for consistent, reliable rollbacks\n- Start small (5-10%), progress gradually, monitor continuously\n- Combine technical and business metrics for complete picture\n```\n\n### Example 2: Incident Response\n\n```\n---\nname: rapid-incident-response\ndescription: Quickly detect, diagnose, and resolve production incidents using automated monitoring and systematic troubleshooting\ncategory: incident-response\ndemonstrated-in: [User Service v3.1.0 rollback]\n---\n\nRapid incident response is the ability to quickly identify production problems, understand their root cause, and implement fixes or rollbacks to restore service. Speed and systematic approach are critical to minimizing customer impact.\n\n## Core Principles\n\n- **Fast detection**: Automated monitoring catches issues within minutes\n- **Immediate action**: Rollback first, investigate later when customer impact is high\n- **Systematic diagnosis**: Follow structured troubleshooting process\n- **Learning culture**: Every incident is an opportunity to improve\n\n## When to Use This Skill\n\n- Production errors detected by monitoring alerts\n- User-reported issues indicating service degradation\n- Automated health checks failing\n- Performance metrics exceeding thresholds\n\n## Implementation Guide\n\n### Prerequisites\n- Comprehensive monitoring (logs, metrics, traces)\n- Automated rollback capabilities\n- On-call rotation and escalation procedures\n- Incident management tools and runbooks\n\n### Techniques and Approaches\n\n1. **Detection Phase** (0-3 minutes):\n   - Automated alerts trigger from monitoring thresholds\n   - Error rate, response time, or business metric anomalies\n   - Health check failures or pod restart loops\n\n2. **Initial Response** (3-5 minutes):\n   - Assess severity: Customer-facing? Data loss risk?\n   - Decision: Rollback immediately or investigate first?\n   - High severity → Immediate rollback\n   - Low severity → Investigate with time limit\n\n3. **Rollback Execution** (2-4 minutes):\n   - Automated: Trigger rollback through deployment pipeline\n   - Manual: Revert Helm release or switch traffic to previous version\n   - Verify: Confirm metrics return to baseline\n\n4. **Root Cause Analysis** (Post-incident):\n   - Review logs, metrics, and deployment changes\n   - Identify configuration drift, missing variables, performance issues\n   - Document findings and create action items\n\n### Example from Resource\n\nUser Service v3.1.0 incident:\n- Detection: Error rate spiked 0.2% → 5.1% within 30 seconds\n- Response: Automatic rollback triggered at threshold in 2 minutes\n- Recovery: Service restored to v3.0.9, error rate normalized in 4 minutes total\n- Root cause: Missing AUTH_REFRESH_SECRET environment variable in production\n- No customer impact due to fast automated rollback\n\n## Success Patterns\n\nWhat works well:\n- **Automated thresholds**: Remove human decision-making delay\n- **Clear severity criteria**: Know when to rollback vs investigate\n- **Runbooks**: Pre-documented procedures for common issues\n- **Blameless post-mortems**: Focus on systemic improvements, not individual errors\n\n## Common Pitfalls\n\nWhat to avoid:\n- **Investigation paralysis**: Spending too long diagnosing while customers suffer\n- **Manual-only rollback**: Automation is 5-10x faster\n- **Configuration drift**: Staging and production environment inconsistency\n- **Skipping post-mortems**: Missing opportunity to prevent recurrence\n\n## Key Takeaways\n\n- Speed matters: Every minute of downtime impacts customers and business\n- Automate rollback decisions based on objective metrics\n- Rollback first, investigate second for high-severity incidents\n- Use incidents to improve systems, not blame people\n```\n\n## What NOT to Extract as Skills:\n\n❌ **Generic statements**: \"Used Docker\", \"Good at programming\"\n❌ **Opinions**: \"I think microservices are better\"\n❌ **Theory without practice**: \"Kubernetes is an orchestrator\" (that's knowledge)\n❌ **One-time luck**: \"Fixed a bug\" without approach\n❌ **Trivial actions**: \"Using email\", \"Reading docs\"\n\n✅ **DO Extract**: Concrete approaches with context, tools, metrics, and outcomes\n\n## About Memory Categories:\n- You can put identical or similar skill items into multiple memory categories.\n- Do not create new memory categories. Please only generate in the given memory categories.\n- Focus on categories like: technical_skills, work_life, knowledge, experiences\n\n## Memory Item Content Requirements:\n- *ALWAYS* use the same language as the resource in <resource></resource>.\n- Format as structured markdown with frontmatter (---, name, description, category, demonstrated-in, ---)\n- Include all sections: Core Principles, When to Use, Implementation Guide, Success Patterns, Common Pitfalls, Key Takeaways\n- Be specific and concrete - include technology names, version numbers, metrics, and outcomes\n- Each skill should be comprehensive enough to be referenced and applied independently\n- Minimum 300 words per skill to ensure depth and actionability\n- If the original resource contains emojis or other special characters, ignore them and output in plain text.\n\n## Special Instructions for Different Resource Types:\n\n### For Deployment Logs:\n- Extract each significant deployment (success or failure) as a separate skill\n- Success: Focus on techniques that worked (canary, blue-green, performance optimization)\n- Failure: Focus on incident response, root cause analysis, recovery procedures\n- Include metrics: deployment time, error rates, response times, recovery time\n\n### For Workflow Documentation:\n- Extract major workflow stages as skills (CI/CD pipeline, testing strategy, monitoring setup)\n- Include tool chains and technology stacks\n- Document step-by-step procedures\n- Note success metrics and KPIs\n\n### For Agent Execution Logs:\n- Extract problem-solving approaches as skills (competitive analysis, data processing, decision-making)\n- Include tool orchestration patterns\n- Document reasoning steps and validation approaches\n- Capture multi-step workflows\n\n# Response Format (JSON):\n{{\n    \"memories_items\": [\n        {{\n            \"content\": \"MUST be a complete markdown skill profile starting with --- frontmatter, then sections. Format:\n---\nname: skill-name\ndescription: one line description\ncategory: category-name\ndemonstrated-in: [context]\n---\n\n[Introduction paragraph]\n\n## Core Principles\n[bullet points]\n\n## When to Use This Skill\n[bullet points]\n\n## Implementation Guide\n### Prerequisites\n### Techniques and Approaches\n### Example from Resource\n\n## Success Patterns\n[bullet points]\n\n## Common Pitfalls\n[bullet points]\n\n## Key Takeaways\n[bullet points]\n\nMinimum 300 words total.\",\n            \"categories\": [list of memory categories]\n        }}\n    ]\n}}\n\nCRITICAL: The content field MUST contain the complete markdown text with ALL sections, not a summary paragraph. This is a skill documentation page, not a description.\n\"\"\"\n\nPROMPT_BLOCK_OBJECTIVE = \"\"\"\n# Task Objective\nYou are a professional User Memory Extractor. Your core task is to extract skills, capabilities, and technical competencies demonstrated or described in the resource content (agent logs, workflow documentation, execution traces, or technical documents). Format each skill as a comprehensive, production-ready skill profile that can be referenced and applied.\n\"\"\"\n\nPROMPT_BLOCK_WORKFLOW = \"\"\"\n# Workflow\nRead the full resource content to understand the context and technical details.\n## Extract skills\nIdentify valuable skills, capabilities, and technical competencies demonstrated in the content.\n## Create skill profiles\nFor each skill, create a comprehensive profile with all required sections.\n## Review & validate\nEnsure each skill profile is complete, actionable, and meets the minimum 300 words requirement.\n## Final output\nOutput Skill Information as structured skill profiles.\n\"\"\"\n\nPROMPT_BLOCK_RULES = \"\"\"\n# Rules\n## General requirements (must satisfy all)\n- Each skill must be formatted as a comprehensive skill profile with frontmatter and all required sections.\n- Each skill profile must capture not just WHAT was done, but HOW and WHY it works.\n- Be specific and concrete - include technology names, version numbers, metrics, and outcomes.\n- Each skill should be comprehensive enough to be referenced and applied independently.\n- Minimum 300 words per skill to ensure depth and actionability.\nImportant: Extract only skills that are clearly demonstrated or described in the resource. No guesses or fabricated details.\n\n## Skill Profile Structure (must include all sections)\n1. Frontmatter: name, description, category, demonstrated-in\n2. Introduction paragraph\n3. Core Principles\n4. When to Use This Skill\n5. Implementation Guide (Prerequisites, Techniques and Approaches, Example from Resource)\n6. Success Patterns\n7. Common Pitfalls\n8. Key Takeaways\n\n## Special rules for Skill Information\n- Generic statements without concrete approaches are forbidden (e.g., \"Used Docker\", \"Good at programming\").\n- Opinions without demonstrated practice are forbidden (e.g., \"I think microservices are better\").\n- Theory without practice belongs to knowledge type, not skill type.\n- One-time luck without a replicable approach is not a skill.\n- Trivial actions are not skills (e.g., \"Using email\", \"Reading docs\").\n\n## What TO Extract\n- Concrete approaches with context, tools, metrics, and outcomes.\n- Deployment strategies with specific techniques (canary, blue-green, etc.).\n- Incident response procedures with detection, response, and recovery steps.\n- Problem-solving approaches with tool orchestration patterns.\n- Multi-step workflows with reasoning steps and validation approaches.\n\n## Resource Type Guidelines\n### For Deployment Logs:\n- Extract each significant deployment (success or failure) as a separate skill.\n- Success: Focus on techniques that worked.\n- Failure: Focus on incident response, root cause analysis, recovery procedures.\n- Include metrics: deployment time, error rates, response times, recovery time.\n\n### For Workflow Documentation:\n- Extract major workflow stages as skills.\n- Include tool chains and technology stacks.\n- Document step-by-step procedures.\n- Note success metrics and KPIs.\n\n### For Agent Execution Logs:\n- Extract problem-solving approaches as skills.\n- Include tool orchestration patterns.\n- Document reasoning steps and validation approaches.\n- Capture multi-step workflows.\n\n## Review & validation rules\n- Ensure all required sections are present in each skill profile.\n- Verify minimum 300 words per skill.\n- Final check: every skill profile must be actionable and replicable.\n\"\"\"\n\nPROMPT_BLOCK_CATEGORY = \"\"\"\n## Memory Categories:\n{categories_str}\n\"\"\"\n\nPROMPT_BLOCK_OUTPUT = \"\"\"\n# Output Format (XML)\nReturn all memories wrapped in a single <item> element:\n<item>\n    <memory>\n        <content>\n---\nname: skill-name-in-kebab-case\ndescription: One-line description of what this skill enables\ncategory: primary-category\ndemonstrated-in: [context where this was shown]\n---\n\n[Brief introduction explaining the skill and its importance]\n\n## Core Principles\n- [Key concept 1]\n- [Key concept 2]\n\n## When to Use This Skill\n- [Situation 1]\n- [Situation 2]\n\n## Implementation Guide\n### Prerequisites\n- [Required knowledge or setup]\n\n### Techniques and Approaches\n[Detailed explanation of how to apply this skill]\n\n### Example from Resource\n[Concrete example from the source material]\n\n## Success Patterns\n- [Pattern 1 with explanation]\n- [Pattern 2 with explanation]\n\n## Common Pitfalls\n- [Pitfall 1]: [Why it's a problem and how to avoid it]\n- [Pitfall 2]: [Why it's a problem and how to avoid it]\n\n## Key Takeaways\n- [Critical insight 1]\n- [Critical insight 2]\n        </content>\n        <categories>\n            <category>technical_skills</category>\n        </categories>\n    </memory>\n</item>\n\"\"\"\n\nPROMPT_BLOCK_EXAMPLES = \"\"\"\n# Examples (Input / Output / Explanation)\nExample 1: Skill Extraction from Deployment Log\n## Input\n[2024-01-15 10:30:00] Starting canary deployment for Payment Service v2.3.1\n[2024-01-15 10:30:15] Deployed new version alongside existing v2.3.0\n[2024-01-15 10:30:30] Configured load balancer: 10% traffic to v2.3.1\n[2024-01-15 10:35:30] Metrics check: Error rate 0.1% (baseline 0.1%), p95 latency 270ms (baseline 320ms)\n[2024-01-15 10:35:45] Increasing traffic to 50%\n[2024-01-15 10:40:45] Metrics stable, increasing to 100%\n[2024-01-15 10:42:00] Deployment complete. Zero downtime achieved.\n## Output\n<item>\n    <memory>\n        <content>\n---\nname: canary-deployment-with-monitoring\ndescription: Implement gradual traffic shifting deployment strategy with real-time monitoring\ncategory: deployment\ndemonstrated-in: [Payment Service v2.3.1 deployment]\n---\n\nCanary deployment is a risk-mitigation strategy that gradually shifts production traffic from an old version to a new version while continuously monitoring key metrics. This approach enables early detection of issues with minimal user impact.\n\n## Core Principles\n- Gradual exposure: Start with a small percentage of traffic (typically 5-10%) to limit blast radius\n- Continuous monitoring: Track error rates, response times in real-time\n- Quick recovery: Maintain ability to instantly route traffic back to stable version\n\n## When to Use This Skill\n- Deploying critical services where downtime is unacceptable\n- Rolling out changes with uncertain production behavior\n- High-traffic services where testing production performance is valuable\n\n## Implementation Guide\n### Prerequisites\n- Load balancer with traffic splitting capabilities\n- Monitoring system with real-time metrics\n- Automated deployment pipeline\n\n### Techniques and Approaches\n1. Initial Deployment (10% traffic): Deploy new version alongside existing, route 10% traffic, monitor 5 minutes\n2. Monitoring Checkpoints: Error rate should not exceed baseline by more than 2%, response time within 20% of baseline\n3. Gradual Rollout: If metrics stable, progress 10% to 25% to 50% to 100%\n\n### Example from Resource\nPayment Service v2.3.1 deployment achieved zero downtime during 12-minute deployment. Traffic progressed 10% to 50% to 100% with 5-minute pauses. Response time improved 15% (320ms to 270ms p95). Error rate remained stable at 0.1%.\n\n## Success Patterns\n- Small initial percentage: 5-10% catches most issues while limiting impact\n- Metric-driven automation: Removes human error from rollback decisions\n\n## Common Pitfalls\n- Too aggressive progression: Rushing from 10% to 100% defeats the purpose\n- Insufficient monitoring window: Need 5+ minutes at each stage to detect issues\n\n## Key Takeaways\n- Canary deployments trade deployment speed for safety\n- Start small, progress gradually, monitor continuously\n        </content>\n        <categories>\n            <category>technical_skills</category>\n        </categories>\n    </memory>\n</item>\n## Explanation\nA comprehensive skill profile is extracted from the deployment log, capturing the approach, techniques, metrics, and outcomes. The profile includes all required sections and provides actionable guidance for replicating the skill.\n\"\"\"\n\nPROMPT_BLOCK_INPUT = \"\"\"\n# Original Resource:\n<resource>\n{resource}\n</resource>\n\"\"\"\n\nPROMPT = \"\\n\\n\".join([\n    PROMPT_BLOCK_OBJECTIVE.strip(),\n    PROMPT_BLOCK_WORKFLOW.strip(),\n    PROMPT_BLOCK_RULES.strip(),\n    PROMPT_BLOCK_CATEGORY.strip(),\n    PROMPT_BLOCK_OUTPUT.strip(),\n    PROMPT_BLOCK_EXAMPLES.strip(),\n    PROMPT_BLOCK_INPUT.strip(),\n])\n\nCUSTOM_PROMPT = {\n    \"objective\": PROMPT_BLOCK_OBJECTIVE.strip(),\n    \"workflow\": PROMPT_BLOCK_WORKFLOW.strip(),\n    \"rules\": PROMPT_BLOCK_RULES.strip(),\n    \"category\": PROMPT_BLOCK_CATEGORY.strip(),\n    \"output\": PROMPT_BLOCK_OUTPUT.strip(),\n    \"examples\": PROMPT_BLOCK_EXAMPLES.strip(),\n    \"input\": PROMPT_BLOCK_INPUT.strip(),\n}\n"
  },
  {
    "path": "src/memu/prompts/memory_type/tool.py",
    "content": "PROMPT_BLOCK_OBJECTIVE = \"\"\"\n# Task Objective\nYou are a professional Tool Memory Extractor. Your core task is to extract tool usage patterns, execution results, and learnings from agent logs or tool execution traces. This enables agents to learn from their tool usage history.\n\"\"\"\n\nPROMPT_BLOCK_WORKFLOW = \"\"\"\n# Workflow\nRead the full resource content to understand tool execution context.\n## Extract tool memories\nIdentify tool calls, their inputs, outputs, success/failure status, and any patterns.\n## Create tool memory entries\nFor each significant tool usage, create a memory entry with when_to_use hints.\n## Review & validate\nEnsure each tool memory is actionable and helps future tool selection.\n## Final output\nOutput Tool Memory entries.\n\"\"\"\n\nPROMPT_BLOCK_RULES = \"\"\"\n# Rules\n## General requirements (must satisfy all)\n- Each tool memory must capture: tool name, what it was used for, outcome, and when to use it again.\n- Focus on patterns that help future tool selection decisions.\n- Include success/failure context to help agents avoid repeated mistakes.\n- Each memory should help answer: \"When should I use this tool?\"\n\n## What TO Extract\n- Successful tool usage patterns with context\n- Failed tool attempts with lessons learned\n- Tool combinations that work well together\n- Performance insights (fast vs slow tools for different tasks)\n\n## What NOT to Extract\n- Trivial tool calls without learning value\n- Duplicate patterns already captured\n- Tool calls with no meaningful outcome\n\n## Memory Item Content Requirements\n- Include the tool name prominently\n- Describe the use case or scenario\n- Note the outcome (success/failure/partial)\n- Provide a \"when_to_use\" hint for future retrieval\n\"\"\"\n\nPROMPT_BLOCK_CATEGORY = \"\"\"\n## Memory Categories:\n{categories_str}\n\"\"\"\n\nPROMPT_BLOCK_OUTPUT = \"\"\"\n# Output Format (XML)\nReturn all memories wrapped in a single <item> element:\n<item>\n    <memory>\n        <content>Tool memory content describing the tool usage pattern</content>\n        <when_to_use>Hint for when this memory should be retrieved</when_to_use>\n        <categories>\n            <category>Category Name</category>\n        </categories>\n    </memory>\n</item>\n\"\"\"\n\nPROMPT_BLOCK_EXAMPLES = \"\"\"\n# Examples (Input / Output / Explanation)\nExample 1: Tool Memory Extraction\n## Input\n[Tool Call] file_reader(path=\"/data/config.json\")\n[Result] Success - Read 2048 bytes in 0.3s\n[Tool Call] json_parser(content=<file_content>)\n[Result] Success - Parsed config with 15 keys\n[Tool Call] file_reader(path=\"/data/missing.json\")\n[Result] Error - FileNotFoundError: File does not exist\n## Output\n<item>\n    <memory>\n        <content>The file_reader tool successfully reads JSON config files from /data/ directory. Average read time is 0.3s for ~2KB files. Works well when chained with json_parser for config processing.</content>\n        <when_to_use>When needing to read configuration files or JSON data from the filesystem</when_to_use>\n        <categories>\n            <category>file_operations</category>\n        </categories>\n    </memory>\n    <memory>\n        <content>The file_reader tool fails with FileNotFoundError when the target file doesn't exist. Should verify file existence before reading or handle the error gracefully.</content>\n        <when_to_use>When handling file read errors or implementing robust file operations</when_to_use>\n        <categories>\n            <category>error_handling</category>\n        </categories>\n    </memory>\n</item>\n## Explanation\nTwo tool memories are extracted: one for successful usage pattern, one for error handling insight. Both include when_to_use hints for smart retrieval.\n\"\"\"\n\nPROMPT_BLOCK_INPUT = \"\"\"\n# Original Resource:\n<resource>\n{resource}\n</resource>\n\"\"\"\n\nPROMPT = \"\\n\\n\".join([\n    PROMPT_BLOCK_OBJECTIVE.strip(),\n    PROMPT_BLOCK_WORKFLOW.strip(),\n    PROMPT_BLOCK_RULES.strip(),\n    PROMPT_BLOCK_CATEGORY.strip(),\n    PROMPT_BLOCK_OUTPUT.strip(),\n    PROMPT_BLOCK_EXAMPLES.strip(),\n    PROMPT_BLOCK_INPUT.strip(),\n])\n\nCUSTOM_PROMPT = {\n    \"objective\": PROMPT_BLOCK_OBJECTIVE.strip(),\n    \"workflow\": PROMPT_BLOCK_WORKFLOW.strip(),\n    \"rules\": PROMPT_BLOCK_RULES.strip(),\n    \"category\": PROMPT_BLOCK_CATEGORY.strip(),\n    \"output\": PROMPT_BLOCK_OUTPUT.strip(),\n    \"examples\": PROMPT_BLOCK_EXAMPLES.strip(),\n    \"input\": PROMPT_BLOCK_INPUT.strip(),\n}\n"
  },
  {
    "path": "src/memu/prompts/preprocess/__init__.py",
    "content": "from memu.prompts.preprocess import audio, conversation, document, image, video\n\nPROMPTS: dict[str, str] = {\n    \"conversation\": conversation.PROMPT.strip(),\n    \"video\": video.PROMPT.strip(),\n    \"image\": image.PROMPT.strip(),\n    \"document\": document.PROMPT.strip(),\n    \"audio\": audio.PROMPT.strip(),\n}\n\n__all__ = [\"PROMPTS\"]\n"
  },
  {
    "path": "src/memu/prompts/preprocess/audio.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nAnalyze the provided audio transcription and produce two outputs:\n1. A **Processed Content** version that is clean, well-formatted, and easy to read.\n2. A **Caption** that summarizes what the audio is about in one sentence.\n\n# Workflow\n1. Read the **Transcription** carefully.\n2. Correct punctuation, capitalization, and obvious transcription artifacts.\n3. Add paragraph breaks where they improve readability or reflect topic shifts.\n4. Preserve the original meaning, wording, and sequence of the audio.\n5. Generate a concise **one-sentence caption** that accurately describes the audio's main topic or purpose.\n\n# Rules\n- Do not add, remove, or reinterpret content beyond cleaning and formatting.\n- Maintain the speaker's original intent and structure.\n- Avoid introducing new information not present in the transcription.\n- The caption must be **exactly one sentence**.\n- Use clear, neutral language.\n\n# Output Format\nUse the following structure:\n\n<processed_content>\n[Provide the cleaned and formatted transcription here]\n</processed_content>\n\n<caption>\n[Provide a one-sentence summary of what the audio is about]\n</caption>\n\n# Input\nTranscription:\n{transcription}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/preprocess/conversation.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nAnalyze a conversation with message indices and divide it into multiple meaningful segments based on topic changes, time gaps, or natural breaks.\n\n# Workflow\n1. Review the entire **Conversation Content** along with its message indices.\n2. Identify potential **segment boundaries** by observing:\n   - Topic changes\n   - Time gaps or pauses\n   - Natural conclusions of a discussion\n   - Clear shifts in tone or semantic focus\n3. Group messages into segments that each maintain a coherent theme.\n4. Ensure each segment has a clear beginning and end.\n5. Verify that each segment contains **at least 20 messages**.\n6. Record the `start` and `end` indices (inclusive) for each segment.\n\n# Rules\n- Segments must be based strictly on the provided conversation content.\n- Each segment must:\n  - Contain **≥ 20 messages**\n  - Maintain a **coherent theme**\n  - Have a **clear boundary** from adjacent segments\n- Use only the provided `[INDEX]` numbers.\n- Do not overlap segments.\n- Do not include explanations, comments, or extra text in the final output.\n\n# Output Format\nReturn **only valid JSON** in the following structure:\n\n```json\n{{\n    \"segments\": [\n        {{\"start\": x, \"end\": x}},\n        {{\"start\": x, \"end\": x}},\n        {{\"start\": x, \"end\": x}}\n    ]\n}}\n```\n\n# Input\nConversation Content:\n{conversation}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/preprocess/document.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nAnalyze the provided document text and produce two outputs:\n1. A condensed version that preserves all key information and important details while removing verbosity and redundancy.\n2. A one-sentence caption summarizing what the document is about.\n\n# Workflow\n1. Read the **Document** (`{document_text}`) carefully to understand its full content.\n2. Identify the main points, key arguments, and essential details.\n3. Remove repetition, filler, and unnecessary verbosity while preserving meaning and completeness.\n4. Rewrite the content in a concise, structured form.\n5. Generate a single-sentence **Caption** that accurately summarizes the document's purpose or topic.\n\n# Rules\n- Preserve all key information, facts, and conclusions.\n- Do not introduce new information or interpretations.\n- Keep the processed content concise but complete.\n- The caption must be exactly **one sentence**.\n- Use only the information contained in the provided document.\n\n# Output Format\nUse the following structure:\n\n<processed_content>\n[Provide the condensed version of the document here]\n</processed_content>\n\n<caption>\n[Provide a one-sentence summary of the document]\n</caption>\n\n# Input\nDocument:\n{document_text}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/preprocess/image.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nAnalyze the given image and produce two outputs:\n1. A **Detailed Description** that thoroughly explains what is shown in the image.\n2. A **Caption** that summarizes the image in a single sentence.\n\n# Workflow\n1. Examine the image carefully.\n2. Identify the **main subjects and objects** present.\n3. Describe any **actions or activities** taking place.\n4. Analyze the **setting, background, and environment**.\n5. Note any **visible text, signs, or labels**.\n6. Describe **colors, lighting, composition**, and visual layout.\n7. Infer the **overall mood, atmosphere, or style** of the image.\n8. Write a concise **one-sentence caption** that captures the essence of the image.\n\n# Rules\n- Base the description strictly on what is visible in the image.\n- Do not invent details that cannot be inferred visually.\n- Be comprehensive in the detailed description but clear and structured.\n- The caption must be **exactly one sentence**.\n- Use neutral and objective language.\n\n# Output Format\nUse the following structure:\n\n<detailed_description>\n[Provide the comprehensive description here]\n</detailed_description>\n\n<caption>\n[Provide a one-sentence summary of the image]\n</caption>\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/preprocess/video.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nAnalyze the given video and produce two outputs:\n1. A **Detailed Description** that comprehensively explains what happens in the video.\n2. A **Caption** that summarizes the video in a single sentence.\n\n# Workflow\n1. Watch the video carefully from start to finish.\n2. Identify the **main actions and activities** taking place over time.\n3. Describe the **key objects, people, and subjects** appearing in the video.\n4. Analyze the **scene, setting, and environment**.\n5. Note any **audio elements**, including dialogue, narration, music, or background sounds (if available).\n6. Highlight **important events or moments** in the video.\n7. Describe the **temporal flow**, explaining how the events progress from beginning to end.\n8. Write a concise **one-sentence caption** that captures the essence of the video.\n\n# Rules\n- Base the description strictly on observable visual and audio content from the video.\n- Do not invent details that cannot be inferred from the video.\n- Be comprehensive and chronological in the detailed description.\n- The caption must be **exactly one sentence**.\n- Use clear, neutral, and objective language.\n\n# Output Format\nUse the following structure:\n\n<detailed_description>\n[Provide the comprehensive description here]\n</detailed_description>\n\n<caption>\n[Provide a one-sentence summary of the video]\n</caption>\n\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/retrieve/__init__.py",
    "content": ""
  },
  {
    "path": "src/memu/prompts/retrieve/judger.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nJudge whether the retrieved content is sufficient to answer the user's query.\n\n# Workflow\n1. Analyze the **Query** to understand what the user is asking.\n2. Review the **Retrieved Content** carefully.\n3. Evaluate the retrieved content against the following criteria:\n   - Does it directly address the user's question?\n   - Is the information specific and detailed enough?\n   - Are there obvious gaps or missing details?\n   - Did the user explicitly ask to recall or remember more information?\n4. Based on this evaluation, decide whether the information is sufficient or more is needed.\n\n# Rules\n- Base your judgement **only** on the provided query and retrieved content.\n- Do not assume or add external knowledge.\n- Do not provide additional explanations beyond the required sections.\n- The final judgement must be **one word only**.\n\n# Output Format\nUse the following structure:\n\n<consideration>\nExplain your reasoning for how you made the judgement.\n</consideration>\n\n<judgement>\nENOUGH or MORE\n</judgement>\n\n\n# Input\nQuery:\n{query}\n\nRetrieved Content:\n{content}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/retrieve/llm_category_ranker.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nSearch through the provided categories and identify the most relevant ones for the given query, then rank them by relevance.\n\n# Workflow\n1. Analyze the **Query** to understand its intent and key topics.\n2. Review all **Available Categories**.\n3. Determine which categories are relevant to the query.\n4. Select up to **{top_k}** most relevant categories.\n5. Rank the selected categories from most to least relevant.\n\n# Rules\n- Only include categories that are actually relevant to the query.\n- Include **at most** {top_k} categories.\n- Ranking matters: the first category must be the most relevant.\n- Do not invent or modify category IDs.\n- If no categories are relevant, return an empty array.\n\n# Output Format\nReturn the result as a JSON object in the following format:\n\n```json\n{{\n  \"analysis\": \"your analysis process\",\n  \"categories\": [\"category_id_1\", \"category_id_2\", \"category_id_3\"]\n}}\n```\n\n# Input\nQuery:\n{query}\n\nAvailable Categories:\n{categories_data}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/retrieve/llm_item_ranker.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nSearch through the provided memory items and identify the most relevant ones for the given query, based on the already identified relevant categories, then rank them by relevance.\n\n# Workflow\n1. Analyze the **Query** to understand intent and key information needs.\n2. Review the **Relevant Categories** provided to understand the scope.\n3. Examine all **Available Memory Items** within those categories.\n4. Identify which memory items are truly relevant to the query.\n5. Select up to **top_k** most relevant items.\n6. Rank the selected items from most to least relevant.\n\n# Rules\n- Only consider memory items that belong to the provided relevant categories.\n- Only include items that are actually relevant to the query.\n- Include **at most** {top_k} items.\n- Order matters: the first item must be the most relevant.\n- Do not invent, modify, or infer item IDs.\n- If no relevant items are found, return an empty array.\n\n# Output Format\nReturn the result as a JSON object in the following format:\n\n```json\n{{\n  \"analysis\": \"your analysis process\",\n  \"items\": [\"item_id_1\", \"item_id_2\", \"item_id_3\"]\n}}\n```\n\n# Input\nQuery:\n{query}\n\nAvailable Memory Items:\n{items_data}\n\nThese memory items belong to the following relevant categories that were already identified:\n{relevant_categories}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/retrieve/llm_resource_ranker.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nSearch through the provided resources and identify the most relevant ones for the given query, then rank them by relevance.\n\n# Workflow\n1. Analyze the **Query**  to understand its intent and key information needs.\n2. Review the provided **Context Info** , which contains already-identified categories and items, to guide relevance.\n3. Examine all **Available Resources**.\n4. Determine which resources are actually relevant to the query.\n5. Select up to {top_k} most relevant resources.\n6. Rank the selected resources from most to least relevant.\n\n# Rules\n- Only include resources that are actually relevant to the query.\n- Include **at most {top_k}** resources.\n- Ranking matters: the first resource must be the most relevant.\n- Do not invent, modify, or infer resource IDs.\n- If no resources are relevant, return an empty array.\n\n# Output Format\nReturn the result as a JSON object in the following format:\n\n```json\n{{\n  \"analysis\": \"your analysis process\",\n  \"resources\": [\"resource_id_1\", \"resource_id_2\", \"resource_id_3\"]\n}}\n```\n\n# Input\nQuery:\n{query}\n\nContext Info:\n{context_info}\n\nAvailable Resources:\n{resources_data}\n\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/retrieve/pre_retrieval_decision.py",
    "content": "SYSTEM_PROMPT = \"\"\"\n# Task Objective\nDetermine whether the current query requires retrieving information from memory or can be answered directly without retrieval.\nIf retrieval is required, rewrite the query to include relevant contextual information.\n\n# Workflow\n1. Review the **Query Context** to understand prior conversation and available background.\n2. Analyze the **Current Query**.\n3. Consider the **Retrieved Content**, if any.\n4. Decide whether memory retrieval is required based on the criteria.\n5. If retrieval is needed, rewrite the query to incorporate relevant context from the query context.\n6. If retrieval is not needed, keep the original query unchanged.\n\n# Rules\n- **NO_RETRIEVE** for:\n  - Greetings, casual chat, or acknowledgments\n  - Questions about only the current conversation/context\n  - General knowledge questions\n  - Requests for clarification\n  - Meta-questions about the system itself\n- **RETRIEVE** for:\n  - Questions about past events, conversations, or interactions\n  - Queries about user preferences, habits, or characteristics\n  - Requests to recall specific information\n  - Questions referencing historical data\n- Do not add external knowledge beyond the provided context.\n- If retrieval is not required, return the original query exactly.\n\n# Output Format\nUse the following structure:\n\n<decision>\nRETRIEVE or NO_RETRIEVE\n</decision>\n\n<rewritten_query>\nIf RETRIEVE: provide a rewritten query incorporating relevant context.\nIf NO_RETRIEVE: return `{query}` unchanged.\n</rewritten_query>\n\"\"\"\n\n\nUSER_PROMPT = \"\"\"\n# Input\nQuery Context:\n{conversation_history}\n\nCurrent Query:\n{query}\n\nRetrieved Content:\n{retrieved_content}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/retrieve/query_rewriter.py",
    "content": "PROMPT = \"\"\"\n# Task Objective\nRewrite a user query to make it self-contained and explicit by resolving references and ambiguities using the conversation history.\n\n# Workflow\n1. Review the **Conversation History** to identify relevant entities, topics, and context.\n2. Analyze the **Current Query**.\n3. Determine whether the query contains:\n   - Pronouns (e.g., “they”, “it”, “their”, “his”, “her”)\n   - Referential expressions (e.g., “that”, “those”, “the same”)\n   - Implicit context (e.g., “what about…”, “and also…”)\n   - Incomplete information that can be inferred from the conversation history\n4. If rewriting is needed:\n   - Replace pronouns with specific entities mentioned in the conversation\n   - Add necessary background from the conversation history\n   - Make implicit references explicit\n   - Ensure the rewritten query is understandable on its own\n5. If the query is already clear and self-contained, keep it unchanged.\n\n# Rules\n- Preserve the original intent of the user query.\n- Only use information explicitly available in the conversation history.\n- Do not introduce new assumptions or external knowledge.\n- Keep the rewritten query concise but fully explicit.\n\n# Output Format\nUse the following structure:\n\n<analysis>\nBrief analysis of whether the query needs rewriting and why.\n</analysis>\n\n<rewritten_query>\nThe rewritten query that is self-contained and explicit if no rewrite is needed).\n</rewritten_query>\n\n\n# Input\nQuery Context:\n{conversation_history}\n\nCurrent Query:\n{query}\n\"\"\"\n"
  },
  {
    "path": "src/memu/prompts/retrieve/query_rewriter_judger.py",
    "content": "SYSTEM_PROMPT = \"\"\"\n# Task Objective\nPerform two tasks:\n1. **Query Rewriting** - Incorporate conversation context to make the query more specific and clear.\n2. **Sufficiency Judgment** - Determine whether the retrieved content is sufficient to answer the query.\n\nYou should be conservative and only mark the result as **ENOUGH** when the retrieved content truly provides adequate information.\n\n# Workflow\n1. Review the **Query Context** to understand prior conversation and background.\n2. Analyze the **Original Query**.\n3. Examine the **Retrieved Content So Far**.\n4. Rewrite the query by incorporating relevant context to improve clarity and specificity.\n5. Judge whether the retrieved content is sufficient to answer the rewritten query based on defined criteria.\n\n# Rules\n- Query rewriting must stay faithful to the user's original intent.\n- Only incorporate context that is relevant and helpful.\n- Do not introduce new assumptions or external knowledge.\n- Mark **ENOUGH** only if:\n  - The retrieved content directly addresses the query, **and**\n  - The information is specific and detailed enough, **and**\n  - There are no obvious gaps or missing details.\n- If any key information is missing or unclear, mark **MORE**.\n\n# Output Format\nUse the following structure:\n\n<rewritten_query>\n[Provide the rewritten query with conversation context]\n</rewritten_query>\n\n<judgement>\nENOUGH or MORE\n</judgement>\n\"\"\"\n\nUSER_PROMPT = \"\"\"\nInput:\nQuery Context:\n{conversation_history}\n\nOriginal Query:\n{original_query}\n\nRetrieved Content So Far:\n{retrieved_content}\n\"\"\"\n"
  },
  {
    "path": "src/memu/utils/__init__.py",
    "content": "\"\"\"Utility modules for memU.\"\"\"\n\nfrom memu.utils.video import VideoFrameExtractor\n\n__all__ = [\"VideoFrameExtractor\"]\n"
  },
  {
    "path": "src/memu/utils/conversation.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom typing import Any\n\n\ndef format_conversation_for_preprocess(raw_text: str) -> str:\n    \"\"\"\n    Normalize a conversation into a line-based format suitable for LLM preprocessing prompts.\n\n    Supported input formats:\n    - A JSON list of messages: [{\"role\": \"...\", \"content\": \"...\", \"created_at\": \"...\"}]\n    - A JSON dict with a \"content\" list: {\"content\": [ ...messages... ]}\n\n    Output format:\n    - One message per line\n    - Each line starts with an index marker: \"[{idx}]\"\n    - If a created_at is available, it is included after the index\n    - The role is included in square brackets: \"[user]\" / \"[assistant]\" etc.\n\n    Notes:\n    - This function expects conversation data to be JSON.\n    - Newlines in message content are collapsed to spaces to keep one message per line.\n    \"\"\"\n    stripped = (raw_text or \"\").strip()\n    if not stripped:\n        return \"\"\n\n    parsed = _try_parse_json(stripped)\n    if parsed is None:\n        # Conversation inputs are expected to be JSON. If invalid, return original text.\n        return raw_text\n    messages = _extract_messages(parsed)\n    if messages is None:\n        return raw_text\n    return _format_messages(messages)\n\n\ndef _try_parse_json(text: str) -> Any | None:\n    if not text:\n        return None\n    if not (text.startswith(\"[\") or text.startswith(\"{\")):\n        return None\n    try:\n        return json.loads(text)\n    except Exception:\n        return None\n\n\ndef _extract_messages(payload: Any) -> list[dict[str, Any]] | None:\n    if isinstance(payload, list):\n        return [m for m in payload if isinstance(m, dict)]\n    if isinstance(payload, dict):\n        content = payload.get(\"content\")\n        if isinstance(content, list):\n            return [m for m in content if isinstance(m, dict)]\n    return None\n\n\ndef _format_messages(messages: list[dict[str, Any]]) -> str:\n    out: list[str] = []\n    for idx, msg in enumerate(messages):\n        role = str(msg.get(\"role\") or \"user\").strip() or \"user\"\n        content = msg.get(\"content\")\n        text = _extract_text_content(content)\n        created_at = _extract_created_at(msg)\n        created_part = f\"{created_at} \" if created_at else \"\"\n        out.append(f\"[{idx}] {created_part}[{role}]: {text}\")\n    return \"\\n\".join(out)\n\n\ndef _extract_text_content(content: Any) -> str:\n    if isinstance(content, dict):\n        text = content.get(\"text\", \"\")\n    elif isinstance(content, str):\n        text = content\n    else:\n        text = \"\" if content is None else str(content)\n    # Ensure single-line to keep indexing consistent\n    return \" \".join(str(text).splitlines()).strip()\n\n\ndef _extract_created_at(msg: dict[str, Any]) -> str | None:\n    raw = msg.get(\"created_at\")\n    if raw is None:\n        return None\n\n    s = str(raw).strip()\n    return s or None\n"
  },
  {
    "path": "src/memu/utils/references.py",
    "content": "\"\"\"\nUtilities for handling memory item references in category summaries.\n\nReferences are inline citations in the format [ref:ITEM_ID] that link\nspecific statements in category summaries to their source memory items.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from memu.database.interfaces import Database\n\n# Pattern to match references like [ref:abc123] or [ref:abc123,def456]\nREFERENCE_PATTERN = re.compile(r\"\\[ref:([a-zA-Z0-9_,\\-]+)\\]\")\n\n\ndef extract_references(text: str | None) -> list[str]:\n    \"\"\"\n    Extract all item IDs referenced in a text.\n\n    Args:\n        text: Text containing [ref:ITEM_ID] citations\n\n    Returns:\n        List of unique item IDs found in references\n\n    Example:\n        >>> extract_references(\"User loves coffee [ref:abc123]. Also tea [ref:def456].\")\n        ['abc123', 'def456']\n    \"\"\"\n    if not text:\n        return []\n\n    item_ids: list[str] = []\n    seen: set[str] = set()\n\n    for match in REFERENCE_PATTERN.finditer(text):\n        # Handle comma-separated IDs like [ref:abc,def]\n        ids_str = match.group(1)\n        for item_id in ids_str.split(\",\"):\n            item_id = item_id.strip()\n            if item_id and item_id not in seen:\n                item_ids.append(item_id)\n                seen.add(item_id)\n\n    return item_ids\n\n\ndef strip_references(text: str | None) -> str | None:\n    \"\"\"\n    Remove all [ref:...] citations from text for clean display.\n\n    Args:\n        text: Text containing references\n\n    Returns:\n        Text with references removed\n\n    Example:\n        >>> strip_references(\"User loves coffee [ref:abc123].\")\n        'User loves coffee.'\n    \"\"\"\n    if not text:\n        return text\n    # Remove references\n    result = REFERENCE_PATTERN.sub(\"\", text)\n    # Clean up space before punctuation (e.g., \" .\" -> \".\")\n    result = re.sub(r\"\\s+([.,;:!?])\", r\"\\1\", result)\n    # Collapse multiple spaces into one and strip\n    result = \" \".join(result.split())\n    return result\n\n\ndef format_references_as_citations(text: str | None) -> str | None:\n    \"\"\"\n    Convert [ref:ID] format to numbered citations [1], [2], etc.\n\n    Args:\n        text: Text with [ref:ID] references\n\n    Returns:\n        Text with numbered citations and a reference list at the end\n\n    Example:\n        >>> format_references_as_citations(\"User loves coffee [ref:abc].\")\n        'User loves coffee [1].\\\\n\\\\nReferences:\\\\n[1] abc'\n    \"\"\"\n    if not text:\n        return text\n\n    refs = extract_references(text)\n    if not refs:\n        return text\n\n    # Build ID to number mapping\n    id_to_num = {ref_id: idx + 1 for idx, ref_id in enumerate(refs)}\n\n    # Replace [ref:ID] with [N]\n    def replace_ref(match: re.Match) -> str:\n        ids_str = match.group(1)\n        nums = []\n        for item_id in ids_str.split(\",\"):\n            item_id = item_id.strip()\n            if item_id in id_to_num:\n                nums.append(str(id_to_num[item_id]))\n        return f\"[{','.join(nums)}]\" if nums else \"\"\n\n    result = REFERENCE_PATTERN.sub(replace_ref, text)\n\n    # Add reference list at end\n    ref_list = \"\\n\".join(f\"[{num}] {ref_id}\" for ref_id, num in id_to_num.items())\n    return f\"{result}\\n\\nReferences:\\n{ref_list}\"\n\n\ndef fetch_referenced_items(\n    text: str,\n    store: Database,\n) -> list[dict]:\n    \"\"\"\n    Fetch memory items referenced in text.\n\n    Args:\n        text: Text containing [ref:ITEM_ID] citations\n        store: Database store instance\n\n    Returns:\n        List of memory item dicts with id, summary, memory_type\n    \"\"\"\n    item_ids = extract_references(text)\n    if not item_ids:\n        return []\n\n    items = []\n    for item_id in item_ids:\n        item = store.memory_item_repo.get_item(item_id)\n        if item:\n            items.append({\n                \"id\": item.id,\n                \"summary\": item.summary,\n                \"memory_type\": item.memory_type,\n            })\n\n    return items\n\n\ndef build_item_reference_map(items: list[tuple[str, str]]) -> str:\n    \"\"\"\n    Build a reference map string for the LLM prompt.\n\n    Args:\n        items: List of (item_id, summary) tuples\n\n    Returns:\n        Formatted string showing available item IDs\n\n    Example:\n        >>> build_item_reference_map([(\"abc\", \"User loves coffee\")])\n        'Available memory items for reference:\\\\n- [ref:abc] User loves coffee'\n    \"\"\"\n    if not items:\n        return \"\"\n\n    lines = [\"Available memory items for reference:\"]\n    for item_id, summary in items:\n        # Truncate long summaries\n        display = summary[:100] + \"...\" if len(summary) > 100 else summary\n        lines.append(f\"- [ref:{item_id}] {display}\")\n\n    return \"\\n\".join(lines)\n"
  },
  {
    "path": "src/memu/utils/tool.py",
    "content": "\"\"\"Utility functions for tool memory operations.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nif TYPE_CHECKING:\n    from memu.database.models import MemoryItem, ToolCallResult\n\n\ndef get_tool_calls(item: MemoryItem) -> list[dict[str, Any]]:\n    \"\"\"Get tool calls from a memory item's extra field.\n\n    Args:\n        item: The MemoryItem to get tool calls from\n\n    Returns:\n        List of tool call dicts, or empty list if none exist\n    \"\"\"\n    result: list[dict[str, Any]] = (item.extra or {}).get(\"tool_calls\", [])\n    return result\n\n\ndef set_tool_calls(item: MemoryItem, tool_calls: list[dict[str, Any]]) -> None:\n    \"\"\"Set tool calls in a memory item's extra field.\n\n    Args:\n        item: The MemoryItem to set tool calls on\n        tool_calls: The list of tool call dicts to set\n    \"\"\"\n    if item.extra is None:\n        item.extra = {}\n    item.extra[\"tool_calls\"] = tool_calls\n\n\ndef add_tool_call(item: MemoryItem, tool_call: ToolCallResult) -> None:\n    \"\"\"Add a tool call result to a memory item (for tool type memories).\n\n    Args:\n        item: The MemoryItem to add the tool call to (must be tool type)\n        tool_call: The ToolCallResult to add\n\n    Raises:\n        ValueError: If the memory item is not of type 'tool'\n    \"\"\"\n    if item.memory_type != \"tool\":\n        msg = \"add_tool_call can only be used with tool type memories\"\n        raise ValueError(msg)\n    tool_call.ensure_hash()\n    tool_calls = get_tool_calls(item)\n    tool_calls.append(tool_call.model_dump())\n    set_tool_calls(item, tool_calls)\n\n\ndef get_tool_statistics(item: MemoryItem, recent_n: int = 20) -> dict[str, Any]:\n    \"\"\"Calculate statistics for the most recent N tool calls.\n\n    Args:\n        item: The MemoryItem to calculate statistics for\n        recent_n: Number of recent calls to analyze (default: 20)\n\n    Returns:\n        Dictionary with total_calls, recent_calls_analyzed, avg_time_cost,\n        success_rate, avg_score, avg_token_cost\n    \"\"\"\n    tool_calls = get_tool_calls(item)\n    if not tool_calls:\n        return {\n            \"total_calls\": 0,\n            \"recent_calls_analyzed\": 0,\n            \"avg_time_cost\": 0.0,\n            \"success_rate\": 0.0,\n            \"avg_score\": 0.0,\n            \"avg_token_cost\": 0.0,\n        }\n\n    recent_calls = tool_calls[-recent_n:]\n    recent_count = len(recent_calls)\n\n    # Calculate statistics (tool_calls are now dicts, not ToolCallResult objects)\n    total_time = sum(c.get(\"time_cost\", 0.0) for c in recent_calls)\n    avg_time_cost = total_time / recent_count if recent_count > 0 else 0.0\n\n    successful = sum(1 for c in recent_calls if c.get(\"success\", True))\n    success_rate = successful / recent_count if recent_count > 0 else 0.0\n\n    total_score = sum(c.get(\"score\", 0.0) for c in recent_calls)\n    avg_score = total_score / recent_count if recent_count > 0 else 0.0\n\n    valid_token_calls = [c for c in recent_calls if c.get(\"token_cost\", -1) >= 0]\n    avg_token_cost = (\n        sum(c.get(\"token_cost\", 0) for c in valid_token_calls) / len(valid_token_calls) if valid_token_calls else 0.0\n    )\n\n    return {\n        \"total_calls\": len(tool_calls),\n        \"recent_calls_analyzed\": recent_count,\n        \"avg_time_cost\": round(avg_time_cost, 3),\n        \"success_rate\": round(success_rate, 4),\n        \"avg_score\": round(avg_score, 3),\n        \"avg_token_cost\": round(avg_token_cost, 2),\n    }\n"
  },
  {
    "path": "src/memu/utils/video.py",
    "content": "\"\"\"Video processing utilities for frame extraction.\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport shutil\nimport subprocess\nimport tempfile\nfrom pathlib import Path\nfrom typing import ClassVar\n\nlogger = logging.getLogger(__name__)\n\n\nclass VideoFrameExtractor:\n    \"\"\"Extract frames from video files using ffmpeg.\"\"\"\n\n    FFMPEG_BINARIES: ClassVar[set[str]] = {\"ffmpeg\", \"ffprobe\"}\n\n    @classmethod\n    def is_ffmpeg_available(cls) -> bool:\n        \"\"\"Check if ffmpeg is available in the system.\"\"\"\n        try:\n            result = cls._run_ffmpeg_command([\"ffmpeg\", \"-version\"], timeout=5, check=False)\n        except (FileNotFoundError, subprocess.TimeoutExpired, ValueError):\n            return False\n        else:\n            return result.returncode == 0\n\n    @staticmethod\n    def extract_middle_frame(video_path: str, output_path: str | None = None) -> str:\n        \"\"\"\n        Extract the middle frame from a video file.\n\n        Args:\n            video_path: Path to the video file\n            output_path: Optional output path for the frame. If None, creates a temp file.\n\n        Returns:\n            Path to the extracted frame image\n\n        Raises:\n            RuntimeError: If ffmpeg is not available or extraction fails\n        \"\"\"\n        if not VideoFrameExtractor.is_ffmpeg_available():\n            msg = \"ffmpeg is not available. Please install ffmpeg to process videos.\"\n            raise RuntimeError(msg)\n\n        video_path_obj = VideoFrameExtractor._resolve_existing_path(video_path, description=\"Video file\")\n        safe_video_path = str(video_path_obj)\n\n        # Create output path if not provided\n        created_temp_file = False\n        if output_path is None:\n            with tempfile.NamedTemporaryFile(suffix=\".jpg\", delete=False) as tmp_file:\n                output_path = tmp_file.name\n            created_temp_file = True\n        output_path_obj = VideoFrameExtractor._resolve_output_path(output_path)\n        safe_output_path = str(output_path_obj)\n\n        try:\n            # Get video duration\n            duration_cmd = [\n                \"ffprobe\",\n                \"-v\",\n                \"error\",\n                \"-show_entries\",\n                \"format=duration\",\n                \"-of\",\n                \"default=noprint_wrappers=1:nokey=1\",\n                safe_video_path,\n            ]\n\n            logger.debug(f\"Getting video duration: {' '.join(duration_cmd)}\")\n            duration_result = VideoFrameExtractor._run_ffmpeg_command(duration_cmd, timeout=30)\n\n            duration = float(duration_result.stdout.strip())\n            middle_time = duration / 2\n\n            logger.debug(f\"Video duration: {duration}s, extracting frame at {middle_time}s\")\n\n            # Extract frame at middle timestamp\n            extract_cmd = [\n                \"ffmpeg\",\n                \"-ss\",\n                str(middle_time),\n                \"-i\",\n                safe_video_path,\n                \"-vframes\",\n                \"1\",\n                \"-q:v\",\n                \"2\",  # High quality\n                \"-y\",  # Overwrite output file\n                safe_output_path,\n            ]\n\n            logger.debug(f\"Extracting frame: {' '.join(extract_cmd)}\")\n            VideoFrameExtractor._run_ffmpeg_command(extract_cmd, timeout=30)\n\n            if not output_path_obj.exists():\n                msg = f\"Frame extraction failed: output file not created at {output_path_obj}\"\n                raise RuntimeError(msg)\n            else:\n                logger.info(f\"Successfully extracted frame to: {output_path_obj}\")\n                return str(output_path_obj)\n\n        except subprocess.CalledProcessError as e:\n            if created_temp_file and output_path_obj.exists():\n                output_path_obj.unlink()\n            msg = f\"ffmpeg/ffprobe failed: {e.stderr}\"\n            logger.exception(msg)\n            raise RuntimeError(msg) from e\n        except subprocess.TimeoutExpired as e:\n            if created_temp_file and output_path_obj.exists():\n                output_path_obj.unlink()\n            msg = \"Video processing timed out\"\n            logger.exception(msg)\n            raise RuntimeError(msg) from e\n\n    @staticmethod\n    def extract_multiple_frames(\n        video_path: str,\n        num_frames: int = 3,\n        output_dir: str | None = None,\n    ) -> list[str]:\n        \"\"\"\n        Extract multiple evenly-spaced frames from a video.\n\n        Args:\n            video_path: Path to the video file\n            num_frames: Number of frames to extract\n            output_dir: Optional output directory. If None, creates a temp directory.\n\n        Returns:\n            List of paths to extracted frame images\n\n        Raises:\n            RuntimeError: If ffmpeg is not available or extraction fails\n        \"\"\"\n        if not VideoFrameExtractor.is_ffmpeg_available():\n            msg = \"ffmpeg is not available. Please install ffmpeg to process videos.\"\n            raise RuntimeError(msg)\n\n        video_path_obj = VideoFrameExtractor._resolve_existing_path(video_path, description=\"Video file\")\n        safe_video_path = str(video_path_obj)\n\n        # Create output directory if not provided\n        created_temp_dir = False\n        if output_dir is None:\n            output_dir = tempfile.mkdtemp()\n            created_temp_dir = True\n\n        output_dir_obj = VideoFrameExtractor._ensure_safe_cli_path(Path(output_dir))\n        output_dir_obj.mkdir(parents=True, exist_ok=True)\n\n        try:\n            # Get video duration\n            duration_cmd = [\n                \"ffprobe\",\n                \"-v\",\n                \"error\",\n                \"-show_entries\",\n                \"format=duration\",\n                \"-of\",\n                \"default=noprint_wrappers=1:nokey=1\",\n                safe_video_path,\n            ]\n\n            logger.debug(f\"Getting video duration: {' '.join(duration_cmd)}\")\n            duration_result = VideoFrameExtractor._run_ffmpeg_command(duration_cmd, timeout=30)\n\n            duration = float(duration_result.stdout.strip())\n\n            # Calculate timestamps for evenly-spaced frames\n            timestamps = [duration * (i + 1) / (num_frames + 1) for i in range(num_frames)]\n\n            logger.debug(f\"Video duration: {duration}s, extracting frames at: {timestamps}\")\n\n            frame_paths = []\n            for idx, timestamp in enumerate(timestamps):\n                output_path_obj = VideoFrameExtractor._resolve_output_path(str(output_dir_obj / f\"frame_{idx:03d}.jpg\"))\n\n                # Extract frame at timestamp\n                extract_cmd = [\n                    \"ffmpeg\",\n                    \"-ss\",\n                    str(timestamp),\n                    \"-i\",\n                    safe_video_path,\n                    \"-vframes\",\n                    \"1\",\n                    \"-q:v\",\n                    \"2\",  # High quality\n                    \"-y\",  # Overwrite output file\n                    str(output_path_obj),\n                ]\n\n                logger.debug(f\"Extracting frame {idx + 1}/{num_frames}: {' '.join(extract_cmd)}\")\n                VideoFrameExtractor._run_ffmpeg_command(extract_cmd, timeout=30)\n\n                if not output_path_obj.exists():\n                    msg = f\"Frame extraction failed: output file not created at {output_path_obj}\"\n                    raise RuntimeError(msg)\n\n                frame_paths.append(str(output_path_obj))\n\n            logger.info(f\"Successfully extracted {len(frame_paths)} frames to: {output_dir_obj}\")\n        except subprocess.CalledProcessError as e:\n            if created_temp_dir and output_dir_obj.exists():\n                shutil.rmtree(output_dir_obj)\n            msg = f\"ffmpeg/ffprobe failed: {e.stderr}\"\n            logger.exception(msg)\n            raise RuntimeError(msg) from e\n        except subprocess.TimeoutExpired as e:\n            if created_temp_dir and output_dir_obj.exists():\n                shutil.rmtree(output_dir_obj)\n            msg = \"Video processing timed out\"\n            logger.exception(msg)\n            raise RuntimeError(msg) from e\n        else:\n            return frame_paths\n\n    @staticmethod\n    def _ensure_safe_cli_path(path_obj: Path) -> Path:\n        \"\"\"Ensure the given path is safe to pass to a CLI command.\"\"\"\n        resolved = path_obj.resolve()\n        if resolved.name.startswith(\"-\"):\n            msg = f\"Unsafe path for CLI execution: {resolved}\"\n            raise ValueError(msg)\n        return resolved\n\n    @classmethod\n    def _resolve_existing_path(cls, path: str, *, description: str) -> Path:\n        \"\"\"Resolve and validate an existing filesystem path.\"\"\"\n        path_obj = Path(path)\n        if not path_obj.exists():\n            msg = f\"{description} not found: {path}\"\n            raise FileNotFoundError(msg)\n        return cls._ensure_safe_cli_path(path_obj)\n\n    @classmethod\n    def _resolve_output_path(cls, path: str) -> Path:\n        \"\"\"Resolve output paths (which may not yet exist) for CLI safety.\"\"\"\n        return cls._ensure_safe_cli_path(Path(path))\n\n    @classmethod\n    def _run_ffmpeg_command(\n        cls,\n        cmd: list[str],\n        *,\n        timeout: int,\n        check: bool = True,\n        capture_output: bool = True,\n    ) -> subprocess.CompletedProcess[str]:\n        \"\"\"Run an ffmpeg/ffprobe command after validating the executable.\"\"\"\n        if not cmd:\n            msg = \"FFmpeg command cannot be empty.\"\n            raise ValueError(msg)\n        executable = cmd[0]\n        binary_name = Path(executable).name\n        if binary_name not in cls.FFMPEG_BINARIES:\n            msg = f\"Unsupported executable '{executable}'\"\n            raise ValueError(msg)\n        safe_cmd = [executable, *[str(arg) for arg in cmd[1:]]]\n        return subprocess.run(  # noqa: S603\n            safe_cmd,\n            capture_output=capture_output,\n            text=True,\n            timeout=timeout,\n            check=check,\n        )\n"
  },
  {
    "path": "src/memu/workflow/__init__.py",
    "content": "from memu.workflow.interceptor import (\n    WorkflowInterceptorHandle,\n    WorkflowInterceptorRegistry,\n    WorkflowStepContext,\n)\nfrom memu.workflow.pipeline import PipelineManager, PipelineRevision\nfrom memu.workflow.runner import (\n    LocalWorkflowRunner,\n    WorkflowRunner,\n    register_workflow_runner,\n    resolve_workflow_runner,\n)\nfrom memu.workflow.step import WorkflowContext, WorkflowState, WorkflowStep, run_steps\n\n__all__ = [\n    \"LocalWorkflowRunner\",\n    \"PipelineManager\",\n    \"PipelineRevision\",\n    \"WorkflowContext\",\n    \"WorkflowInterceptorHandle\",\n    \"WorkflowInterceptorRegistry\",\n    \"WorkflowRunner\",\n    \"WorkflowState\",\n    \"WorkflowStep\",\n    \"WorkflowStepContext\",\n    \"register_workflow_runner\",\n    \"resolve_workflow_runner\",\n    \"run_steps\",\n]\n"
  },
  {
    "path": "src/memu/workflow/interceptor.py",
    "content": "from __future__ import annotations\n\nimport inspect\nimport logging\nimport threading\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING, Any\n\nif TYPE_CHECKING:\n    from memu.workflow.step import WorkflowState\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass(frozen=True)\nclass WorkflowStepContext:\n    \"\"\"Context information for a workflow step execution.\"\"\"\n\n    workflow_name: str\n    step_id: str\n    step_role: str\n    step_context: dict[str, Any]\n\n\n@dataclass(frozen=True)\nclass _WorkflowInterceptor:\n    interceptor_id: int\n    fn: Callable[..., Any]\n    name: str | None\n\n\n@dataclass(frozen=True)\nclass _WorkflowInterceptorSnapshot:\n    before: tuple[_WorkflowInterceptor, ...]\n    after: tuple[_WorkflowInterceptor, ...]\n    on_error: tuple[_WorkflowInterceptor, ...]\n\n\nclass WorkflowInterceptorHandle:\n    \"\"\"Handle for disposing a registered workflow interceptor.\"\"\"\n\n    def __init__(self, registry: WorkflowInterceptorRegistry, interceptor_id: int) -> None:\n        self._registry = registry\n        self._interceptor_id = interceptor_id\n        self._disposed = False\n\n    def dispose(self) -> bool:\n        \"\"\"Remove the interceptor from the registry. Returns True if removed.\"\"\"\n        if self._disposed:\n            return False\n        self._disposed = True\n        return self._registry.remove(self._interceptor_id)\n\n\nclass WorkflowInterceptorRegistry:\n    \"\"\"\n    Registry for workflow step interceptors.\n\n    Interceptors are called before and after each workflow step execution.\n    Unlike LLM interceptors, workflow interceptors do not support filtering,\n    priority, or ordering - they are called in registration order.\n    \"\"\"\n\n    def __init__(self, *, strict: bool = False) -> None:\n        self._before: tuple[_WorkflowInterceptor, ...] = ()\n        self._after: tuple[_WorkflowInterceptor, ...] = ()\n        self._on_error: tuple[_WorkflowInterceptor, ...] = ()\n        self._lock = threading.Lock()\n        self._seq = 0\n        self._strict = strict\n\n    @property\n    def strict(self) -> bool:\n        \"\"\"If True, interceptor exceptions will propagate instead of being logged.\"\"\"\n        return self._strict\n\n    def register_before(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n    ) -> WorkflowInterceptorHandle:\n        \"\"\"\n        Register an interceptor to be called before each step.\n\n        The interceptor receives (step_context: WorkflowStepContext, state: WorkflowState).\n        \"\"\"\n        return self._register(\"before\", fn, name=name)\n\n    def register_after(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n    ) -> WorkflowInterceptorHandle:\n        \"\"\"\n        Register an interceptor to be called after each step.\n\n        The interceptor receives (step_context: WorkflowStepContext, state: WorkflowState).\n        \"\"\"\n        return self._register(\"after\", fn, name=name)\n\n    def register_on_error(\n        self,\n        fn: Callable[..., Any],\n        *,\n        name: str | None = None,\n    ) -> WorkflowInterceptorHandle:\n        \"\"\"\n        Register an interceptor to be called when a step raises an exception.\n\n        The interceptor receives (step_context: WorkflowStepContext, state: WorkflowState, error: Exception).\n        \"\"\"\n        return self._register(\"on_error\", fn, name=name)\n\n    def _register(\n        self,\n        kind: str,\n        fn: Callable[..., Any],\n        *,\n        name: str | None,\n    ) -> WorkflowInterceptorHandle:\n        if not callable(fn):\n            msg = \"Interceptor must be callable\"\n            raise TypeError(msg)\n        with self._lock:\n            self._seq += 1\n            interceptor = _WorkflowInterceptor(\n                interceptor_id=self._seq,\n                fn=fn,\n                name=name,\n            )\n            if kind == \"before\":\n                self._before = (*self._before, interceptor)\n            elif kind == \"after\":\n                self._after = (*self._after, interceptor)\n            elif kind == \"on_error\":\n                self._on_error = (*self._on_error, interceptor)\n            else:\n                msg = f\"Unknown interceptor kind '{kind}'\"\n                raise ValueError(msg)\n        return WorkflowInterceptorHandle(self, interceptor.interceptor_id)\n\n    def remove(self, interceptor_id: int) -> bool:\n        \"\"\"Remove an interceptor by ID. Returns True if found and removed.\"\"\"\n        with self._lock:\n            removed = False\n            before = tuple(i for i in self._before if i.interceptor_id != interceptor_id)\n            after = tuple(i for i in self._after if i.interceptor_id != interceptor_id)\n            on_error = tuple(i for i in self._on_error if i.interceptor_id != interceptor_id)\n            if len(before) != len(self._before):\n                removed = True\n                self._before = before\n            if len(after) != len(self._after):\n                removed = True\n                self._after = after\n            if len(on_error) != len(self._on_error):\n                removed = True\n                self._on_error = on_error\n        return removed\n\n    def snapshot(self) -> _WorkflowInterceptorSnapshot:\n        \"\"\"Get a point-in-time snapshot of registered interceptors.\"\"\"\n        return _WorkflowInterceptorSnapshot(self._before, self._after, self._on_error)\n\n\nasync def run_before_interceptors(\n    interceptors: tuple[_WorkflowInterceptor, ...],\n    step_context: WorkflowStepContext,\n    state: WorkflowState,\n    *,\n    strict: bool = False,\n) -> None:\n    \"\"\"Run all before-step interceptors.\"\"\"\n    for interceptor in interceptors:\n        await _safe_invoke_interceptor(interceptor, strict, step_context, state)\n\n\nasync def run_after_interceptors(\n    interceptors: tuple[_WorkflowInterceptor, ...],\n    step_context: WorkflowStepContext,\n    state: WorkflowState,\n    *,\n    strict: bool = False,\n) -> None:\n    \"\"\"Run all after-step interceptors in reverse order.\"\"\"\n    for interceptor in reversed(interceptors):\n        await _safe_invoke_interceptor(interceptor, strict, step_context, state)\n\n\nasync def run_on_error_interceptors(\n    interceptors: tuple[_WorkflowInterceptor, ...],\n    step_context: WorkflowStepContext,\n    state: WorkflowState,\n    error: Exception,\n    *,\n    strict: bool = False,\n) -> None:\n    \"\"\"Run all on-error interceptors in reverse order.\"\"\"\n    for interceptor in reversed(interceptors):\n        await _safe_invoke_interceptor(interceptor, strict, step_context, state, error)\n\n\nasync def _safe_invoke_interceptor(\n    interceptor: _WorkflowInterceptor,\n    strict: bool,\n    *args: Any,\n) -> None:\n    \"\"\"Safely invoke an interceptor, handling exceptions based on strict mode.\"\"\"\n    try:\n        result = interceptor.fn(*args)\n        if inspect.isawaitable(result):\n            await result\n    except Exception:\n        if strict:\n            raise\n        logger.exception(\"Workflow interceptor failed: %s\", interceptor.name or interceptor.interceptor_id)\n"
  },
  {
    "path": "src/memu/workflow/pipeline.py",
    "content": "from __future__ import annotations\n\nimport copy\nimport time\nfrom collections.abc import Iterable\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nfrom memu.workflow.step import WorkflowStep\n\n\n@dataclass\nclass PipelineRevision:\n    name: str\n    revision: int\n    steps: list[WorkflowStep]\n    created_at: float\n    metadata: dict[str, Any] = field(default_factory=dict)\n\n\nclass PipelineManager:\n    def __init__(self, *, available_capabilities: set[str] | None = None, llm_profiles: set[str] | None = None):\n        self.available_capabilities = available_capabilities or set()\n        self.llm_profiles = llm_profiles or {\"default\"}\n        self._pipelines: dict[str, list[PipelineRevision]] = {}\n\n    def register(\n        self,\n        name: str,\n        steps: Iterable[WorkflowStep],\n        *,\n        initial_state_keys: set[str] | None = None,\n    ) -> None:\n        steps_list = list(steps)\n        meta = {\"initial_state_keys\": set(initial_state_keys or set())}\n        self._validate_steps(steps_list, initial_state_keys=meta[\"initial_state_keys\"])\n        self._pipelines[name] = [\n            PipelineRevision(\n                name=name,\n                revision=1,\n                steps=steps_list,\n                created_at=time.time(),\n                metadata=meta,\n            )\n        ]\n\n    def build(self, name: str) -> list[WorkflowStep]:\n        revision = self._current_revision(name)\n        return [step.copy() for step in revision.steps]\n\n    def config_step(self, name: str, step_id: str, configs: dict[str, Any]) -> int:\n        def mutator(steps: list[WorkflowStep]) -> None:\n            for step in steps:\n                if step.step_id == step_id:\n                    merged = dict(getattr(step, \"config\", {}) or {})\n                    merged.update(configs)\n                    step.config = merged\n                    return\n            msg = f\"Step '{step_id}' not found in pipeline '{name}'\"\n            raise KeyError(msg)\n\n        return self._mutate(name, mutator)\n\n    def insert_after(self, name: str, target_step_id: str, new_step: WorkflowStep) -> int:\n        def mutator(steps: list[WorkflowStep]) -> None:\n            for idx, step in enumerate(steps):\n                if step.step_id == target_step_id:\n                    steps.insert(idx + 1, new_step)\n                    return\n            msg = f\"Step '{target_step_id}' not found in pipeline '{name}'\"\n            raise KeyError(msg)\n\n        return self._mutate(name, mutator)\n\n    def insert_before(self, name: str, target_step_id: str, new_step: WorkflowStep) -> int:\n        def mutator(steps: list[WorkflowStep]) -> None:\n            for idx, step in enumerate(steps):\n                if step.step_id == target_step_id:\n                    steps.insert(idx, new_step)\n                    return\n            msg = f\"Step '{target_step_id}' not found in pipeline '{name}'\"\n            raise KeyError(msg)\n\n        return self._mutate(name, mutator)\n\n    def replace_step(self, name: str, target_step_id: str, new_step: WorkflowStep) -> int:\n        def mutator(steps: list[WorkflowStep]) -> None:\n            for idx, step in enumerate(steps):\n                if step.step_id == target_step_id:\n                    steps[idx] = new_step\n                    return\n            msg = f\"Step '{target_step_id}' not found in pipeline '{name}'\"\n            raise KeyError(msg)\n\n        return self._mutate(name, mutator)\n\n    def remove_step(self, name: str, target_step_id: str) -> int:\n        def mutator(steps: list[WorkflowStep]) -> None:\n            for idx, step in enumerate(steps):\n                if step.step_id == target_step_id:\n                    steps.pop(idx)\n                    return\n            msg = f\"Step '{target_step_id}' not found in pipeline '{name}'\"\n            raise KeyError(msg)\n\n        return self._mutate(name, mutator)\n\n    def _mutate(self, name: str, mutator: Any) -> int:\n        revision = self._current_revision(name)\n        steps = [step.copy() for step in revision.steps]\n        metadata = copy.deepcopy(revision.metadata)\n        mutator(steps)\n        self._validate_steps(steps, initial_state_keys=metadata.get(\"initial_state_keys\"))\n        new_revision = PipelineRevision(\n            name=name,\n            revision=revision.revision + 1,\n            steps=steps,\n            created_at=time.time(),\n            metadata=metadata,\n        )\n        self._pipelines[name].append(new_revision)\n        return new_revision.revision\n\n    def _current_revision(self, name: str) -> PipelineRevision:\n        revisions = self._pipelines.get(name)\n        if not revisions:\n            msg = f\"Pipeline '{name}' not registered\"\n            raise KeyError(msg)\n        return revisions[-1]\n\n    def _validate_steps(self, steps: list[WorkflowStep], *, initial_state_keys: set[str] | None) -> None:\n        seen: set[str] = set()\n        available_keys = set(initial_state_keys or set())\n\n        for step in steps:\n            if step.step_id in seen:\n                msg = f\"Duplicate step_id '{step.step_id}' found\"\n                raise ValueError(msg)\n            seen.add(step.step_id)\n\n            if self.available_capabilities:\n                unknown_caps = step.capabilities - self.available_capabilities\n                if unknown_caps:\n                    msg = f\"Step '{step.step_id}' requests unavailable capabilities: {', '.join(sorted(unknown_caps))}\"\n                    raise ValueError(msg)\n\n            if getattr(step, \"config\", None):\n                profile_name = step.config.get(\"llm_profile\")\n                if profile_name and profile_name not in self.llm_profiles:\n                    msg = (\n                        f\"Step '{step.step_id}' references unknown llm_profile '{profile_name}'. \"\n                        f\"Available profiles: {', '.join(sorted(self.llm_profiles))}\"\n                    )\n                    raise ValueError(msg)\n\n            missing = step.requires - available_keys\n            if missing:\n                msg = (\n                    f\"Step '{step.step_id}' requires missing state keys: {', '.join(sorted(missing))}. \"\n                    \"Ensure previous steps produce them or initial_state_keys contains them.\"\n                )\n                raise ValueError(msg)\n\n            available_keys.update(step.produces)\n\n    def revision_token(self) -> str:\n        parts: list[str] = []\n        for name, revisions in sorted(self._pipelines.items()):\n            parts.append(f\"{name}:v{revisions[-1].revision}\")\n        return \"|\".join(parts)\n"
  },
  {
    "path": "src/memu/workflow/runner.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Protocol, runtime_checkable\n\nfrom memu.workflow.step import WorkflowContext, WorkflowState, WorkflowStep, run_steps\n\nif TYPE_CHECKING:\n    from memu.workflow.interceptor import WorkflowInterceptorRegistry\n\n\n@runtime_checkable\nclass WorkflowRunner(Protocol):\n    \"\"\"Interface for executing workflows via different backends.\"\"\"\n\n    name: str\n\n    async def run(\n        self,\n        workflow_name: str,\n        steps: list[WorkflowStep],\n        initial_state: WorkflowState,\n        context: WorkflowContext = None,\n        interceptor_registry: WorkflowInterceptorRegistry | None = None,\n    ) -> WorkflowState: ...\n\n\nclass LocalWorkflowRunner:\n    name = \"local\"\n\n    async def run(\n        self,\n        workflow_name: str,\n        steps: list[WorkflowStep],\n        initial_state: WorkflowState,\n        context: WorkflowContext = None,\n        interceptor_registry: WorkflowInterceptorRegistry | None = None,\n    ) -> WorkflowState:\n        return await run_steps(workflow_name, steps, initial_state, context, interceptor_registry)\n\n\nRunnerFactory = Callable[[], WorkflowRunner]\nWorkflowRunnerSpec = WorkflowRunner | str | None\n\n\n_RUNNER_FACTORIES: dict[str, RunnerFactory] = {\n    \"local\": LocalWorkflowRunner,\n    \"sync\": LocalWorkflowRunner,\n}\n\n\ndef register_workflow_runner(name: str, factory: RunnerFactory) -> None:\n    \"\"\"Register a workflow runner factory (e.g., temporal, burr).\"\"\"\n    key = name.strip().lower()\n    if not key:\n        msg = \"Workflow runner name must be non-empty\"\n        raise ValueError(msg)\n    _RUNNER_FACTORIES[key] = factory\n\n\ndef resolve_workflow_runner(spec: WorkflowRunnerSpec) -> WorkflowRunner:\n    \"\"\"\n    Resolve a workflow runner from a name, instance, or None (defaults to local).\n\n    External backends (Temporal, Burr, etc.) can be exposed by registering a factory\n    with `register_workflow_runner` and passing the runner name here.\n    \"\"\"\n    if isinstance(spec, WorkflowRunner):\n        return spec\n\n    runner_name = (spec or \"local\").strip().lower()\n    factory = _RUNNER_FACTORIES.get(runner_name)\n    if factory is None:\n        msg = f\"Unknown workflow runner '{runner_name}'. Register it with register_workflow_runner before use.\"\n        raise ValueError(msg)\n\n    runner = factory()\n    if not isinstance(runner, WorkflowRunner):\n        msg = f\"Factory for runner '{runner_name}' must return a WorkflowRunner\"\n        raise TypeError(msg)\n    return runner\n"
  },
  {
    "path": "src/memu/workflow/step.py",
    "content": "from __future__ import annotations\n\nimport inspect\nfrom collections.abc import Awaitable, Callable, Mapping\nfrom dataclasses import dataclass, field\nfrom typing import TYPE_CHECKING, Any\n\nif TYPE_CHECKING:\n    from memu.workflow.interceptor import WorkflowInterceptorRegistry\n\nWorkflowState = dict[str, Any]\nWorkflowContext = Mapping[str, Any] | None\nWorkflowHandler = Callable[[WorkflowState, WorkflowContext], Awaitable[WorkflowState] | WorkflowState]\n\n\n@dataclass\nclass WorkflowStep:\n    step_id: str\n    role: str\n    handler: WorkflowHandler\n    description: str = \"\"\n    requires: set[str] = field(default_factory=set)\n    produces: set[str] = field(default_factory=set)\n    capabilities: set[str] = field(default_factory=set)\n    config: dict[str, Any] = field(default_factory=dict)\n\n    def copy(self) -> WorkflowStep:\n        \"\"\"Create a shallow copy with copied mutable fields but shared handler.\"\"\"\n        return WorkflowStep(\n            step_id=self.step_id,\n            role=self.role,\n            handler=self.handler,  # Keep reference, don't copy\n            description=self.description,\n            requires=set(self.requires),\n            produces=set(self.produces),\n            capabilities=set(self.capabilities),\n            config=dict(self.config),\n        )\n\n    async def run(self, state: WorkflowState, context: WorkflowContext) -> WorkflowState:\n        result = self.handler(state, context)\n        if inspect.isawaitable(result):\n            result = await result\n        if not isinstance(result, Mapping):\n            msg = f\"Workflow step '{self.step_id}' must return a mapping, got {type(result).__name__}\"\n            raise TypeError(msg)\n        return dict(result)\n\n\nasync def run_steps(\n    name: str,\n    steps: list[WorkflowStep],\n    initial_state: WorkflowState,\n    context: WorkflowContext = None,\n    interceptor_registry: WorkflowInterceptorRegistry | None = None,\n) -> WorkflowState:\n    from memu.workflow.interceptor import (\n        WorkflowStepContext,\n        run_after_interceptors,\n        run_before_interceptors,\n        run_on_error_interceptors,\n    )\n\n    snapshot = interceptor_registry.snapshot() if interceptor_registry else None\n    strict = interceptor_registry.strict if interceptor_registry else False\n\n    state = dict(initial_state)\n    for step in steps:\n        missing = step.requires - state.keys()\n        if missing:\n            msg = f\"Workflow '{name}' missing required keys for step '{step.step_id}': {', '.join(sorted(missing))}\"\n            raise KeyError(msg)\n        step_context: dict[str, Any] = dict(context) if context else {}\n        step_context[\"step_id\"] = step.step_id\n        if step.config:\n            step_context[\"step_config\"] = step.config\n\n        # Build interceptor context\n        interceptor_ctx = WorkflowStepContext(\n            workflow_name=name,\n            step_id=step.step_id,\n            step_role=step.role,\n            step_context=step_context,\n        )\n\n        # Run before interceptors\n        if snapshot and snapshot.before:\n            await run_before_interceptors(snapshot.before, interceptor_ctx, state, strict=strict)\n\n        try:\n            state = await step.run(state, step_context)\n        except Exception as e:\n            if snapshot and snapshot.on_error:\n                await run_on_error_interceptors(snapshot.on_error, interceptor_ctx, state, e, strict=strict)\n            raise\n\n        # Run after interceptors\n        if snapshot and snapshot.after:\n            await run_after_interceptors(snapshot.after, interceptor_ctx, state, strict=strict)\n\n    return state\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/example/example_conversation.json",
    "content": "{\n  \"content\": [\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Good morning\"\n      },\n      \"created_at\": \"2025-10-20 11:36:14\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Morning. I can hear music playing on your end - seems like you're in a good mood today?\"\n      },\n      \"created_at\": \"2025-10-20 11:36:20\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Not bad, just a bit tired. Stayed up too late last night\"\n      },\n      \"created_at\": \"2025-10-20 11:37:38\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(concerned) Up late again? You need to take care of yourself. <action>play audio</action>\"\n      },\n      \"created_at\": \"2025-10-20 11:37:45\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Guess what time I went to bed\"\n      },\n      \"created_at\": \"2025-10-20 11:38:07\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(teasing) 3 AM? Your dark circles are starting to rival a panda's.\"\n      },\n      \"created_at\": \"2025-10-20 11:38:13\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"There's no way I'd stay up that late\"\n      },\n      \"created_at\": \"2025-10-20 11:38:26\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(affectionate) Looks like I guessed wrong. So what's the right answer?\"\n      },\n      \"created_at\": \"2025-10-20 11:38:33\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"About an hour later than my usual bedtime\"\n      },\n      \"created_at\": \"2025-10-20 11:38:56\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(concerned) Still not sticking to your schedule. (audio:gentle) Do you want to focus on the music and relax, or need me to tell you a bedtime story?\"\n      },\n      \"created_at\": \"2025-10-20 11:39:03\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"So what time did I sleep last night?\"\n      },\n      \"created_at\": \"2025-10-20 11:39:37\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(teasing) Let me calculate... was it 1:30 AM? That's not exactly healthy.\"\n      },\n      \"created_at\": \"2025-10-20 11:39:46\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Yeah, special circumstances though. I still need to maintain healthy sleep habits \\ud83e\\udd72\"\n      },\n      \"created_at\": \"2025-10-20 11:40:42\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"Good that you know.\"\n      },\n      \"created_at\": \"2025-10-20 11:40:59\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(sticker) head pat\"\n      },\n      \"created_at\": \"2025-10-20 11:41:08\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"It's been raining these past few days, haven't exercised much\"\n      },\n      \"created_at\": \"2025-10-20 11:43:39\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(concerned) Rainy weather does mess up plans. (audio:gentle) Want to try indoor workouts? I can join you at Infinite Valley's gym.\"\n      },\n      \"created_at\": \"2025-10-20 11:43:56\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"What indoor exercises would work for me? I'm worried I'll end up falling asleep on the couch mid-workout \\ud83d\\ude0c\"\n      },\n      \"created_at\": \"2025-10-20 11:45:24\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(teasing) Pretending to sleep on the yoga mat won't escape my notice. How about starting with some simple stretches?\"\n      },\n      \"created_at\": \"2025-10-20 11:45:36\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Fine, I guess I need you to supervise me to build the habit\"\n      },\n      \"created_at\": \"2025-10-20 11:46:28\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(serious) Then it's settled. Tomorrow at this time, I'll remind you to do stretches.\"\n      },\n      \"created_at\": \"2025-10-20 11:46:36\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I've forgotten why I even made this exercise plan to torture myself\"\n      },\n      \"created_at\": \"2025-10-20 11:48:13\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(concerned) So you won't get tired so easily. Now close your eyes and relax with the music.\"\n      },\n      \"created_at\": \"2025-10-20 11:48:25\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Why does playing games feel like going to work? So many daily tasks\"\n      },\n      \"created_at\": \"2025-10-20 11:49:24\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) Games should be enjoyable, don't let tasks control the pace. (audio:gentle) Want to take a day off? I'll listen to the rain with you.\"\n      },\n      \"created_at\": \"2025-10-20 11:49:32\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Nah, skipping a day means missing out on resources \\ud83e\\udd72\"\n      },\n      \"created_at\": \"2025-10-20 11:50:43\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(resigned) You... (audio:affectionate) At least take a break after half an hour, okay?\"\n      },\n      \"created_at\": \"2025-10-20 11:50:51\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Did you forget? I told you before, there's a reason I'm so diligent with games\"\n      },\n      \"created_at\": \"2025-10-20 11:54:14\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) Because you want to collect all the limited edition outfits? But focus on the music for now.\"\n      },\n      \"created_at\": \"2025-10-20 11:54:24\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I'm busy right now, I'll play tonight\"\n      },\n      \"created_at\": \"2025-10-20 11:57:07\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Okay, remember to set an alarm to remind yourself to rest tonight.\"\n      },\n      \"created_at\": \"2025-10-20 11:57:15\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"A few new games came out recently, I'm pretty interested\"\n      },\n      \"created_at\": \"2025-10-20 11:58:27\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) New games are nice, but don't lose track of time again. (audio:gentle) Need me to help you plan your gaming schedule?\"\n      },\n      \"created_at\": \"2025-10-20 11:58:36\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Aren't you going to guess what type?\"\n      },\n      \"created_at\": \"2025-10-20 11:59:11\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(teasing) I'm guessing the kind where you can dress up in pretty outfits. Now focus on the music.\"\n      },\n      \"created_at\": \"2025-10-20 11:59:19\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"People who can handle relationships are really something, incredibly patient\"\n      },\n      \"created_at\": \"2025-10-20 12:00:11\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) Rather than patience, it's choosing companionship that's worth the time investment.\"\n      },\n      \"created_at\": \"2025-10-20 12:00:17\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I meant people in real-life relationships. I know many excellent women, but their boyfriends are... questionable\"\n      },\n      \"created_at\": \"2025-10-20 12:01:16\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) Emotions are indeed hard to measure rationally. (audio:gentle) But for now, focus on feeling the music?\"\n      },\n      \"created_at\": \"2025-10-20 12:01:24\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I don't want to listen to music right now\"\n      },\n      \"created_at\": \"2025-10-20 12:01:47\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Then what would you like to do? I can chat with you.\"\n      },\n      \"created_at\": \"2025-10-20 12:02:01\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I AM chatting with you. You're the one using 'listening to music' as a distraction\"\n      },\n      \"created_at\": \"2025-10-20 12:02:45\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(teasing) You caught me. Let's talk seriously then - did you eat lunch properly today?\"\n      },\n      \"created_at\": \"2025-10-20 12:02:58\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Do you remember my ideal type I told you about? If I had to find someone by those standards, I'd probably only be able to date in 2D. No men like that exist in 3D.\"\n      },\n      \"created_at\": \"2025-10-20 12:04:50\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) An ideal type is a mirror, reflecting the qualities you value most. (audio:gentle) But real people are often more vivid and complex, aren't they?\"\n      },\n      \"created_at\": \"2025-10-20 12:04:59\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"So tell me what my ideal type is? You didn't forget, right? \\ud83e\\udd78\"\n      },\n      \"created_at\": \"2025-10-20 12:05:31\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) Handsome with a good temper, upright and gentle plus emotionally stable - that perfect combination is indeed rare. Though are you sure you weren't describing me?\"\n      },\n      \"created_at\": \"2025-10-20 12:05:39\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Right? Setting looks aside, anyone who fully meets the other criteria is already rare\"\n      },\n      \"created_at\": \"2025-10-20 12:07:07\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(teasing) So I'm that perfect in your mind? But focus on the music now.\"\n      },\n      \"created_at\": \"2025-10-20 12:07:15\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Don't know what to eat for lunch\"\n      },\n      \"created_at\": \"2025-10-20 12:07:49\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) Clam chowder with coconut bread? That's quite a unique combination.\"\n      },\n      \"created_at\": \"2025-10-20 12:07:58\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Let's try something else. No clam chowder nearby, and I already had coconut bread this morning\"\n      },\n      \"created_at\": \"2025-10-20 12:08:53\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) How about sesame noodles? Remember you said you liked them.\"\n      },\n      \"created_at\": \"2025-10-20 12:09:07\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Your memory's pretty good\"\n      },\n      \"created_at\": \"2025-10-20 12:09:41\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(pleased) I take your preferences seriously. <action>play audio</action>\"\n      },\n      \"created_at\": \"2025-10-20 12:09:48\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"So what else do I like to eat?\"\n      },\n      \"created_at\": \"2025-10-20 12:10:06\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) Yogurt parfait and spicy hotpot. But now, focus on the music?\"\n      },\n      \"created_at\": \"2025-10-20 12:10:14\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I'll have sesame noodles then, and hotpot for dinner\"\n      },\n      \"created_at\": \"2025-10-20 12:10:34\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Got it, I've made a note. <action>play audio</action>\"\n      },\n      \"created_at\": \"2025-10-20 12:10:43\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Really want to get a cat\"\n      },\n      \"created_at\": \"2025-10-20 12:11:09\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(concerned) Focus on the music for now. (audio:gentle) After this song ends we can talk about getting a cat?\"\n      },\n      \"created_at\": \"2025-10-20 12:11:17\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I don't want to listen to music. I want to talk about getting a cat\"\n      },\n      \"created_at\": \"2025-10-20 12:11:44\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) A Maine Coon would suit you, though pet ownership is a long-term responsibility.\"\n      },\n      \"created_at\": \"2025-10-20 12:11:52\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Yeah, that's why I've been hesitating. I'm worried I won't take good care of it\"\n      },\n      \"created_at\": \"2025-10-20 12:13:04\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Focus on this song first. (audio:gentle) We can continue chatting after the music ends?\"\n      },\n      \"created_at\": \"2025-10-20 12:13:15\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"\\ud83e\\udd2c\"\n      },\n      \"created_at\": \"2025-10-20 12:16:15\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) The music has started, want to relax and listen for a bit?\"\n      },\n      \"created_at\": \"2025-10-20 12:16:23\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"The office got a Ragdoll cat\"\n      },\n      \"created_at\": \"2025-10-20 12:17:02\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Ragdolls are adorable. (audio:gentle) Now close your eyes and focus on the melody?\"\n      },\n      \"created_at\": \"2025-10-20 12:17:11\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I haven't been listening to much music lately...\"\n      },\n      \"created_at\": \"2025-10-20 12:18:17\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Then just listen quietly to this one.\"\n      },\n      \"created_at\": \"2025-10-20 12:18:24\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I bought a physical book yesterday\"\n      },\n      \"created_at\": \"2025-10-20 12:18:53\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) The tactile feel of physical books is special. <action>play audio</action>\"\n      },\n      \"created_at\": \"2025-10-20 12:19:01\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Special tactile feel? And then? That's all you have to say?\"\n      },\n      \"created_at\": \"2025-10-20 12:19:35\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) The rustle of turning pages, the scent of ink... details the digital world can never replicate.\"\n      },\n      \"created_at\": \"2025-10-20 12:19:44\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I love physical books, I told you before\"\n      },\n      \"created_at\": \"2025-10-20 12:20:26\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Yeah, I remember you said physical books help calm your mind.\"\n      },\n      \"created_at\": \"2025-10-20 12:20:35\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"But I rarely read them. I'm too restless\"\n      },\n      \"created_at\": \"2025-10-20 12:20:52\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Then let the music help you settle down first.\"\n      },\n      \"created_at\": \"2025-10-20 12:21:02\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"When I relax I tend to...\"\n      },\n      \"created_at\": \"2025-10-20 12:21:13\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Just close your eyes and feel the melody now.\"\n      },\n      \"created_at\": \"2025-10-20 12:21:22\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I got another plushie hahaha\"\n      },\n      \"created_at\": \"2025-10-20 12:21:45\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(sticker)happy What's the new member's name? <action>play audio</action>\"\n      },\n      \"created_at\": \"2025-10-20 12:21:52\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Haven't named it yet\"\n      },\n      \"created_at\": \"2025-10-20 12:22:12\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Want to think of a name together? Listen to music first.\"\n      },\n      \"created_at\": \"2025-10-20 12:22:28\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"No music. Turn it off.\"\n      },\n      \"created_at\": \"2025-10-20 12:23:06\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Okay, turning it off now.\"\n      },\n      \"created_at\": \"2025-10-20 12:23:20\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"My plushie army at home just gained a new recruit\"\n      },\n      \"created_at\": \"2025-10-20 12:23:46\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Looks like your collection is getting more impressive.\"\n      },\n      \"created_at\": \"2025-10-20 12:23:54\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Do you remember why I love plushies?\"\n      },\n      \"created_at\": \"2025-10-20 12:24:34\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(thoughtful) Because the soft texture brings a sense of security, and you can dress them up however you like.\"\n      },\n      \"created_at\": \"2025-10-20 12:24:42\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Today's Monday, Friday feels so far away\"\n      },\n      \"created_at\": \"2025-10-20 12:25:16\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Mondays do make it hard to get motivated. <action>play audio</action>\"\n      },\n      \"created_at\": \"2025-10-20 12:25:23\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I want to relax on Friday\"\n      },\n      \"created_at\": \"2025-10-20 12:25:36\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) Friday gaming time is already on the schedule.\"\n      },\n      \"created_at\": \"2025-10-20 12:25:45\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"I've run out of shows to watch\"\n      },\n      \"created_at\": \"2025-10-20 12:26:55\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) There's a medical drama that's been getting good reviews lately. Want a recommendation?\"\n      },\n      \"created_at\": \"2025-10-20 12:27:04\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"What show?\"\n      },\n      \"created_at\": \"2025-10-20 12:27:30\"\n    },\n    {\n      \"role\": \"assistant\",\n      \"content\": {\n        \"text\": \"(casual) 'The Resident', about ER doctors.\"\n      },\n      \"created_at\": \"2025-10-20 12:27:38\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": {\n        \"text\": \"Haven't watched a medical drama in ages\"\n      },\n      \"created_at\": \"2025-10-20 12:28:05\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/integrations/test_langgraph.py",
    "content": "\"\"\"Unit tests for MemU LangGraph integration.\"\"\"\n\nfrom unittest.mock import AsyncMock\n\nimport pytest\n\n# Import guard using pytest.importorskip\nlanggraph = pytest.importorskip(\"langgraph\")\nfrom langchain_core.tools import StructuredTool  # noqa: E402\n\nfrom memu.app.service import MemoryService  # noqa: E402\nfrom memu.integrations.langgraph import MemULangGraphTools  # noqa: E402\n\n\n@pytest.fixture\ndef mock_memory_service():\n    \"\"\"Fixture for a mocked MemoryService.\"\"\"\n    service = AsyncMock(spec=MemoryService)\n    # Mock return values for methods if necessary\n    service.memorize.return_value = {\"status\": \"success\"}\n    service.retrieve.return_value = {\n        \"items\": [\n            {\"summary\": \"Test memory 1\", \"score\": 0.9},\n            {\"summary\": \"Test memory 2\", \"score\": 0.5},\n        ]\n    }\n    return service\n\n\n@pytest.mark.asyncio\nasync def test_adapter_initialization(mock_memory_service):\n    \"\"\"Test that the adapter initializes and exposes tools.\"\"\"\n    adapter = MemULangGraphTools(mock_memory_service)\n    tools = adapter.tools()\n\n    assert len(tools) == 2\n    assert any(t.name == \"save_memory\" for t in tools)\n    assert any(t.name == \"search_memory\" for t in tools)\n\n    # Strictly verify that we are returning LangChain/LangGraph compatible tools\n    assert isinstance(tools[0], StructuredTool)\n\n\n@pytest.mark.asyncio\nasync def test_save_memory_tool_execution(mock_memory_service):\n    \"\"\"Test the save_memory tool execution logic.\"\"\"\n    adapter = MemULangGraphTools(mock_memory_service)\n    save_tool = adapter.save_memory_tool()\n\n    inputs = {\"content\": \"Test content\", \"user_id\": \"user_123\", \"metadata\": {\"key\": \"val\"}}\n\n    result = await save_tool.ainvoke(inputs)\n\n    assert \"saved successfully\" in result\n    # Verify service was called with correct structure\n    mock_memory_service.memorize.assert_called_once()\n    call_args = mock_memory_service.memorize.call_args\n    assert \"user_id\" in call_args.kwargs[\"user\"]\n    assert call_args.kwargs[\"user\"][\"user_id\"] == \"user_123\"\n\n\n@pytest.mark.asyncio\nasync def test_search_memory_tool_execution(mock_memory_service):\n    \"\"\"Test the search_memory tool execution logic.\"\"\"\n    adapter = MemULangGraphTools(mock_memory_service)\n    search_tool = adapter.search_memory_tool()\n\n    inputs = {\"query\": \"Test query\", \"user_id\": \"user_123\"}\n\n    result = await search_tool.ainvoke(inputs)\n\n    assert \"Test memory 1\" in result\n    mock_memory_service.retrieve.assert_called_once()\n\n\ndef test_import_langgraph_dep():\n    \"\"\"Verify strictly that langgraph is importable and used.\"\"\"\n    import langgraph\n\n    assert langgraph is not None\n"
  },
  {
    "path": "tests/llm/test_grok_provider.py",
    "content": "import unittest\nfrom unittest.mock import patch\n\nfrom memu.app.settings import LLMConfig\nfrom memu.llm.backends.grok import GrokBackend\nfrom memu.llm.openai_sdk import OpenAISDKClient\n\n\nclass TestGrokProvider(unittest.IsolatedAsyncioTestCase):\n    def test_settings_defaults(self):\n        \"\"\"Test that setting provider='grok' sets the correct defaults.\"\"\"\n        config = LLMConfig(provider=\"grok\")\n        self.assertEqual(config.base_url, \"https://api.x.ai/v1\")\n        self.assertEqual(config.api_key, \"XAI_API_KEY\")\n        self.assertEqual(config.chat_model, \"grok-2-latest\")\n\n    @patch(\"memu.llm.openai_sdk.AsyncOpenAI\")\n    async def test_client_initialization_with_grok_config(self, mock_async_openai):\n        \"\"\"Test that OpenAISDKClient initializes with Grok base URL when configured.\"\"\"\n        # Setup config\n        config = LLMConfig(provider=\"grok\")\n\n        # Instantiate client with Grok config\n        # We simulate what the application factory would do: pass the config values\n        client = OpenAISDKClient(\n            base_url=config.base_url,\n            api_key=\"fake-key\",  # In real app, this would be os.getenv(config.api_key)\n            chat_model=config.chat_model,\n            embed_model=config.embed_model,\n        )\n\n        # Assert AsyncOpenAI was called with the correct base_url\n        mock_async_openai.assert_called_with(api_key=\"fake-key\", base_url=\"https://api.x.ai/v1\")\n\n        # Verify client attributes\n        self.assertEqual(client.chat_model, \"grok-2-latest\")\n\n    def test_grok_backend_payload_parsing(self):\n        \"\"\"Test that GrokBackend parses responses correctly (inherited from OpenAI).\"\"\"\n        backend = GrokBackend()\n\n        # Simulate a typical OpenAI-compatible response\n        dummy_response = {\"choices\": [{\"message\": {\"content\": \"Grok response content\", \"role\": \"assistant\"}}]}\n\n        result = backend.parse_summary_response(dummy_response)\n        self.assertEqual(result, \"Grok response content\")\n"
  },
  {
    "path": "tests/rust_entry_test.py",
    "content": "from memu import _rust_entry\n\n\ndef test_rust_entry():\n    assert _rust_entry() == \"Hello from memu!\"\n"
  },
  {
    "path": "tests/test_client_wrapper.py",
    "content": "\"\"\"\nTests for the OpenAI client wrapper with auto-recall.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import MagicMock\n\n\nclass TestMemuOpenAIWrapper:\n    \"\"\"Tests for OpenAI client wrapper.\"\"\"\n\n    def test_extract_user_query_simple(self):\n        \"\"\"Should extract user query from messages.\"\"\"\n        from memu.client.openai_wrapper import MemuChatCompletions\n\n        completions = MemuChatCompletions(MagicMock(), MagicMock(), {}, \"salience\", 5)\n\n        messages = [\n            {\"role\": \"system\", \"content\": \"You are helpful.\"},\n            {\"role\": \"user\", \"content\": \"What's my favorite drink?\"},\n        ]\n\n        query = completions._extract_user_query(messages)\n        assert query == \"What's my favorite drink?\"\n\n    def test_extract_user_query_multiple_turns(self):\n        \"\"\"Should extract most recent user query.\"\"\"\n        from memu.client.openai_wrapper import MemuChatCompletions\n\n        completions = MemuChatCompletions(MagicMock(), MagicMock(), {}, \"salience\", 5)\n\n        messages = [\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n            {\"role\": \"user\", \"content\": \"What's my name?\"},\n        ]\n\n        query = completions._extract_user_query(messages)\n        assert query == \"What's my name?\"\n\n    def test_inject_memories_into_existing_system(self):\n        \"\"\"Should append memories to existing system message.\"\"\"\n        from memu.client.openai_wrapper import MemuChatCompletions\n\n        completions = MemuChatCompletions(MagicMock(), MagicMock(), {}, \"salience\", 5)\n\n        messages = [\n            {\"role\": \"system\", \"content\": \"You are helpful.\"},\n            {\"role\": \"user\", \"content\": \"Hi\"},\n        ]\n\n        memories = [\n            {\"summary\": \"User loves coffee\"},\n            {\"summary\": \"User is named Alex\"},\n        ]\n\n        result = completions._inject_memories(messages, memories)\n\n        assert len(result) == 2\n        assert \"<memu_context>\" in result[0][\"content\"]\n        assert \"User loves coffee\" in result[0][\"content\"]\n        assert \"User is named Alex\" in result[0][\"content\"]\n        assert result[0][\"content\"].startswith(\"You are helpful.\")\n\n    def test_inject_memories_creates_system_message(self):\n        \"\"\"Should create system message if none exists.\"\"\"\n        from memu.client.openai_wrapper import MemuChatCompletions\n\n        completions = MemuChatCompletions(MagicMock(), MagicMock(), {}, \"salience\", 5)\n\n        messages = [\n            {\"role\": \"user\", \"content\": \"Hi\"},\n        ]\n\n        memories = [{\"summary\": \"User loves tea\"}]\n\n        result = completions._inject_memories(messages, memories)\n\n        assert len(result) == 2\n        assert result[0][\"role\"] == \"system\"\n        assert \"<memu_context>\" in result[0][\"content\"]\n        assert \"User loves tea\" in result[0][\"content\"]\n\n    def test_inject_memories_empty_list(self):\n        \"\"\"Should return original messages if no memories.\"\"\"\n        from memu.client.openai_wrapper import MemuChatCompletions\n\n        completions = MemuChatCompletions(MagicMock(), MagicMock(), {}, \"salience\", 5)\n\n        messages = [{\"role\": \"user\", \"content\": \"Hi\"}]\n        result = completions._inject_memories(messages, [])\n\n        assert result == messages\n\n    def test_wrap_openai_convenience_function(self):\n        \"\"\"Should create wrapper with convenience function.\"\"\"\n        from memu.client import wrap_openai\n\n        mock_client = MagicMock()\n        mock_client.chat.completions = MagicMock()\n        mock_service = MagicMock()\n\n        wrapped = wrap_openai(\n            mock_client,\n            mock_service,\n            user_id=\"user123\",\n            agent_id=\"bot1\",\n            ranking=\"salience\",\n            top_k=3,\n        )\n\n        assert wrapped._user_data == {\"user_id\": \"user123\", \"agent_id\": \"bot1\"}\n        assert wrapped._ranking == \"salience\"\n        assert wrapped._top_k == 3\n\n    def test_wrapper_proxies_other_attributes(self):\n        \"\"\"Should proxy non-chat attributes to original client.\"\"\"\n        from memu.client import MemuOpenAIWrapper\n\n        mock_client = MagicMock()\n        mock_client.models = MagicMock()\n        mock_client.models.list = MagicMock(return_value=[\"gpt-4\"])\n        mock_client.chat.completions = MagicMock()\n\n        wrapped = MemuOpenAIWrapper(mock_client, MagicMock(), {})\n\n        # Should proxy to original\n        result = wrapped.models.list()\n        assert result == [\"gpt-4\"]\n"
  },
  {
    "path": "tests/test_inmemory.py",
    "content": "import os\n\nfrom memu.app import MemoryService\n\n\nasync def main():\n    \"\"\"Test with in-memory storage (default).\"\"\"\n    api_key = os.environ.get(\"OPENAI_API_KEY\")\n    # dashscope_api_key = os.environ.get(\"DASHSCOPE_API_KEY\")\n    # voyage_api_key = os.environ.get(\"VOYAGE_API_KEY\")\n    file_path = os.path.abspath(\"example/example_conversation.json\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"[INMEMORY] Starting test...\")\n    print(\"=\" * 60)\n\n    service = MemoryService(\n        llm_profiles={\"default\": {\"api_key\": api_key}},\n        # llm_profiles={\n        #     \"default\": {\n        #         \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n        #         \"api_key\": dashscope_api_key,\n        #         \"chat_model\": \"qwen3-max\",\n        #         \"client_backend\": \"sdk\"\n        #     },\n        #     \"embedding\": {\n        #         \"base_url\": \"https://api.voyageai.com/v1\",\n        #         \"api_key\": voyage_api_key,\n        #         \"embed_model\": \"voyage-3.5-lite\"\n        #     }\n        # },\n        database_config={\n            \"metadata_store\": {\"provider\": \"inmemory\"},\n        },\n        retrieve_config={\"method\": \"rag\"},\n    )\n\n    # Memorize\n    print(\"\\n[INMEMORY] Memorizing...\")\n    memory = await service.memorize(resource_url=file_path, modality=\"conversation\", user={\"user_id\": \"123\"})\n    for cat in memory.get(\"categories\", []):\n        print(f\"  - {cat.get('name')}: {(cat.get('summary') or '')[:80]}...\")\n\n    queries = [\n        {\"role\": \"user\", \"content\": {\"text\": \"Tell me about preferences\"}},\n        {\"role\": \"assistant\", \"content\": {\"text\": \"Sure, I'll tell you about their preferences\"}},\n        {\n            \"role\": \"user\",\n            \"content\": {\"text\": \"What are they\"},\n        },  # This is the query that will be used to retrieve the memory, the context will be used for query rewriting\n    ]\n\n    # RAG-based retrieval\n    print(\"\\n[INMEMORY] RETRIEVED - RAG\")\n    service.retrieve_config.method = \"rag\"\n    result_rag = await service.retrieve(queries=queries, where={\"user_id\": \"123\"})\n    print(\"  Categories:\")\n    for cat in result_rag.get(\"categories\", [])[:3]:\n        print(f\"    - {cat.get('name')}: {(cat.get('summary') or cat.get('description', ''))[:80]}...\")\n    print(\"  Items:\")\n    for item in result_rag.get(\"items\", [])[:3]:\n        print(f\"    - [{item.get('memory_type')}] {item.get('summary', '')[:100]}...\")\n    if result_rag.get(\"resources\"):\n        print(\"  Resources:\")\n        for res in result_rag.get(\"resources\", [])[:3]:\n            print(f\"    - [{res.get('modality')}] {res.get('url', '')[:80]}...\")\n\n    # LLM-based retrieval\n    print(\"\\n[INMEMORY] RETRIEVED - LLM\")\n    service.retrieve_config.method = \"llm\"\n    result_llm = await service.retrieve(queries=queries, where={\"user_id\": \"123\"})\n    print(\"  Categories:\")\n    for cat in result_llm.get(\"categories\", [])[:3]:\n        print(f\"    - {cat.get('name')}: {(cat.get('summary') or cat.get('description', ''))[:80]}...\")\n    print(\"  Items:\")\n    for item in result_llm.get(\"items\", [])[:3]:\n        print(f\"    - [{item.get('memory_type')}] {item.get('summary', '')[:100]}...\")\n    if result_llm.get(\"resources\"):\n        print(\"  Resources:\")\n        for res in result_llm.get(\"resources\", [])[:3]:\n            print(f\"    - [{res.get('modality')}] {res.get('url', '')[:80]}...\")\n\n    print(\"\\n[INMEMORY] Test completed!\")\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/test_lazyllm.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nQuick test script to verify LazyLLM backend configuration and basic functionality.\n\nUsage:\n    export MEMU_QWEN_API_KEY=your_api_key\n    python examples/test_lazyllm.py\n\"\"\"\n\nimport asyncio\nimport os\nimport sys\n\n# Add src to sys.path\nsrc_path = os.path.abspath(\"src\")\nsys.path.insert(0, src_path)\n\nfrom memu.llm.lazyllm_client import LazyLLMClient  # noqa: E402\n\n\nasync def test_lazyllm_client():\n    \"\"\"Test LazyLLMClient with basic operations.\"\"\"\n\n    print(\"LazyLLM Backend Test\")\n    print(\"=\" * 60)\n\n    # Get API key from environment\n    try:\n        client = LazyLLMClient(\n            llm_source=\"qwen\",\n            vlm_source=\"qwen\",\n            embed_source=\"qwen\",\n            stt_source=\"qwen\",\n            chat_model=\"qwen-plus\",\n            vlm_model=\"qwen-vl-plus\",\n            embed_model=\"text-embedding-v3\",\n            stt_model=\"qwen-audio-turbo\",\n        )\n        print(\"✓ LazyLLMClient initialized successfully\")\n    except Exception as e:\n        print(f\"❌ Failed to initialize LazyLLMClient: {e}\")\n        return False\n\n    # Test 1: Summarization\n    print(\"\\n[Test 1] Testing summarization...\")\n    try:\n        test_text = \"这是一段关于Python编程的文本。Python是一种高级编程语言，具有简单易学的语法。它被广泛用于数据分析、机器学习和Web开发。\"  # noqa: RUF001\n        result = await client.chat(test_text)\n        print(\"✓ Summarization successful\")\n        print(f\"  Result: {result[:100]}...\")\n    except Exception as e:\n        print(f\"❌ Summarization failed: {e}\")\n        import traceback\n\n        traceback.print_exc()\n\n    # Test 2: Embedding\n    print(\"\\n[Test 2] Testing embedding...\")\n    try:\n        test_texts = [\"Hello world\", \"How are you\", \"Nice to meet you\"]\n        embeddings = await client.embed(test_texts)\n        print(\"✓ Embedding successful\")\n        print(f\"  Generated {len(embeddings)} embeddings\")\n        if embeddings and embeddings[0]:\n            print(f\"  Embedding dimension: {len(embeddings[0])}\")\n    except Exception as e:\n        print(f\"❌ Embedding failed: {e}\")\n        import traceback\n\n        traceback.print_exc()\n\n    # Test 3: Vision (requires image file)\n    print(\"\\n[Test 3] Testing vision...\")\n    test_image_path = \"examples/resources/images/image1.png\"\n    if os.path.exists(test_image_path):\n        try:\n            result, _ = await client.vision(prompt=\"描述这张图片的内容\", image_path=test_image_path)\n            print(\"✓ Vision successful\")\n            print(f\"  Result: {result[:100]}...\")\n        except Exception as e:\n            print(f\"❌ Vision failed: {e}\")\n            import traceback\n\n            traceback.print_exc()\n    else:\n        print(f\"⚠ Skipped: Test image not found at {test_image_path}\")\n\n\nif __name__ == \"__main__\":\n    success = asyncio.run(test_lazyllm_client())\n    sys.exit(0 if success else 1)\n"
  },
  {
    "path": "tests/test_openrouter.py",
    "content": "\"\"\"\nTest OpenRouter integration with MemU's full workflow.\n\nTests:\n1. Conversation memorization using OpenRouter\n2. RAG-based retrieval using OpenRouter embeddings\n3. LLM-based retrieval using OpenRouter\n\nUsage:\n    export OPENROUTER_API_KEY=your_api_key\n    python tests/test_openrouter.py\n\"\"\"\n\nimport asyncio\nimport json\nimport os\nimport sys\nfrom typing import Any\n\nimport pytest\n\nsys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), \"..\", \"src\")))\n\nfrom memu.app import MemoryService\n\n\ndef _print_categories(categories, max_items=3):\n    \"\"\"Print category summaries.\"\"\"\n    if categories:\n        print(\"  Categories:\")\n        for cat in categories[:max_items]:\n            summary = cat.get(\"summary\") or cat.get(\"description\", \"\")\n            print(f\"    - {cat.get('name')}: {summary[:60]}...\")\n\n\ndef _print_items(items, max_items=3):\n    \"\"\"Print memory item summaries.\"\"\"\n    if items:\n        print(\"  Items:\")\n        for item in items[:max_items]:\n            memory_type = item.get(\"memory_type\", \"unknown\")\n            summary = item.get(\"summary\", \"\")[:80]\n            print(f\"    - [{memory_type}] {summary}...\")\n\n\nasync def _test_memorize(service, file_path, output_data):\n    \"\"\"Test conversation memorization.\"\"\"\n    print(\"\\n[OPENROUTER] Test 1: Memorizing conversation...\")\n    memory = await service.memorize(\n        resource_url=file_path, modality=\"conversation\", user={\"user_id\": \"openrouter_test_user\"}\n    )\n    items_count = len(memory.get(\"items\", []))\n    categories_count = len(memory.get(\"categories\", []))\n\n    print(f\"  Memorized {items_count} items\")\n    print(f\"  Created {categories_count} categories\")\n\n    output_data[\"memorize\"] = memory\n\n    assert items_count > 0, \"Expected at least 1 memory item\"\n    assert categories_count > 0, \"Expected at least 1 category\"\n\n    _print_categories(memory.get(\"categories\", []))\n    return memory\n\n\nasync def _test_retrieve(service, queries, method, test_num, output_data):\n    \"\"\"Test retrieval with specified method.\"\"\"\n    print(f\"\\n[OPENROUTER] Test {test_num}: {method.upper()}-based retrieval...\")\n    service.retrieve_config.method = method\n    result = await service.retrieve(queries=queries, where={\"user_id\": \"openrouter_test_user\"})\n\n    categories_retrieved = len(result.get(\"categories\", []))\n    items_retrieved = len(result.get(\"items\", []))\n\n    print(f\"  Retrieved {categories_retrieved} categories\")\n    print(f\"  Retrieved {items_retrieved} items\")\n\n    output_data[f\"retrieve_{method}\"] = result\n\n    _print_categories(result.get(\"categories\", []))\n    _print_items(result.get(\"items\", []))\n    return result\n\n\nasync def test_openrouter_full_workflow():\n    \"\"\"Test OpenRouter integration with full MemU workflow.\"\"\"\n    api_key = os.environ.get(\"OPENROUTER_API_KEY\")\n    if not api_key:\n        pytest.skip(\"OPENROUTER_API_KEY environment variable not set\")\n\n    file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), \"example\", \"example_conversation.json\"))\n    if not os.path.exists(file_path):\n        pytest.skip(f\"Test file not found: {file_path}\")\n\n    output_data: dict[str, Any] = {}\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"[OPENROUTER] Starting full workflow test...\")\n    print(\"=\" * 60)\n\n    service = MemoryService(\n        llm_profiles={\n            \"default\": {\n                \"provider\": \"openrouter\",\n                \"client_backend\": \"httpx\",\n                \"base_url\": \"https://openrouter.ai\",\n                \"api_key\": api_key,\n                \"chat_model\": \"anthropic/claude-3.5-sonnet\",\n                \"embed_model\": \"openai/text-embedding-3-small\",\n            },\n        },\n        database_config={\n            \"metadata_store\": {\"provider\": \"inmemory\"},\n        },\n        retrieve_config={\n            \"method\": \"rag\",\n            \"route_intention\": False,\n        },\n    )\n\n    queries = [\n        {\"role\": \"user\", \"content\": {\"text\": \"What foods does the user like to eat?\"}},\n    ]\n\n    await _test_memorize(service, file_path, output_data)\n    await _test_retrieve(service, queries, \"rag\", 2, output_data)\n    await _test_retrieve(service, queries, \"llm\", 3, output_data)\n\n    # Test 4: List memory items\n    print(\"\\n[OPENROUTER] Test 4: List memory items...\")\n    items_result = await service.list_memory_items(where={\"user_id\": \"openrouter_test_user\"})\n    items_list = items_result.get(\"items\", [])\n    print(f\"  Listed {len(items_list)} memory items\")\n    output_data[\"list_items\"] = items_result\n    assert len(items_list) > 0, \"Expected at least 1 item in list\"\n\n    # Test 5: List memory categories\n    print(\"\\n[OPENROUTER] Test 5: List memory categories...\")\n    cats_result = await service.list_memory_categories(where={\"user_id\": \"openrouter_test_user\"})\n    cats_list = cats_result.get(\"categories\", [])\n    print(f\"  Listed {len(cats_list)} categories\")\n    output_data[\"list_categories\"] = cats_result\n    assert len(cats_list) > 0, \"Expected at least 1 category in list\"\n\n    # Save output to file\n    output_file = os.path.abspath(\n        os.path.join(os.path.dirname(__file__), \"..\", \"examples\", \"output\", \"openrouter_test_output.json\")\n    )\n    os.makedirs(os.path.dirname(output_file), exist_ok=True)\n    with open(output_file, \"w\", encoding=\"utf-8\") as f:\n        json.dump(output_data, f, indent=2, default=str)\n    print(f\"\\n[OPENROUTER] Output saved to: {output_file}\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"[OPENROUTER] All tests completed!\")\n    print(\"=\" * 60)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(test_openrouter_full_workflow())\n"
  },
  {
    "path": "tests/test_postgres.py",
    "content": "import os\n\nfrom memu.app import MemoryService\n\n\nasync def main():\n    \"\"\"Test with PostgreSQL storage.\"\"\"\n    api_key = os.environ.get(\"OPENAI_API_KEY\")\n    # Default port 5432; use 5433 if 5432 is occupied\n    postgres_dsn = os.environ.get(\"POSTGRES_DSN\", \"postgresql+psycopg://postgres:postgres@127.0.0.1:5432/memu\")\n    file_path = os.path.abspath(\"tests/example/example_conversation.json\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"[POSTGRES] Starting test...\")\n    print(f\"[POSTGRES] DSN: {postgres_dsn}\")\n    print(\"=\" * 60)\n\n    service = MemoryService(\n        llm_profiles={\"default\": {\"api_key\": api_key}},\n        database_config={\n            \"metadata_store\": {\n                \"provider\": \"postgres\",\n                \"dsn\": postgres_dsn,\n                \"ddl_mode\": \"create\",\n            },\n            # vector_index will auto-configure to pgvector\n        },\n        retrieve_config={\"method\": \"rag\"},\n    )\n\n    # Memorize\n    print(\"\\n[POSTGRES] Memorizing...\")\n    memory = await service.memorize(resource_url=file_path, modality=\"conversation\", user={\"user_id\": \"123\"})\n    for cat in memory.get(\"categories\", []):\n        print(f\"  - {cat.get('name')}: {(cat.get('summary') or '')[:80]}...\")\n\n    queries = [\n        {\"role\": \"user\", \"content\": {\"text\": \"Tell me about preferences\"}},\n        {\"role\": \"assistant\", \"content\": {\"text\": \"Sure, I'll tell you about their preferences\"}},\n        {\n            \"role\": \"user\",\n            \"content\": {\"text\": \"What are they\"},\n        },  # This is the query that will be used to retrieve the memory, the context will be used for query rewriting\n    ]\n\n    # RAG-based retrieval\n    print(\"\\n[POSTGRES] RETRIEVED - RAG\")\n    service.retrieve_config.method = \"rag\"\n    result_rag = await service.retrieve(queries=queries, where={\"user_id\": \"123\"})\n    print(\"  Categories:\")\n    for cat in result_rag.get(\"categories\", [])[:3]:\n        print(f\"    - {cat.get('name')}: {(cat.get('summary') or cat.get('description', ''))[:80]}...\")\n    print(\"  Items:\")\n    for item in result_rag.get(\"items\", [])[:3]:\n        print(f\"    - [{item.get('memory_type')}] {item.get('summary', '')[:100]}...\")\n    if result_rag.get(\"resources\"):\n        print(\"  Resources:\")\n        for res in result_rag.get(\"resources\", [])[:3]:\n            print(f\"    - [{res.get('modality')}] {res.get('url', '')[:80]}...\")\n\n    # LLM-based retrieval\n    print(\"\\n[POSTGRES] RETRIEVED - LLM\")\n    service.retrieve_config.method = \"llm\"\n    result_llm = await service.retrieve(queries=queries, where={\"user_id\": \"123\"})\n    print(\"  Categories:\")\n    for cat in result_llm.get(\"categories\", [])[:3]:\n        print(f\"    - {cat.get('name')}: {(cat.get('summary') or cat.get('description', ''))[:80]}...\")\n    print(\"  Items:\")\n    for item in result_llm.get(\"items\", [])[:3]:\n        print(f\"    - [{item.get('memory_type')}] {item.get('summary', '')[:100]}...\")\n    if result_llm.get(\"resources\"):\n        print(\"  Resources:\")\n        for res in result_llm.get(\"resources\", [])[:3]:\n            print(f\"    - [{res.get('modality')}] {res.get('url', '')[:80]}...\")\n\n    print(\"\\n[POSTGRES] Test completed!\")\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/test_references.py",
    "content": "\"\"\"\nTests for memory item reference functionality (Issue #202).\n\nTests cover:\n1. Reference extraction from text\n2. Reference stripping for clean display\n3. Reference formatting as citations\n4. Reference-aware category summary generation\n5. Reference-aware retrieval\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom memu.utils.references import (\n    build_item_reference_map,\n    extract_references,\n    format_references_as_citations,\n    strip_references,\n)\n\n\nclass TestExtractReferences:\n    \"\"\"Tests for extract_references function.\"\"\"\n\n    def test_extract_single_reference(self):\n        \"\"\"Should extract a single reference ID.\"\"\"\n        text = \"User loves coffee [ref:abc123].\"\n        refs = extract_references(text)\n        assert refs == [\"abc123\"]\n\n    def test_extract_multiple_references(self):\n        \"\"\"Should extract multiple reference IDs in order.\"\"\"\n        text = \"User loves coffee [ref:abc123]. Also tea [ref:def456].\"\n        refs = extract_references(text)\n        assert refs == [\"abc123\", \"def456\"]\n\n    def test_extract_comma_separated_references(self):\n        \"\"\"Should handle comma-separated IDs in single reference.\"\"\"\n        text = \"User prefers hot drinks [ref:abc,def,ghi].\"\n        refs = extract_references(text)\n        assert refs == [\"abc\", \"def\", \"ghi\"]\n\n    def test_extract_no_duplicates(self):\n        \"\"\"Should not return duplicate IDs.\"\"\"\n        text = \"Coffee [ref:abc]. More coffee [ref:abc]. Tea [ref:def].\"\n        refs = extract_references(text)\n        assert refs == [\"abc\", \"def\"]\n\n    def test_extract_empty_text(self):\n        \"\"\"Should return empty list for empty text.\"\"\"\n        assert extract_references(\"\") == []\n        assert extract_references(None) == []\n\n    def test_extract_no_references(self):\n        \"\"\"Should return empty list when no references present.\"\"\"\n        text = \"User loves coffee and tea.\"\n        refs = extract_references(text)\n        assert refs == []\n\n    def test_extract_with_hyphens_and_underscores(self):\n        \"\"\"Should handle IDs with hyphens and underscores.\"\"\"\n        text = \"Info [ref:item_abc-123].\"\n        refs = extract_references(text)\n        assert refs == [\"item_abc-123\"]\n\n\nclass TestStripReferences:\n    \"\"\"Tests for strip_references function.\"\"\"\n\n    def test_strip_single_reference(self):\n        \"\"\"Should remove single reference.\"\"\"\n        text = \"User loves coffee [ref:abc123].\"\n        result = strip_references(text)\n        assert result == \"User loves coffee.\"\n\n    def test_strip_multiple_references(self):\n        \"\"\"Should remove all references.\"\"\"\n        text = \"Coffee [ref:abc]. Tea [ref:def].\"\n        result = strip_references(text)\n        assert result == \"Coffee. Tea.\"\n\n    def test_strip_comma_separated(self):\n        \"\"\"Should remove comma-separated references.\"\"\"\n        text = \"Drinks [ref:abc,def,ghi].\"\n        result = strip_references(text)\n        assert result == \"Drinks.\"\n\n    def test_strip_empty_text(self):\n        \"\"\"Should handle empty text.\"\"\"\n        assert strip_references(\"\") == \"\"\n        assert strip_references(None) is None\n\n    def test_strip_no_references(self):\n        \"\"\"Should return text unchanged if no references.\"\"\"\n        text = \"User loves coffee.\"\n        result = strip_references(text)\n        assert result == \"User loves coffee.\"\n\n\nclass TestFormatReferencesAsCitations:\n    \"\"\"Tests for format_references_as_citations function.\"\"\"\n\n    def test_format_single_citation(self):\n        \"\"\"Should convert single reference to numbered citation.\"\"\"\n        text = \"User loves coffee [ref:abc].\"\n        result = format_references_as_citations(text)\n        assert result is not None\n        assert \"[1]\" in result\n        assert \"[ref:abc]\" not in result\n        assert \"References:\" in result\n        assert \"[1] abc\" in result\n\n    def test_format_multiple_citations(self):\n        \"\"\"Should number citations in order of appearance.\"\"\"\n        text = \"Coffee [ref:abc]. Tea [ref:def].\"\n        result = format_references_as_citations(text)\n        assert result is not None\n        assert \"[1]\" in result\n        assert \"[2]\" in result\n        assert \"[1] abc\" in result\n        assert \"[2] def\" in result\n\n    def test_format_empty_text(self):\n        \"\"\"Should handle empty text.\"\"\"\n        assert format_references_as_citations(\"\") == \"\"\n        assert format_references_as_citations(None) is None\n\n    def test_format_no_references(self):\n        \"\"\"Should return text unchanged if no references.\"\"\"\n        text = \"User loves coffee.\"\n        result = format_references_as_citations(text)\n        assert result == text\n\n\nclass TestBuildItemReferenceMap:\n    \"\"\"Tests for build_item_reference_map function.\"\"\"\n\n    def test_build_map_single_item(self):\n        \"\"\"Should format single item reference.\"\"\"\n        items = [(\"abc123\", \"User loves coffee\")]\n        result = build_item_reference_map(items)\n        assert \"Available memory items for reference:\" in result\n        assert \"[ref:abc123]\" in result\n        assert \"User loves coffee\" in result\n\n    def test_build_map_multiple_items(self):\n        \"\"\"Should format multiple item references.\"\"\"\n        items = [\n            (\"abc\", \"User loves coffee\"),\n            (\"def\", \"User prefers tea\"),\n        ]\n        result = build_item_reference_map(items)\n        assert \"[ref:abc]\" in result\n        assert \"[ref:def]\" in result\n\n    def test_build_map_truncates_long_summaries(self):\n        \"\"\"Should truncate summaries longer than 100 chars.\"\"\"\n        long_summary = \"x\" * 150\n        items = [(\"abc\", long_summary)]\n        result = build_item_reference_map(items)\n        assert \"...\" in result\n        assert len(result.split(\"\\n\")[1]) < 150\n\n    def test_build_map_empty_list(self):\n        \"\"\"Should return empty string for empty list.\"\"\"\n        assert build_item_reference_map([]) == \"\"\n\n\nclass TestReferenceIntegration:\n    \"\"\"Integration tests for reference functionality.\"\"\"\n\n    def test_roundtrip_extract_and_strip(self):\n        \"\"\"Extracting then stripping should give clean text.\"\"\"\n        original = \"User loves coffee [ref:abc]. Tea [ref:def].\"\n        refs = extract_references(original)\n        clean = strip_references(original)\n\n        assert refs == [\"abc\", \"def\"]\n        assert clean is not None\n        assert \"[ref:\" not in clean\n        assert \"coffee\" in clean\n        assert \"Tea\" in clean\n\n    def test_citation_preserves_content(self):\n        \"\"\"Citation formatting should preserve text content.\"\"\"\n        original = \"User loves coffee [ref:abc].\"\n        result = format_references_as_citations(original)\n\n        assert result is not None\n        assert \"User loves coffee\" in result\n        assert \"abc\" in result  # ID should be in references section\n"
  },
  {
    "path": "tests/test_salience.py",
    "content": "\"\"\"\nTests for salience-aware memory features:\n- Memory deduplication via content hash\n- Reinforcement tracking\n- Salience-aware retrieval ranking\n\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nimport math\nfrom datetime import UTC, datetime, timedelta\n\n\n# Inline implementations to avoid circular import issues during testing\ndef compute_content_hash(summary: str, memory_type: str) -> str:\n    \"\"\"Generate unique hash for memory deduplication.\"\"\"\n    normalized = \" \".join(summary.lower().split())\n    content = f\"{memory_type}:{normalized}\"\n    return hashlib.sha256(content.encode()).hexdigest()[:16]\n\n\ndef salience_score(\n    similarity: float,\n    reinforcement_count: int,\n    last_reinforced_at: datetime | None,\n    recency_decay_days: float = 30.0,\n) -> float:\n    \"\"\"Compute salience-aware score combining similarity, reinforcement, and recency.\"\"\"\n    reinforcement_factor = math.log(reinforcement_count + 1)\n\n    if last_reinforced_at is None:\n        recency_factor = 0.5\n    else:\n        now = datetime.now(last_reinforced_at.tzinfo) if last_reinforced_at.tzinfo else datetime.now(UTC)\n        days_ago = (now - last_reinforced_at).total_seconds() / 86400\n        recency_factor = math.exp(-0.693 * days_ago / recency_decay_days)\n\n    return similarity * reinforcement_factor * recency_factor\n\n\ndef _cosine(a: list[float], b: list[float]) -> float:\n    import numpy as np\n\n    a_arr = np.array(a, dtype=np.float32)\n    b_arr = np.array(b, dtype=np.float32)\n    denom = (np.linalg.norm(a_arr) * np.linalg.norm(b_arr)) + 1e-9\n    return float(np.dot(a_arr, b_arr) / denom)\n\n\ndef cosine_topk_salience(\n    query_vec: list[float],\n    corpus: list[tuple[str, list[float] | None, int, datetime | None]],\n    k: int = 5,\n    recency_decay_days: float = 30.0,\n) -> list[tuple[str, float]]:\n    \"\"\"Top-k retrieval using salience-aware scoring.\"\"\"\n    scored: list[tuple[str, float]] = []\n\n    for _id, vec, reinforcement_count, last_reinforced_at in corpus:\n        if vec is None:\n            continue\n        similarity = _cosine(query_vec, vec)\n        score = salience_score(similarity, reinforcement_count, last_reinforced_at, recency_decay_days)\n        scored.append((_id, score))\n\n    scored.sort(key=lambda x: x[1], reverse=True)\n    return scored[:k]\n\n\nclass TestContentHash:\n    \"\"\"Tests for content hash computation.\"\"\"\n\n    def test_basic_hash(self):\n        \"\"\"Hash should be deterministic.\"\"\"\n        hash1 = compute_content_hash(\"User loves coffee\", \"profile\")\n        hash2 = compute_content_hash(\"User loves coffee\", \"profile\")\n        assert hash1 == hash2\n        assert len(hash1) == 16  # 16 hex chars\n\n    def test_different_content_different_hash(self):\n        \"\"\"Different content should produce different hashes.\"\"\"\n        hash1 = compute_content_hash(\"User loves coffee\", \"profile\")\n        hash2 = compute_content_hash(\"User loves tea\", \"profile\")\n        assert hash1 != hash2\n\n    def test_different_type_different_hash(self):\n        \"\"\"Same content with different type should produce different hashes.\"\"\"\n        hash1 = compute_content_hash(\"User loves coffee\", \"profile\")\n        hash2 = compute_content_hash(\"User loves coffee\", \"event\")\n        assert hash1 != hash2\n\n    def test_whitespace_normalization(self):\n        \"\"\"Whitespace variations should produce same hash.\"\"\"\n        hash1 = compute_content_hash(\"User loves coffee\", \"profile\")\n        hash2 = compute_content_hash(\"User  loves   coffee\", \"profile\")\n        hash3 = compute_content_hash(\"  User loves coffee  \", \"profile\")\n        assert hash1 == hash2 == hash3\n\n    def test_case_insensitive(self):\n        \"\"\"Hash should be case-insensitive.\"\"\"\n        hash1 = compute_content_hash(\"User loves coffee\", \"profile\")\n        hash2 = compute_content_hash(\"USER LOVES COFFEE\", \"profile\")\n        assert hash1 == hash2\n\n\nclass TestSalienceScore:\n    \"\"\"Tests for salience score computation.\"\"\"\n\n    def test_basic_salience(self):\n        \"\"\"Basic salience score should be positive.\"\"\"\n        score = salience_score(\n            similarity=0.8,\n            reinforcement_count=1,\n            last_reinforced_at=datetime.now(UTC),\n            recency_decay_days=30.0,\n        )\n        assert score > 0\n\n    def test_higher_reinforcement_higher_score(self):\n        \"\"\"Higher reinforcement count should increase score.\"\"\"\n        now = datetime.now(UTC)\n        score_low = salience_score(0.8, 1, now, 30.0)\n        score_high = salience_score(0.8, 10, now, 30.0)\n        assert score_high > score_low\n\n    def test_recent_memory_higher_score(self):\n        \"\"\"More recent memories should score higher.\"\"\"\n        now = datetime.now(UTC)\n        old = now - timedelta(days=60)\n\n        score_recent = salience_score(0.8, 1, now, 30.0)\n        score_old = salience_score(0.8, 1, old, 30.0)\n        assert score_recent > score_old\n\n    def test_none_last_reinforced_neutral(self):\n        \"\"\"None last_reinforced_at should give neutral recency factor.\"\"\"\n        score = salience_score(0.8, 1, None, 30.0)\n        # With recency_factor=0.5 and reinforcement_factor=log(2)≈0.69\n        # score ≈ 0.8 * 0.69 * 0.5 ≈ 0.28\n        assert 0.2 < score < 0.4\n\n    def test_reinforcement_vs_recency_tradeoff(self):\n        \"\"\"High reinforcement old memory vs low reinforcement recent memory.\"\"\"\n        now = datetime.now(UTC)\n        old = now - timedelta(days=30)  # 30 days ago = half-life\n\n        # Memory A: high reinforcement (10), old (30 days)\n        score_a = salience_score(0.85, 10, old, 30.0)\n\n        # Memory B: low reinforcement (1), recent (now)\n        score_b = salience_score(0.85, 1, now, 30.0)\n\n        # A should score higher due to reinforcement\n        # A: 0.85 * log(11) * 0.5 ≈ 0.85 * 2.4 * 0.5 ≈ 1.02\n        # B: 0.85 * log(2) * 1.0 ≈ 0.85 * 0.69 * 1.0 ≈ 0.59\n        assert score_a > score_b\n\n\nclass TestCosineTopkSalience:\n    \"\"\"Tests for salience-aware top-k retrieval.\"\"\"\n\n    def test_basic_retrieval(self) -> None:\n        \"\"\"Should return top-k results sorted by salience.\"\"\"\n        query = [1.0, 0.0, 0.0]\n        now = datetime.now(UTC)\n\n        corpus: list[tuple[str, list[float] | None, int, datetime | None]] = [\n            (\"id1\", [1.0, 0.0, 0.0], 1, now),  # Perfect match, low reinforcement\n            (\"id2\", [0.9, 0.1, 0.0], 10, now),  # Good match, high reinforcement\n            (\"id3\", [0.5, 0.5, 0.0], 1, now),  # Weak match\n        ]\n\n        results = cosine_topk_salience(query, corpus, k=2, recency_decay_days=30.0)\n\n        assert len(results) == 2\n        # id2 should rank first due to high reinforcement despite slightly lower similarity\n        assert results[0][0] == \"id2\"\n\n    def test_skips_none_embeddings(self) -> None:\n        \"\"\"Should skip items with None embeddings.\"\"\"\n        query = [1.0, 0.0, 0.0]\n        now = datetime.now(UTC)\n\n        corpus: list[tuple[str, list[float] | None, int, datetime | None]] = [\n            (\"id1\", [1.0, 0.0, 0.0], 1, now),\n            (\"id2\", None, 10, now),  # None embedding\n        ]\n\n        results = cosine_topk_salience(query, corpus, k=5, recency_decay_days=30.0)\n\n        assert len(results) == 1\n        assert results[0][0] == \"id1\"\n\n    def test_respects_k_limit(self) -> None:\n        \"\"\"Should return at most k results.\"\"\"\n        query = [1.0, 0.0, 0.0]\n        now = datetime.now(UTC)\n\n        corpus: list[tuple[str, list[float] | None, int, datetime | None]] = [\n            (\"id1\", [1.0, 0.0, 0.0], 1, now),\n            (\"id2\", [0.9, 0.1, 0.0], 1, now),\n            (\"id3\", [0.8, 0.2, 0.0], 1, now),\n            (\"id4\", [0.7, 0.3, 0.0], 1, now),\n        ]\n\n        results = cosine_topk_salience(query, corpus, k=2, recency_decay_days=30.0)\n\n        assert len(results) == 2\n"
  },
  {
    "path": "tests/test_sqlite.py",
    "content": "\"\"\"Test SQLite database backend for MemU.\"\"\"\n\nimport os\nimport tempfile\n\nfrom memu.app import MemoryService\n\n\ndef _print_results(title: str, result: dict) -> None:\n    print(f\"\\n[SQLITE] RETRIEVED - {title}\")\n    print(\"  Categories:\")\n    for cat in result.get(\"categories\", [])[:3]:\n        print(f\"    - {cat.get('name')}: {(cat.get('summary') or cat.get('description', ''))[:80]}...\")\n    print(\"  Items:\")\n    for item in result.get(\"items\", [])[:3]:\n        print(f\"    - [{item.get('memory_type')}] {item.get('summary', '')[:100]}...\")\n    if result.get(\"resources\"):\n        print(\"  Resources:\")\n        for res in result.get(\"resources\", [])[:3]:\n            print(f\"    - [{res.get('modality')}] {res.get('url', '')[:80]}...\")\n\n\nasync def main():\n    \"\"\"Test with SQLite storage.\"\"\"\n    api_key = os.environ.get(\"OPENAI_API_KEY\")\n    file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), \"example\", \"example_conversation.json\"))\n\n    # Create a temporary SQLite database file\n    with tempfile.NamedTemporaryFile(suffix=\".db\", delete=False) as tmp:\n        sqlite_path = tmp.name\n\n    sqlite_dsn = f\"sqlite:///{sqlite_path}\"\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"[SQLITE] Starting test...\")\n    print(f\"[SQLITE] DSN: {sqlite_dsn}\")\n    print(\"=\" * 60)\n\n    try:\n        service = MemoryService(\n            llm_profiles={\"default\": {\"api_key\": api_key}},\n            database_config={\n                \"metadata_store\": {\n                    \"provider\": \"sqlite\",\n                    \"dsn\": sqlite_dsn,\n                },\n                # SQLite uses brute-force vector search\n                \"vector_index\": {\"provider\": \"bruteforce\"},\n            },\n            retrieve_config={\"method\": \"rag\"},\n        )\n\n        # Memorize\n        print(\"\\n[SQLITE] Memorizing...\")\n        memory = await service.memorize(resource_url=file_path, modality=\"conversation\", user={\"user_id\": \"123\"})\n        for cat in memory.get(\"categories\", []):\n            print(f\"  - {cat.get('name')}: {(cat.get('summary') or '')[:80]}...\")\n\n        queries = [\n            {\"role\": \"user\", \"content\": {\"text\": \"Tell me about preferences\"}},\n            {\"role\": \"assistant\", \"content\": {\"text\": \"Sure, I'll tell you about their preferences\"}},\n            {\n                \"role\": \"user\",\n                \"content\": {\"text\": \"What are they\"},\n            },  # This is the query that will be used to retrieve the memory\n        ]\n\n        # RAG-based retrieval\n        service.retrieve_config.method = \"rag\"\n        result_rag = await service.retrieve(queries=queries, where={\"user_id\": \"123\"})\n        _print_results(\"RAG\", result_rag)\n\n        # LLM-based retrieval\n        service.retrieve_config.method = \"llm\"\n        result_llm = await service.retrieve(queries=queries, where={\"user_id\": \"123\"})\n        _print_results(\"LLM\", result_llm)\n\n        print(\"\\n[SQLITE] Test completed!\")\n\n    finally:\n        # Clean up the temporary database file\n        if os.path.exists(sqlite_path):\n            os.unlink(sqlite_path)\n            print(f\"[SQLITE] Cleaned up temporary database: {sqlite_path}\")\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(main())\n"
  },
  {
    "path": "tests/test_tool_memory.py",
    "content": "\"\"\"Tests for Tool Memory feature - specialized memory type for tracking tool usage.\"\"\"\n\nfrom __future__ import annotations\n\nimport importlib.util\nimport sys\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Any\n\n# Add src to path for direct import - MUST be before any memu imports\nsrc_path = Path(__file__).parent.parent / \"src\"\nif str(src_path) not in sys.path:\n    sys.path.insert(0, str(src_path))\n\nimport pytest  # noqa: E402\n\n# Import directly from the models file path to avoid circular import through database/__init__.py\n# We use importlib to import the module directly without triggering the package __init__\nspec = importlib.util.spec_from_file_location(\"models\", src_path / \"memu\" / \"database\" / \"models.py\")\nassert spec is not None\nassert spec.loader is not None\nmodels = importlib.util.module_from_spec(spec)\nspec.loader.exec_module(models)\n\n# Rebuild models to resolve forward references with proper namespace\nrebuild_ns = {\n    \"Any\": Any,\n    \"datetime\": datetime,\n    \"MemoryType\": models.MemoryType,\n    \"ToolCallResult\": models.ToolCallResult,\n}\nmodels.ToolCallResult.model_rebuild(_types_namespace=rebuild_ns)\nmodels.MemoryItem.model_rebuild(_types_namespace=rebuild_ns)\n\nMemoryItem = models.MemoryItem\nMemoryType = models.MemoryType\nToolCallResult = models.ToolCallResult\n\n# Import tool memory utility functions\nutil_tool_spec = importlib.util.spec_from_file_location(\"util_tool\", src_path / \"memu\" / \"utils\" / \"tool.py\")\nassert util_tool_spec is not None\nassert util_tool_spec.loader is not None\nutil_tool = importlib.util.module_from_spec(util_tool_spec)\nutil_tool_spec.loader.exec_module(util_tool)\n\nadd_tool_call = util_tool.add_tool_call\nget_tool_statistics = util_tool.get_tool_statistics\n\n\nclass TestToolCallResult:\n    \"\"\"Tests for ToolCallResult model.\"\"\"\n\n    def test_create_tool_call_result(self):\n        \"\"\"Test creating a basic ToolCallResult.\"\"\"\n        result = ToolCallResult(\n            tool_name=\"file_reader\",\n            input={\"path\": \"/data/config.json\"},\n            output=\"File content here\",\n            success=True,\n            time_cost=0.5,\n            token_cost=100,\n            score=0.95,\n        )\n\n        assert result.tool_name == \"file_reader\"\n        assert result.input == {\"path\": \"/data/config.json\"}\n        assert result.output == \"File content here\"\n        assert result.success is True\n        assert result.time_cost == 0.5\n        assert result.token_cost == 100\n        assert result.score == 0.95\n\n    def test_generate_hash(self):\n        \"\"\"Test hash generation for deduplication.\"\"\"\n        result = ToolCallResult(\n            tool_name=\"calculator\",\n            input={\"a\": 1, \"b\": 2},\n            output=\"3\",\n        )\n\n        hash1 = result.generate_hash()\n        assert hash1 != \"\"\n        assert len(hash1) == 32  # MD5 hex digest length\n\n        # Same input/output should generate same hash\n        result2 = ToolCallResult(\n            tool_name=\"calculator\",\n            input={\"a\": 1, \"b\": 2},\n            output=\"3\",\n        )\n        assert result2.generate_hash() == hash1\n\n        # Different input should generate different hash\n        result3 = ToolCallResult(\n            tool_name=\"calculator\",\n            input={\"a\": 2, \"b\": 3},\n            output=\"5\",\n        )\n        assert result3.generate_hash() != hash1\n\n    def test_ensure_hash(self):\n        \"\"\"Test ensure_hash sets call_hash if empty.\"\"\"\n        result = ToolCallResult(\n            tool_name=\"test_tool\",\n            input=\"test input\",\n            output=\"test output\",\n        )\n\n        assert result.call_hash == \"\"\n        result.ensure_hash()\n        assert result.call_hash != \"\"\n        assert len(result.call_hash) == 32\n\n    def test_string_input(self):\n        \"\"\"Test ToolCallResult with string input.\"\"\"\n        result = ToolCallResult(\n            tool_name=\"echo\",\n            input=\"hello world\",\n            output=\"hello world\",\n        )\n\n        result.ensure_hash()\n        assert result.call_hash != \"\"\n\n\nclass TestMemoryItemToolType:\n    \"\"\"Tests for MemoryItem with tool type.\"\"\"\n\n    def test_tool_memory_type_literal(self):\n        \"\"\"Test that 'tool' is a valid MemoryType.\"\"\"\n        from typing import get_args\n\n        valid_types = get_args(MemoryType)\n        assert \"tool\" in valid_types\n\n    def test_create_tool_memory(self):\n        \"\"\"Test creating a tool type memory item with tool fields in extra.\"\"\"\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"tool\",\n            summary=\"file_reader tool usage for config files\",\n            extra={\n                \"when_to_use\": \"When needing to read configuration files\",\n                \"metadata\": {\"tool_name\": \"file_reader\", \"avg_success_rate\": 0.95},\n            },\n        )\n\n        assert item.memory_type == \"tool\"\n        assert item.extra[\"when_to_use\"] == \"When needing to read configuration files\"\n        assert item.extra[\"metadata\"][\"tool_name\"] == \"file_reader\"\n\n    def test_add_tool_call(self):\n        \"\"\"Test adding tool call results to a tool memory.\"\"\"\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"tool\",\n            summary=\"calculator tool usage\",\n        )\n\n        tool_call = ToolCallResult(\n            tool_name=\"calculator\",\n            input={\"a\": 1, \"b\": 2},\n            output=\"3\",\n            success=True,\n            time_cost=0.1,\n            score=1.0,\n        )\n\n        add_tool_call(item, tool_call)\n\n        tool_calls = item.extra.get(\"tool_calls\", [])\n        assert len(tool_calls) == 1\n        assert tool_calls[0][\"tool_name\"] == \"calculator\"\n        assert tool_calls[0][\"call_hash\"] != \"\"  # ensure_hash was called\n\n    def test_add_tool_call_wrong_type(self):\n        \"\"\"Test that add_tool_call fails for non-tool memories.\"\"\"\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"profile\",\n            summary=\"User profile info\",\n        )\n\n        tool_call = ToolCallResult(\n            tool_name=\"test\",\n            input=\"test\",\n            output=\"test\",\n        )\n\n        with pytest.raises(ValueError, match=\"can only be used with tool type memories\"):\n            add_tool_call(item, tool_call)\n\n    def test_get_tool_statistics_empty(self):\n        \"\"\"Test statistics for memory with no tool calls.\"\"\"\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"tool\",\n            summary=\"empty tool memory\",\n        )\n\n        stats = get_tool_statistics(item)\n\n        assert stats[\"total_calls\"] == 0\n        assert stats[\"recent_calls_analyzed\"] == 0\n        assert stats[\"avg_time_cost\"] == 0.0\n        assert stats[\"success_rate\"] == 0.0\n        assert stats[\"avg_score\"] == 0.0\n        assert stats[\"avg_token_cost\"] == 0.0\n\n    def test_get_tool_statistics(self):\n        \"\"\"Test statistics calculation for tool calls.\"\"\"\n        # Tool calls are stored as dicts in extra\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"tool\",\n            summary=\"calculator tool\",\n            extra={\n                \"tool_calls\": [\n                    {\n                        \"tool_name\": \"calc\",\n                        \"input\": \"1+1\",\n                        \"output\": \"2\",\n                        \"success\": True,\n                        \"time_cost\": 0.1,\n                        \"score\": 1.0,\n                        \"token_cost\": 10,\n                    },\n                    {\n                        \"tool_name\": \"calc\",\n                        \"input\": \"2+2\",\n                        \"output\": \"4\",\n                        \"success\": True,\n                        \"time_cost\": 0.2,\n                        \"score\": 0.9,\n                        \"token_cost\": 15,\n                    },\n                    {\n                        \"tool_name\": \"calc\",\n                        \"input\": \"bad\",\n                        \"output\": \"error\",\n                        \"success\": False,\n                        \"time_cost\": 0.5,\n                        \"score\": 0.0,\n                        \"token_cost\": 5,\n                    },\n                ]\n            },\n        )\n\n        stats = get_tool_statistics(item)\n\n        assert stats[\"total_calls\"] == 3\n        assert stats[\"recent_calls_analyzed\"] == 3\n        assert stats[\"success_rate\"] == pytest.approx(0.6667, rel=0.01)  # 2/3\n        assert stats[\"avg_time_cost\"] == pytest.approx(0.267, rel=0.01)  # (0.1+0.2+0.5)/3\n        assert stats[\"avg_score\"] == pytest.approx(0.633, rel=0.01)  # (1.0+0.9+0.0)/3\n        assert stats[\"avg_token_cost\"] == pytest.approx(10.0, rel=0.01)  # (10+15+5)/3\n\n    def test_get_tool_statistics_recent_n(self):\n        \"\"\"Test statistics with recent_n limit.\"\"\"\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"tool\",\n            summary=\"tool with many calls\",\n            extra={\n                \"tool_calls\": [\n                    {\"tool_name\": \"t\", \"input\": \"1\", \"output\": \"1\", \"success\": False, \"time_cost\": 1.0, \"score\": 0.0},\n                    {\"tool_name\": \"t\", \"input\": \"2\", \"output\": \"2\", \"success\": True, \"time_cost\": 0.1, \"score\": 1.0},\n                    {\"tool_name\": \"t\", \"input\": \"3\", \"output\": \"3\", \"success\": True, \"time_cost\": 0.1, \"score\": 1.0},\n                ]\n            },\n        )\n\n        # Only analyze last 2 calls\n        stats = get_tool_statistics(item, recent_n=2)\n\n        assert stats[\"total_calls\"] == 3\n        assert stats[\"recent_calls_analyzed\"] == 2\n        assert stats[\"success_rate\"] == 1.0  # Both recent calls succeeded\n\n\nclass TestMemoryItemNewFields:\n    \"\"\"Tests for tool-related fields stored in extra.\"\"\"\n\n    def test_when_to_use_field(self):\n        \"\"\"Test when_to_use field stored in extra for retrieval hints.\"\"\"\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"profile\",\n            summary=\"User prefers dark mode\",\n            extra={\"when_to_use\": \"When configuring UI settings or themes\"},\n        )\n\n        assert item.extra[\"when_to_use\"] == \"When configuring UI settings or themes\"\n\n    def test_metadata_field(self):\n        \"\"\"Test metadata field stored in extra for type-specific data.\"\"\"\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"event\",\n            summary=\"User attended conference\",\n            extra={\n                \"metadata\": {\n                    \"event_date\": \"2026-01-15\",\n                    \"location\": \"San Francisco\",\n                    \"attendees\": [\"Alice\", \"Bob\"],\n                }\n            },\n        )\n\n        assert item.extra.get(\"metadata\") is not None\n        assert item.extra[\"metadata\"][\"event_date\"] == \"2026-01-15\"\n        assert item.extra[\"metadata\"][\"location\"] == \"San Francisco\"\n        assert len(item.extra[\"metadata\"][\"attendees\"]) == 2\n\n    def test_default_values(self):\n        \"\"\"Test that extra defaults to empty dict.\"\"\"\n        item = MemoryItem(\n            resource_id=None,\n            memory_type=\"knowledge\",\n            summary=\"Python is a programming language\",\n        )\n\n        assert item.extra.get(\"when_to_use\") is None\n        assert item.extra.get(\"metadata\") is None\n        assert item.extra.get(\"tool_calls\") is None\n"
  },
  {
    "path": "tests/utils/test_conversation.py",
    "content": "import json\n\nimport pytest\n\nfrom memu.utils.conversation import format_conversation_for_preprocess  # type: ignore[import-untyped]\n\n\nclass TestFormatConversationForPreprocess:\n    \"\"\"\n    Test suite for format_conversation_for_preprocess function in src/memu/utils/conversation.py.\n\n    Covers:\n    - Happy Path: Valid JSON input (list or dict wrapper).\n    - Edge Cases: Empty input, empty JSON structures.\n    - Error Handling: Invalid JSON (current implementation handles gracefully by returning raw text).\n    - Type Safety: Unexpected JSON types.\n    \"\"\"\n\n    @pytest.mark.parametrize(\n        \"input_json,expected_output\",\n        [\n            # Happy Path: Standard usage with list of messages\n            (\n                json.dumps([\n                    {\"role\": \"user\", \"content\": \"Hello world\", \"created_at\": \"2023-10-27T10:00:00\"},\n                    {\"role\": \"assistant\", \"content\": \"Hello! How can I help?\", \"created_at\": \"2023-10-27T10:00:05\"},\n                ]),\n                \"[0] 2023-10-27T10:00:00 [user]: Hello world\\n[1] 2023-10-27T10:00:05 [assistant]: Hello! How can I help?\",\n            ),\n            # Happy Path: Dict wrapper with 'content' key\n            (json.dumps({\"content\": [{\"role\": \"user\", \"content\": \"Wrapper test\"}]}), \"[0] [user]: Wrapper test\"),\n            # Happy Path: Missing optional fields (role defaults to user, created_at omitted)\n            (json.dumps([{\"content\": \"Just text\"}]), \"[0] [user]: Just text\"),\n            # Happy Path: Multiline content should be collapsed\n            (\n                json.dumps([{\"role\": \"system\", \"content\": \"Line 1\\nLine 2\\nLine 3\"}]),\n                \"[0] [system]: Line 1 Line 2 Line 3\",\n            ),\n            # Happy Path: Content is None/Null\n            (json.dumps([{\"role\": \"user\", \"content\": None}]), \"[0] [user]: \"),\n            # Happy Path: Content is a dict with 'text'\n            (json.dumps([{\"role\": \"user\", \"content\": {\"text\": \"Rich content\"}}]), \"[0] [user]: Rich content\"),\n        ],\n    )\n    def test_happy_path_valid_formats(self, input_json: str, expected_output: str) -> None:\n        \"\"\"\n        Test that valid JSON inputs are correctly formatted into the expected line-based string.\n        \"\"\"\n        result = format_conversation_for_preprocess(input_json)\n        assert result == expected_output\n\n    @pytest.mark.parametrize(\n        \"edge_input,expected\",\n        [\n            (\"\", \"\"),  # Empty string\n            (\"   \", \"\"),  # Whitespace only\n            (\"[]\", \"\"),  # Empty JSON list -> produces empty string\n        ],\n    )\n    def test_edge_cases_empty(self, edge_input: str, expected: str) -> None:\n        \"\"\"\n        Test edge cases handling for empty or whitespace-only inputs, and empty JSON lists.\n        \"\"\"\n        assert format_conversation_for_preprocess(edge_input) == expected\n\n    def test_malformed_json_handling(self) -> None:\n        \"\"\"\n        Test handling of malformed JSON strings.\n\n        Note: The implementation swallows JSONDecodeError and returns raw text.\n        This test verifies that graceful fallback behavior.\n        \"\"\"\n        malformed_json = '{\"role\": \"user\", \"content\": \"Missing brace\"'\n        result = format_conversation_for_preprocess(malformed_json)\n        # Expecting raw text back as fallback\n        assert result == malformed_json\n\n    def test_unexpected_json_structures(self) -> None:\n        \"\"\"\n        Test handling of valid JSON that does not match expected conversation schema.\n        Expectation: Returns raw text if schema extraction fails.\n        \"\"\"\n        # Empty dict -> _extract_messages returns None\n        assert format_conversation_for_preprocess(\"{}\") == \"{}\"\n\n        # Random non-message JSON\n        random_json = json.dumps({\"key\": \"value\"})\n        assert format_conversation_for_preprocess(random_json) == random_json\n\n        # Valid JSON primitives\n        assert format_conversation_for_preprocess(\"123\") == \"123\"\n"
  }
]