diff --git a/.github/CONTRIBUTING b/.github/CONTRIBUTING index 05c946ebf..bbb218a61 100644 --- a/.github/CONTRIBUTING +++ b/.github/CONTRIBUTING @@ -1,3 +1,3 @@ -Please read https://memos.openmem.net/docs/contribution/overview to learn how to contribute to this repository. 🌟 +Please read https://memos-docs.openmem.net/contribution/overview to learn how to contribute to this repository. 🌟 -请阅读 https://memos.openmem.net/docs/contribution/overview 了解如何为此项目贡献代码。🌟 +请阅读 https://memos-docs.openmem.net/contribution/overview 了解如何为此项目贡献代码。🌟 diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 5d72271eb..ed17b4109 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -2,40 +2,34 @@ name: "\U0001F680 Feature request" description: Submit a request for a new feature labels: ["enhancement", "pending"] body: - - type: markdown - attributes: - value: | - Please do not create issues that are not related to new features under this category. - 请勿在此分类下创建和新特性无关的 issues。 - - type: checkboxes - id: reminder + id: checklist attributes: - label: Reminder - description: | - Please ensure you have read the above rules carefully and searched the existing issues. - 请确保您已经认真阅读了上述规则并且搜索过现有的 issues。 - + label: Pre-submission checklist options: - - label: I have read the above rules and searched the existing issues. + - label: I have searched existing issues and this feature hasn't been requested before | 我已搜索现有问题,确认此功能尚未被请求 + required: true + - label: I have read the project documentation and confirmed this feature doesn't already exist | 我已阅读项目文档并确认此功能尚未存在 + required: true + - label: This feature request is specific to MemOS and not a general software issue | 该功能请求是针对 MemOS 的,而不是一般软件问题 required: true - type: textarea - id: description + id: problem validations: required: true attributes: - label: Description - description: | - A clear and concise description of the feature proposal. - 请详细描述您希望加入的新功能特性。 + label: Problem Statement + placeholder: | + Describe the problem you're trying to solve... + Example: "As a developer using MemOS, I find it difficult to..." - - type: textarea + - type: checkboxes id: contribution - validations: - required: false attributes: - label: Pull Request - description: | - Have you already created the relevant PR and submitted the code? - 您是否已经创建了相关 PR 并提交了代码? + label: Implementation Contribution + options: + - label: I'm willing to implement this feature myself | 我愿意自己实现此功能 + required: false + - label: I would like someone else to implement this | 我希望其他人来实现此功能 + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4b7f715c9..f532a0c42 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,28 @@ ## Description -> Please include a summary of the change and which issue is fixed. -> -> 请介绍你的修改,并说明它修复了哪个 issue. + + +Summary: (summary) + +Fix: #(issue) + +Reviewer: @(reviewer) ## Checklist: - [ ] I have performed a self-review of my own code | 我已自行检查了自己的代码 -- [ ] I have commented my code, particularly in hard-to-understand areas | 我已对代码进行了注释,特别是在难以理解的地方 +- [ ] I have commented my code in hard-to-understand areas | 我已在难以理解的地方对代码进行了注释 - [ ] I have added tests that prove my fix is effective or that my feature works | 我已添加测试以证明我的修复有效或功能正常 -- [ ] I have added necessary documentation (if appropriate) | 我已添加必要的文档(如果有必要) +- [ ] I have added necessary documentation (if applicable) | 我已添加必要的文档(如果适用) +- [ ] I have linked the issue to this PR (if applicable) | 我已将 issue 链接到此 PR(如果适用) +- [ ] I have mentioned the person who will review this PR | 我已提及将审查此 PR 的人 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 4bf9ef7ba..83ff35572 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -25,6 +25,10 @@ jobs: matrix: os: - "ubuntu-latest" + - "windows-latest" + - "macos-14" + - "macos-15" + # Ref: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-where-your-workflow-runs/choosing-the-runner-for-a-job python-version: - "3.10" - "3.11" @@ -51,4 +55,4 @@ jobs: poetry run ruff format --check - name: Test with pytest run: | - PYTHONPATH=src poetry run pytest tests -vv + poetry run pytest tests -vv diff --git a/.gitignore b/.gitignore index 23b79c7db..6a8e52eb4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,8 @@ evaluation/data/langmemeval evaluation/*tmp/ evaluation/results evaluation/.env -evaluation/scripts/*.sh evaluation/configs/* +**tree_textual_memory_locomo** .env # Byte-compiled / optimized / DLL files @@ -166,6 +166,7 @@ venv.bak/ *.xlsx *.json *.pkl +*.html # but do not ignore docs/openapi.json !docs/openapi.json diff --git a/Makefile b/Makefile index 6d111cf15..c250316ca 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: test docs +.PHONY: test install: poetry install --with dev --with test @@ -11,7 +11,7 @@ clean: rm -rf tmp test: - PYTHONPATH=src poetry run pytest tests + poetry run pytest tests format: poetry run ruff check --fix @@ -23,24 +23,5 @@ pre_commit: serve: poetry run uvicorn memos.api.start_api:app -docs: - pytest tests/test_docs.py -xq - pip install mkdocs mkdocs-material -q - mkdir -p tmp - rm -f tmp/mkdocs.yml - echo "site_name: MemOS Documentation" > tmp/mkdocs.yml - echo "docs_dir: `pwd`/docs" >> tmp/mkdocs.yml - echo "theme: material" >> tmp/mkdocs.yml - echo "markdown_extensions:" >> tmp/mkdocs.yml - echo " - pymdownx.highlight:" >> tmp/mkdocs.yml - echo " anchor_linenums: true" >> tmp/mkdocs.yml - echo " line_spans: __span" >> tmp/mkdocs.yml - echo " pygments_lang_class: true" >> tmp/mkdocs.yml - echo " - pymdownx.inlinehilite" >> tmp/mkdocs.yml - echo " - pymdownx.snippets" >> tmp/mkdocs.yml - echo " - pymdownx.superfences" >> tmp/mkdocs.yml - cat docs/settings.yml >> tmp/mkdocs.yml - mkdocs serve -f tmp/mkdocs.yml --no-livereload - openapi: poetry run python scripts/export_openapi.py --output docs/openapi.json diff --git a/README.md b/README.md index d5be3324d..00af88745 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@
- MemOS Banner + MemOS Banner - -

- MemOS Logo MemOS 1.0: 星河 (Stellar) Preview Badge + MemOS Logo MemOS 1.0: 星河 (Stellar) Preview Badge

@@ -19,7 +17,7 @@ Supported Python versions - + Documentation @@ -31,7 +29,7 @@ Discord - + WeChat Group @@ -42,17 +40,14 @@ --- - - SOTA SCORE - - +SOTA SCORE **MemOS** is an operating system for Large Language Models (LLMs) that enhances them with long-term memory capabilities. It allows LLMs to store, retrieve, and manage information, enabling more context-aware, consistent, and personalized interactions. -- **Website**: https://memos.openmem.net/ -- **Documentation**: https://memos.openmem.net/docs/home -- **API Reference**: https://memos.openmem.net/docs/api/info -- **Source Code**: https://github.com/MemTensor/MemOS +- **Website**: https://memos.openmem.net/ +- **Documentation**: https://memos-docs.openmem.net/home/overview/ +- **API Reference**: https://memos-docs.openmem.net/docs/api/info/ +- **Source Code**: https://github.com/MemTensor/MemOS ## 📈 Performance Benchmark @@ -66,19 +61,12 @@ MemOS demonstrates significant improvements over baseline memory solutions in mu > 💡 **Temporal reasoning accuracy improved by 159% compared to the OpenAI baseline.** - - ### Details of End-to-End Evaluation on LOCOMO > [!NOTE] > Comparison of LLM Judge Scores across five major tasks in the LOCOMO benchmark. Each bar shows the mean evaluation score judged by LLMs for a given method-task pair, with standard deviation as error bars. MemOS-0630 consistently outperforms baseline methods (LangMem, Zep, OpenAI, Mem0) across all task types, especially in multi-hop and temporal reasoning scenarios. - - END2END SCORE - - - - +END2END SCORE ## ✨ Key Features @@ -151,7 +139,11 @@ For more detailed examples, please check out the [`examples`](./examples) direct ## 📦 Installation > [!WARNING] -> Currently, MemOS primarily supports Linux platforms. You may encounter issues on Windows and macOS temporarily. +> MemOS is compatible with Linux, Windows, and macOS. +> +> However, if you're using macOS, please note that there may be dependency issues that are difficult to resolve. +> +> For example, compatibility with macOS 13 Ventura is currently challenging. ### Install via pip @@ -192,26 +184,50 @@ Join our community to ask questions, share your projects, and connect with other - **Discord**: Join our Discord Server. - **WeChat**: Scan the QR code to join our WeChat group. -QR Code +QR Code ## 📜 Citation -If you use MemOS in your research, please cite our paper: +> [!NOTE] +> We publicly released the Short Version on **May 28, 2025**, making it the earliest work to propose the concept of a Memory Operating System for LLMs. + +If you use MemOS in your research, we would appreciate citations to our papers. ```bibtex -@misc{li2025memos, - title={MemOS: A Memory OS for AI System}, - author={Zhiyu Li and Shichao Song and Chenyang Xi and Hanyu Wang and Chen Tang and Simin Niu and Ding Chen and Jiawei Yang and Chunyu Li and Qingchen Yu and Jihao Zhao and Yezhaohui Wang and Peng Liu and Zehao Lin and Pengyuan Wang and Jiahao Huo and Tianyi Chen and Kai Chen and Kehang Li and Zhen Tao and Junpeng Ren and Huayi Lai and Hao Wu and Bo Tang and Zhenren Wang and Zhaoxin Fan and Ningyu Zhang and Linfeng Zhang and Junchi Yan and Mingchuan Yang and Tong Xu and Wei Xu and Huajun Chen and Haofeng Wang and Hongkang Yang and Wentao Zhang and Zhi-Qin John Xu and Siheng Chen and Feiyu Xiong}, - year={2025}, - eprint={2507.03724}, - archivePrefix={arXiv}, - primaryClass={cs.CL} + +@article{li2025memos_long, + title={MemOS: A Memory OS for AI System}, + author={Li, Zhiyu and Song, Shichao and Xi, Chenyang and Wang, Hanyu and Tang, Chen and Niu, Simin and Chen, Ding and Yang, Jiawei and Li, Chunyu and Yu, Qingchen and Zhao, Jihao and Wang, Yezhaohui and Liu, Peng and Lin, Zehao and Wang, Pengyuan and Huo, Jiahao and Chen, Tianyi and Chen, Kai and Li, Kehang and Tao, Zhen and Ren, Junpeng and Lai, Huayi and Wu, Hao and Tang, Bo and Wang, Zhenren and Fan, Zhaoxin and Zhang, Ningyu and Zhang, Linfeng and Yan, Junchi and Yang, Mingchuan and Xu, Tong and Xu, Wei and Chen, Huajun and Wang, Haofeng and Yang, Hongkang and Zhang, Wentao and Xu, Zhi-Qin John and Chen, Siheng and Xiong, Feiyu}, + journal={arXiv preprint arXiv:2507.03724}, + year={2025}, + url={https://arxiv.org/abs/2507.03724} +} + +@article{li2025memos_short, + title={MemOS: An Operating System for Memory-Augmented Generation (MAG) in Large Language Models}, + author={Li, Zhiyu and Song, Shichao and Wang, Hanyu and Niu, Simin and Chen, Ding and Yang, Jiawei and Xi, Chenyang and Lai, Huayi and Zhao, Jihao and Wang, Yezhaohui and others}, + journal={arXiv preprint arXiv:2505.22101}, + year={2025}, + url={https://arxiv.org/abs/2505.22101} +} + +@article{yang2024memory3, +author = {Yang, Hongkang and Zehao, Lin and Wenjin, Wang and Wu, Hao and Zhiyu, Li and Tang, Bo and Wenqiang, Wei and Wang, Jinbo and Zeyun, Tang and Song, Shichao and Xi, Chenyang and Yu, Yu and Kai, Chen and Xiong, Feiyu and Tang, Linpeng and Weinan, E}, +title = {Memory$^3$: Language Modeling with Explicit Memory}, +journal = {Journal of Machine Learning}, +year = {2024}, +volume = {3}, +number = {3}, +pages = {300--346}, +issn = {2790-2048}, +doi = {https://doi.org/10.4208/jml.240708}, +url = {https://global-sci.com/article/91443/memory3-language-modeling-with-explicit-memory} } ``` ## 🙌 Contributing -We welcome contributions from the community! Please read our [contribution guidelines](https://memos.openmem.net/docs/contribution/overview) to get started. +We welcome contributions from the community! Please read our [contribution guidelines](https://memos-docs.openmem.net/contribution/overview) to get started. ## 📄 License diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..bf5fea70d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +All documentation has been moved to a separate repository: https://github.com/MemTensor/MemOS-Docs. Please edit documentation there. + +所有文档已迁移至独立仓库:https://github.com/MemTensor/MemOS-Docs。请在该仓库中编辑文档。 diff --git a/docs/assets/SOTA_Score.jpg b/docs/assets/SOTA_Score.jpg deleted file mode 100644 index fd0084315..000000000 Binary files a/docs/assets/SOTA_Score.jpg and /dev/null differ diff --git a/docs/assets/banner_new.gif b/docs/assets/banner_new.gif deleted file mode 100644 index 8a8309cfe..000000000 Binary files a/docs/assets/banner_new.gif and /dev/null differ diff --git a/docs/assets/memos-architecture.png b/docs/assets/memos-architecture.png deleted file mode 100644 index e73444013..000000000 Binary files a/docs/assets/memos-architecture.png and /dev/null differ diff --git a/docs/assets/memos-banner.png b/docs/assets/memos-banner.png deleted file mode 100644 index 4f2422002..000000000 Binary files a/docs/assets/memos-banner.png and /dev/null differ diff --git a/docs/assets/memos_logo.png b/docs/assets/memos_logo.png deleted file mode 100644 index 83ed20b5e..000000000 Binary files a/docs/assets/memos_logo.png and /dev/null differ diff --git a/docs/assets/qr-code.png b/docs/assets/qr-code.png deleted file mode 100644 index 6eca4e31d..000000000 Binary files a/docs/assets/qr-code.png and /dev/null differ diff --git a/docs/assets/score_all_end2end.jpg b/docs/assets/score_all_end2end.jpg deleted file mode 100644 index 54ea1d700..000000000 Binary files a/docs/assets/score_all_end2end.jpg and /dev/null differ diff --git a/docs/best_practice/common_errors_solutions.md b/docs/best_practice/common_errors_solutions.md deleted file mode 100644 index ff2f06631..000000000 --- a/docs/best_practice/common_errors_solutions.md +++ /dev/null @@ -1,74 +0,0 @@ -# Common Errors and Solutions - -## Configuration Errors - -### Missing Required Fields - -```python -# ✅ Always include required fields -llm_config = { - "backend": "openai", - "config": { - "api_key": "your-api-key", - "model_name_or_path": "gpt-4" - } -} -``` - -### Backend Mismatch - -```python -# ✅ KVCache requires HuggingFace backend -kv_config = { - "backend": "kv_cache", - "config": { - "extractor_llm": { - "backend": "huggingface", - "config": { - "model_name_or_path": "Qwen/Qwen3-1.7B" - } - } - } -} -``` - -## Service Connection Issues - -```bash -# Start required services as needed -docker run -p 6333:6333 qdrant/qdrant -ollama serve -``` - -## Memory Issues - -### Loading Failures - -```python -try: - mem_cube.load("memory_dir") -except Exception: - mem_cube = GeneralMemCube(config) - mem_cube.dump("memory_dir") -``` - -### GPU Memory - -```python -import os -os.environ["CUDA_VISIBLE_DEVICES"] = "0" -# Use smaller models if GPU memory is limited: Qwen/Qwen3-0.6B -``` - -## User Management - -```python -# Register user first -mos.register_mem_cube(cube_path="path", user_id="user_id", cube_id="cube_id") - -# Check if user exists -try: - user_id = mos.create_user(user_name="john", role=UserRole.USER) -except ValueError: - user = mos.user_manager.get_user_by_name("john") -``` diff --git a/docs/best_practice/memory_structure_design.md b/docs/best_practice/memory_structure_design.md deleted file mode 100644 index d018abdf3..000000000 --- a/docs/best_practice/memory_structure_design.md +++ /dev/null @@ -1,87 +0,0 @@ -# Memory Structure Design Best Practices - -## Memory Type Selection - -### TreeTextMemory - -**Best for**: Knowledge management, research assistants, hierarchical data -```python -tree_config = { - "backend": "tree_text", - "config": { - "extractor_llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b" - } - }, - "graph_db": { - "backend": "neo4j", - "config": { - "host": "localhost", - "port": 7687 - } - } - } -} -``` - -### GeneralTextMemory - -**Best for**: Conversational AI, personal assistants, FAQ systems -```python -general_config = { - "backend": "general_text", - "config": { - "extractor_llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b" - } - }, - "vector_db": { - "backend": "qdrant", - "config": { - "collection_name": "general" - } - }, - "embedder": { - "backend": "ollama", - "config": { - "model_name_or_path": "nomic-embed-text" - } - } - } -} -``` - -### NaiveTextMemory - -**Best for**: Simple applications, prototyping -```python -naive_config = { - "backend": "naive_text", - "config": { - "extractor_llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b" - } - } - } -} -``` - -## Capacity Planning - -If you enable the scheduler, you can set memory capacities to control resource usage: - -```python -scheduler_config = { - "memory_capacities": { - "working_memory_capacity": 20, # Active conversation - "user_memory_capacity": 500, # User knowledge - "long_term_memory_capacity": 2000 # Domain knowledge - } -} -``` diff --git a/docs/best_practice/network_workarounds.md b/docs/best_practice/network_workarounds.md deleted file mode 100644 index cc342d2bc..000000000 --- a/docs/best_practice/network_workarounds.md +++ /dev/null @@ -1,108 +0,0 @@ -# Network Workarounds - -Here are some solutions to address the network issues you may encounter during developing. - -## **Downloading Huggingface Models** - -### Mirror Site (HF-Mirror) - -To download Huggingface models using the mirror site, you can follow these steps: - -::steps{level="4"} - -#### Install Dependencies - -Install the necessary dependencies by running: - -```bash -pip install -U huggingface_hub -``` - -#### Set Environment Variable - -Set the environment variable `HF_ENDPOINT` to `https://hf-mirror.com`. - -#### Download Models or Datasets - -Use huggingface-cli to download models or datasets. For example: - -- To download a model: - - ```bash - huggingface-cli download --resume-download gpt2 --local-dir gpt2 - ``` -- To download a dataset: - ``` - huggingface-cli download --repo-type dataset --resume-download wikitext --local-dir wikitext - ``` - -:: - -For more detailed instructions and additional methods, please refer to [this link](https://hf-mirror.com/). - -### Alternative Sources -You may still encounter limitations accessing some models in your regions. In such cases, you can use modelscope: - -::steps{level="4"} - -#### Install ModelScope - -Install the necessary dependencies by running: - -```bash -pip install modelscope[framework] -``` - -#### Download Models or Datasets - -Use modelscope to download models or datasets. For example: - -- To download a model: - ```bash - modelscope download --model 'Qwen/Qwen2-7b' --local_dir 'path/to/dir' - ``` -- To download a dataset: - - ```bash - modelscope download --dataset 'Tongyi-DataEngine/SA1B-Dense-Caption' --local_dir './local_dir' - ``` - -:: - -For more detailed instructions and additional methods, please refer to the [official docs](https://modelscope.cn/docs/home). - -## **Using Poetry** - -### Network Errors during Installing -To address network errors when using "poetry install" in your regions, you can follow these steps: - -::steps{level="4"} - -#### Update Configuration - -Update the `pyproject.toml` file to use a mirror source by adding the following configuration: - -```toml -[[tool.poetry.source]] -name = "mirrors" -url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" -priority = "primary" -``` - -#### Reconfigure Poetry - -Run the command `poetry lock` in the terminal to reconfigure Poetry with the new mirror source. - -:: - -**Tips:** -Be aware that `poetry lock` will modify both Pyproject.toml and poetry.lock files. To avoid committing redundant changes: - - Option 1: After successful `poetry install`, revert to the git HEAD node using `git reset --hard HEAD`. - - Option 2: When executing `git add`, exclude the Pyproject.toml and poetry.lock files by specifying other files. - -For future dependency management tasks like adding or removing packages, you can use the `poetry add` command: -```bash -poetry add -``` - -Refer to the [Poetry CLI documentation](https://python-poetry.org/docs/cli/) for more commands and details. diff --git a/docs/best_practice/performance_tuning.md b/docs/best_practice/performance_tuning.md deleted file mode 100644 index a82d8ce4a..000000000 --- a/docs/best_practice/performance_tuning.md +++ /dev/null @@ -1,53 +0,0 @@ -# Performance Tuning Best Practices - -## Embedding Optimization - -```python -fast_embedder = { - "backend": "ollama", - "config": { - "model_name_or_path": "nomic-embed-text:latest" - } -} - -slow_embedder = { - "backend": "sentence_transformer", - "config": { - "model_name_or_path": "nomic-ai/nomic-embed-text-v1.5" - } -} -``` - -## Inference Speed - -```python -generation_config = { - "max_new_tokens": 256, # Limit response length - "temperature": 0.7, - "do_sample": True -} -``` - -## System Resource Optimization - -### Memory Capacity Limits - -```python -scheduler_config = { - "memory_capacities": { - "working_memory_capacity": 20, # Active context - "user_memory_capacity": 500, # User storage - "long_term_memory_capacity": 2000, # Domain knowledge - "transformed_act_memory_capacity": 50 # KV cache items - } -} -``` - -### Batch Processing - -```python -def batch_memory_operations(operations, batch_size=10): - for i in range(0, len(operations), batch_size): - batch = operations[i:i + batch_size] - yield batch # Process in batches -``` diff --git a/docs/contribution/commit_guidelines.md b/docs/contribution/commit_guidelines.md deleted file mode 100644 index ff95c738b..000000000 --- a/docs/contribution/commit_guidelines.md +++ /dev/null @@ -1,15 +0,0 @@ -# Commit Guidelines - -Please follow the [Conventional Commits](https://www.conventionalcommits.org/) format: - -- `feat:` for new features -- `fix:` for bug fixes -- `docs:` for documentation updates -- `style:` for formatting changes -- `refactor:` for code refactoring -- `test:` for adding or updating tests -- `chore:` for maintenance tasks -- `ci:` for CI/CD or workflow changes - -**Example:** -`feat: add user authentication` diff --git a/docs/contribution/development_workflow.md b/docs/contribution/development_workflow.md deleted file mode 100644 index a8e2e9bda..000000000 --- a/docs/contribution/development_workflow.md +++ /dev/null @@ -1,65 +0,0 @@ -# Development Workflow - -Follow these steps to contribute to the project. - -::steps{level="4"} - -#### Sync with Upstream - -If you've previously forked the repository, sync with the upstream changes: - -```bash -git checkout dev # switch to dev branch -git fetch upstream # fetch latest changes from upstream -git pull upstream dev # merge changes into your local dev branch -git push origin dev # push changes to your fork -``` - -#### Create a Feature Branch - -Create a new branch for your feature or fix: - -```bash -git checkout -b feat/descriptive-name -``` - -#### Make Your Changes - -Implement your feature, fix, or improvement in the appropriate files. - -- For example, you might add a function in `src/memos/hello_world.py` and create corresponding tests in `tests/test_hello_world.py`. - -#### Test Your Changes - -Run the test suite to ensure your changes work correctly: - -```bash -make test -``` - -#### Commit Your Changes - -Follow the project's commit guidelines (see [Commit Guidelines](./commit_guidelines.md)) when committing your changes. - -#### Push to Your Fork - -Push your feature branch to your forked repository: - -```bash -git push origin feat/descriptive-name -``` - -#### Create a Pull Request - -Submit your changes for review: - -- **Important:** Please create your pull request against - - ✅ the `dev` branch of the upstream repository, - - ❎ not the `main` branch of the upstream repository. -- Go to the original repository on GitHub -- Click on "Pull Requests" -- Click on "New Pull Request" -- Select `dev` as the base branch, and your branch as compare -- Fulfill the PR description carefully. - -:: diff --git a/docs/contribution/overview.md b/docs/contribution/overview.md deleted file mode 100644 index bcff726f0..000000000 --- a/docs/contribution/overview.md +++ /dev/null @@ -1,13 +0,0 @@ -# Overview - -Thank you for your interest in contributing! We've structured our contribution guide into several documents to help you find the information you need quickly. - -- **First-time contributors:** Please start by reading the [Setting Up](./setting_up.md) guide to prepare your development environment. -- **Ready to code?** The [Development Workflow](./development_workflow.md) guide will walk you through our process for submitting changes. -- **Writing good commit messages:** See our [Commit Guidelines](./commit_guidelines.md). -- **Contributing to documentation:** If you're helping us improve our docs, check out the [Writing Documentation](./writing_docs.md) guide. -- **Adding or improving tests:** The [Writing Tests](./writing_tests.md) guide is for you. - -Your contributions make this project better! ✨ If you have any questions, feel free to open an issue or join the discussion or scan the QR codes below to connect with us on Discord or WeChat. - -QR Code diff --git a/docs/contribution/setting_up.md b/docs/contribution/setting_up.md deleted file mode 100644 index 4d5578815..000000000 --- a/docs/contribution/setting_up.md +++ /dev/null @@ -1,47 +0,0 @@ -# Setting Up Your Development Environment - -To contribute to MemOS, you'll need to set up your local development environment. - -::steps{level="4"} - -#### Fork & Clone the Repository - -Set up the repository on your local machine: - -- Fork the repository on GitHub -- Clone your fork to your local machine: - ```bash - git clone https://github.com/YOUR-USERNAME/MemOS.git - cd MemOS - ``` -- Add the upstream repository as a remote: - ```bash - git remote add upstream https://github.com/MemTensor/MemOS.git - ``` - -#### Install Poetry - -Install Poetry for dependency management: - -```bash -curl -sSL https://install.python-poetry.org | python3 - -``` - -Or follow the [official instructions](https://python-poetry.org/docs/#installing-with-the-official-installer). - -Verify installation: -```bash -poetry --version -``` - -#### Install Dependencies and Set Up Pre-commit Hooks - -Install all project dependencies and development tools: - -```bash -make install -``` - -As the environment changes across commit history, you may need to **re-run `make install`** time to time to ensure all dependencies are up-to-date. - -:: diff --git a/docs/contribution/writing_docs.md b/docs/contribution/writing_docs.md deleted file mode 100644 index f1f238434..000000000 --- a/docs/contribution/writing_docs.md +++ /dev/null @@ -1,32 +0,0 @@ -# How to Write and Preview Documentation - -This project uses a custom Nuxt frontend to render documentation from Markdown files. The documentation is deployed at [https://memos.openmem.net/docs/home](https://memos.openmem.net/docs/home). - -## Adding a New Document - -1. Create a new `.md` file in the `docs/` directory or one of its subdirectories. -2. Add content to the file using Markdown syntax. -3. Add the new document to the `nav` section in `docs/settings.yml`. -4. Once the your changes are merged into the `main` branch, the documentation will be automatically updated. - -## Navigation Icons - -When adding entries to the navigation in `docs/settings.yml`, you can include icons using the syntax `(ri:icon-name)`. For example: - -```yaml -- "(ri:home-line) Home": overview.md -- "(ri:team-line) Users": modules/mos/users.md -- "(ri:flask-line) Writing Tests": contribution/writing_tests.md -``` - -The frontend will render these as actual icons. You can find available icons at [https://icones.js.org/](https://icones.js.org/). - -## Previewing the Documentation - -To preview a simple version of the documentation locally, run the following command from the root of the project: - -```bash -make docs -``` - -This command will start a local web server, and you can view the documentation by opening the URL provided in the terminal (usually `http://127.0.0.1:8000`). diff --git a/docs/contribution/writing_tests.md b/docs/contribution/writing_tests.md deleted file mode 100644 index 4791a7c13..000000000 --- a/docs/contribution/writing_tests.md +++ /dev/null @@ -1,42 +0,0 @@ -# How to Write Unit Tests - -This project uses [pytest](https://docs.pytest.org/) for unit testing. - -## Writing a Test - -1. Create a new Python file in the `tests/` directory. The filename should start with `test_`. -2. Inside the file, create functions whose names start with `test_`. -3. Use the `assert` statement to check for expected outcomes. - -Here is a basic example: - -```python -# tests/test_example.py - -def test_addition(): - assert 1 + 1 == 2 -``` - -## Running Tests - -To run all the tests, execute the following command from the root of the project: - -```bash -make test -``` - -This will discover and run all the tests in the `tests/` directory. - -## Advanced Techniques - -Pytest has many advanced features, such as fixtures and mocking. - -### Fixtures - -Fixtures are functions that can provide data or set up state for your tests. They are defined using the `@pytest.fixture` decorator. - -### Mocking - -Mocking is used to replace parts of your system with mock objects. This is useful for isolating the code you are testing. The `unittest.mock` library is commonly used for this, often with the `patch` function. - -For an example of mocking, see `tests/test_hello_world.py`. diff --git a/docs/getting_started/examples.md b/docs/getting_started/examples.md deleted file mode 100644 index dffd933d9..000000000 --- a/docs/getting_started/examples.md +++ /dev/null @@ -1,412 +0,0 @@ -# MemOS Examples - -Congratulations — you’ve mastered the Quick Start and built your first -working memory! Now it’s time to see how far you can take MemOS by combining -different memory types and features. Use these curated examples to inspire -your own agents, chatbots, or knowledge systems. - -::card-group - - :::card - --- - icon: ri:play-line - title: Minimal Pipeline - to: /docs/getting_started/examples#example-1-minimal-pipeline - --- - The smallest working pipeline — add, search, update and dump plaintext memories. - ::: - - :::card - --- - icon: ri:tree-line - title: TreeTextMemory Only - to: /docs/getting_started/examples#example-2-treetextmemory-only - --- - Use Neo4j-backed hierarchical memory to build structured, multi-hop knowledge graphs. - ::: - - :::card - --- - icon: ri:database-2-line - title: KVCacheMemory Only - to: /docs/getting_started/examples#example-3-kvcachememory-only - --- - Speed up sessions with short-term KV cache for fast context injection. - ::: - - :::card - --- - icon: hugeicons:share-07 - title: Hybrid TreeText + KVCache - to: /docs/getting_started/examples#example-4-hybrid - --- - Combine explainable graph memory with fast KV caching in a single MemCube. - ::: - - :::card - --- - icon: ri:calendar-check-line - title: Multi-Memory Scheduling - to: /docs/getting_started/examples#example-5-multi-memory-scheduling - --- - Run dynamic memory orchestration for multi-user, multi-session agents. - ::: - -:: - - -## Example 1: Minimal Pipeline - -### When to Use: -- You want the smallest possible working example. -- You only need simple plaintext memories stored in a vector DB. -- Best for getting started or testing your embedding + vector pipeline. - -### Key Points: -- Uses GeneralTextMemory only (no graph, no KV cache). -- Add, search, update and dump memories. -- Integrates a basic MOS pipeline. - -### Full Example Code -```python -import uuid -from memos.configs.mem_os import MOSConfig -from memos.mem_os.main import MOS - - -# init MOSConfig -mos_config = MOSConfig.from_json_file("examples/data/config/simple_memos_config.json") -mos = MOS(mos_config) - -# Create a user and register a memory cube -user_id = str(uuid.uuid4()) -mos.create_user(user_id=user_id) -mos.register_mem_cube("examples/data/mem_cube_2", user_id=user_id) - -# Add a simple conversation -mos.add( - messages=[ - {"role": "user", "content": "I love playing football."}, - {"role": "assistant", "content": "That's awesome!"} - ], - user_id=user_id -) - -# Search the memory -result = mos.search(query="What do you love?", user_id=user_id) -print("Memories found:", result["text_mem"]) - -# Dump and reload -mos.dump("tmp/my_mem_cube") -mos.load("tmp/my_mem_cube") -``` - -## Example 2: TreeTextMemory Only - -### When to Use: -- You need hierarchical graph-based memories with explainable relations. -- You want to store structured knowledge and trace connections. -- Suitable for knowledge graphs, concept trees, and multi-hop reasoning. - -### Key Points: -- Uses TreeTextMemory backed by Neo4j. -- Requires extractor_llm + dispatcher_llm. -- Stores nodes, edges, and supports traversal queries. - -### Full Example Code -```python -from memos.configs.embedder import EmbedderConfigFactory -from memos.configs.memory import TreeTextMemoryConfig -from memos.configs.mem_reader import SimpleStructMemReaderConfig -from memos.embedders.factory import EmbedderFactory -from memos.mem_reader.simple_struct import SimpleStructMemReader -from memos.memories.textual.tree import TreeTextMemory - -# Setup Embedder -embedder_config = EmbedderConfigFactory.model_validate({ - "backend": "ollama", - "config": {"model_name_or_path": "nomic-embed-text:latest"} -}) -embedder = EmbedderFactory.from_config(embedder_config) - -# Create TreeTextMemory -tree_config = TreeTextMemoryConfig.from_json_file("examples/data/config/tree_config.json") -my_tree_textual_memory = TreeTextMemory(tree_config) -my_tree_textual_memory.delete_all() - -# Setup Reader -reader_config = SimpleStructMemReaderConfig.from_json_file( - "examples/data/config/simple_struct_reader_config.json" -) -reader = SimpleStructMemReader(reader_config) - -# Extract from conversation -scene_data = [[ - {"role": "user", "content": "Tell me about your childhood."}, - {"role": "assistant", "content": "I loved playing in the garden with my dog."} -]] -memory = reader.get_memory(scene_data, type="chat", info={"user_id": "1234", "session_id": "2222"}) -for m_list in memory: - my_tree_textual_memory.add(m_list) - -# Search -results = my_tree_textual_memory.search( - "Talk about the user's childhood story?", - top_k=10 -) - -# [Optional] Dump & Drop -my_tree_textual_memory.dump("tmp/my_tree_textual_memory") -my_tree_textual_memory.drop() -``` - -## Example 3: KVCacheMemory Only - -### When to Use: -- You want short-term working memory for faster multi-turn conversation. -- Useful for chatbot session acceleration or prompt reuse. -- Best for caching hidden states / KV pairs. - -### Key Points: -- Uses KVCacheMemory with no explicit text memory. -- Demonstrates extract → add → merge → get → delete. -- Shows how to dump/load KV caches. - -### Full Example Code - -```python -from memos.configs.memory import MemoryConfigFactory -from memos.memories.factory import MemoryFactory - -# Create config for KVCacheMemory (HuggingFace backend) -config = MemoryConfigFactory( - backend="kv_cache", - config={ - "extractor_llm": { - "backend": "huggingface", - "config": { - "model_name_or_path": "Qwen/Qwen3-0.6B", - "max_tokens": 32, - "add_generation_prompt": True, - "remove_think_prefix": True, - }, - }, - }, -) - -# Instantiate KVCacheMemory -kv_mem = MemoryFactory.from_config(config) - -# Extract a KVCacheItem (DynamicCache) -prompt = [ - {"role": "user", "content": "What is MemOS?"}, - {"role": "assistant", "content": "MemOS is a memory operating system for LLMs."}, -] -print("===== Extract KVCacheItem =====") -cache_item = kv_mem.extract(prompt) -print(cache_item) - -# Add the cache to memory -kv_mem.add([cache_item]) -print("All caches:", kv_mem.get_all()) - -# Get by ID -retrieved = kv_mem.get(cache_item.id) -print("Retrieved:", retrieved) - -# Merge caches (simulate multi-turn) -item2 = kv_mem.extract([{"role": "user", "content": "Tell me a joke."}]) -kv_mem.add([item2]) -merged = kv_mem.get_cache([cache_item.id, item2.id]) -print("Merged cache:", merged) - -# Delete one -kv_mem.delete([cache_item.id]) -print("After delete:", kv_mem.get_all()) - -# Dump & load caches -kv_mem.dump("tmp/kv_mem") -print("Dumped to tmp/kv_mem") -kv_mem.delete_all() -kv_mem.load("tmp/kv_mem") -print("Loaded caches:", kv_mem.get_all()) -``` - -## Example 4: Hybrid - -### When to Use: -- You want long-term explainable memory and short-term fast context together. -- Ideal for complex agents that plan, remember facts, and keep chat context. -- Demonstrates multi-memory orchestration. - -### How It Works: - -- **TreeTextMemory** stores your long-term knowledge in a graph DB (Neo4j). -- **KVCacheMemory** stores recent or stable context as activation caches. -- Both work together in a single **MemCube**, managed by your `MOS` pipeline. - - -### Full Example Code - -```python -import os - -from memos.configs.mem_cube import GeneralMemCubeConfig -from memos.configs.mem_os import MOSConfig -from memos.mem_cube.general import GeneralMemCube -from memos.mem_os.main import MOS - -# 1. Setup CUDA (if needed) — for local GPU inference -os.environ["CUDA_VISIBLE_DEVICES"] = "1" - -# 2. Define user & paths -user_id = "root" -cube_id = "root/mem_cube_kv_cache" -tmp_cube_path = "/tmp/default/mem_cube_5" - -# 3. Initialize MOSConfig -mos_config = MOSConfig.from_json_file("examples/data/config/simple_treekvcache_memos_config.json") -mos = MOS(mos_config) - -# 4. Initialize the MemCube (TreeTextMemory + KVCacheMemory) -cube_config = GeneralMemCubeConfig.from_json_file( - "examples/data/config/simple_treekvcache_cube_config.json" -) -mem_cube = GeneralMemCube(cube_config) - -# 5. Dump the MemCube to disk -try: - mem_cube.dump(tmp_cube_path) -except Exception as e: - print(e) - -# 6. Register the MemCube explicitly -mos.register_mem_cube(tmp_cube_path, mem_cube_id=cube_id, user_id=user_id) - -# 7. Extract and add a KVCache memory (simulate stable context) -extract_kvmem = mos.mem_cubes[cube_id].act_mem.extract("I like football") -mos.mem_cubes[cube_id].act_mem.add([extract_kvmem]) - -# 8. Start chatting — now your chat uses: -# - TreeTextMemory: for structured multi-hop retrieval -# - KVCacheMemory: for fast context injection -while True: - user_input = input("👤 [You] ").strip() - print() - response = mos.chat(user_input) - print(f"🤖 [Assistant] {response}\n") - -print("📢 [System] MemChat has stopped.") -``` - -## Example 5: Multi-Memory Scheduling - -### When to Use: -- You want to manage multiple users, multiple MemCubes, or dynamic memory flows. -- Good for SaaS agents or multi-session LLMs. -- Demonstrates MemScheduler + config YAMLs. - -### Key Points: -- Uses parse_yaml to load MOSConfig and MemCubeConfig. -- Dynamic user and cube creation. -- Shows runtime scheduling of memories. - -### Full Example Code - -```python -import shutil -import uuid -from pathlib import Path - -from memos.configs.mem_cube import GeneralMemCubeConfig -from memos.configs.mem_os import MOSConfig -from memos.mem_cube.general import GeneralMemCube -from memos.mem_os.main import MOS -from memos.mem_scheduler.utils import parse_yaml - -# Load main MOS config with MemScheduler -config = parse_yaml("./examples/data/config/mem_scheduler/memos_config_w_scheduler.yaml") -mos_config = MOSConfig(**config) -mos = MOS(mos_config) - -# Create user with dynamic ID -user_id = str(uuid.uuid4()) -mos.create_user(user_id=user_id) - -# Create MemCube config and dump it -config = GeneralMemCubeConfig.from_yaml_file( - "./examples/data/config/mem_scheduler/mem_cube_config.yaml" -) -mem_cube_id = "mem_cube_5" -mem_cube_name_or_path = f"./outputs/mem_scheduler/{user_id}/{mem_cube_id}" - -# Remove old folder if exists -if Path(mem_cube_name_or_path).exists(): - shutil.rmtree(mem_cube_name_or_path) - print(f"{mem_cube_name_or_path} is not empty, and has been removed.") - -# Dump new cube -mem_cube = GeneralMemCube(config) -mem_cube.dump(mem_cube_name_or_path) - -# Register MemCube for this user -mos.register_mem_cube( - mem_cube_name_or_path=mem_cube_name_or_path, - mem_cube_id=mem_cube_id, - user_id=user_id -) - -# Add messages -messages = [ - { - "role": "user", - "content": "I like playing football." - }, - { - "role": "assistant", - "content": "I like playing football too." - }, -] -mos.add(messages, user_id=user_id, mem_cube_id=mem_cube_id) - -# Chat loop: show TreeTextMemory nodes + KVCache -while True: - user_input = input("👤 [You] ").strip() - print() - response = mos.chat(user_input, user_id=user_id) - retrieved_memories = mos.get_all(mem_cube_id=mem_cube_id, user_id=user_id) - - print(f"🤖 [Assistant] {response}") - - # Show WorkingMemory nodes in TreeTextMemory - for node in retrieved_memories["text_mem"][0]["memories"]["nodes"]: - if node["metadata"]["memory_type"] == "WorkingMemory": - print(f"[WorkingMemory] {node['memory']}") - - # Show Activation Memory - if retrieved_memories["act_mem"][0]["memories"]: - for act_mem in retrieved_memories["act_mem"][0]["memories"]: - print(f"⚡ [KVCache] {act_mem['memory']}") - else: - print("⚡ [KVCache] None\n") -``` - - - -::note -**Keep in Mind**
-Use dump() and load() to persist your memory cubes. - -Always check your vector DB dimension matches your embedder. - -For graph memory, you’ll need Neo4j Desktop (community version support coming soon). -:: - -## Next Steps -You’re just getting started!Next, try: - -- Pick the example that matches your use case. -- Combine modules to build smarter, more persistent agents! - -Need more? -See the API Reference or contribute your own example! diff --git a/docs/getting_started/quick_start.md b/docs/getting_started/quick_start.md deleted file mode 100644 index e03823e9d..000000000 --- a/docs/getting_started/quick_start.md +++ /dev/null @@ -1,109 +0,0 @@ -# Quick Start: Get Up and Running with MemOS - -## What You’ll Learn -Welcome! This guide will help you **install**, **initialize**, and **run your first memory-augmented LLM app** in just a few minutes. - -In just a few minutes, you’ll learn how to set up a **minimal, working MemOS pipeline** that connects your LLM with persistent, searchable memory. -By the end, you’ll be able to **store, retrieve, and update simple memories** for a user or session — the foundation for building memory-augmented chatbots and agents. - - -::steps{} - -### Install MemOS - -```bash -pip install MemoryOS -``` - -::note -**Optional**
If you want to use local transformer models, make sure you have PyTorch installed. -:: - -::note -**Requirement for Neo4j Desktop**
If you plan to use Neo4j for graph memory, install Neo4j Desktop (community edition support coming soon!) -:: - -### Create a Minimal Config - -For this Quick Start, we’ll use the built-in GeneralTextMemory — no external vector DB or graph DB needed. -```python -from memos.configs.mem_os import MOSConfig - -# init MOSConfig -mos_config = MOSConfig.from_json_file("examples/data/config/simple_memos_config.json") -``` - -### Create a User & Register a MemCube - -```python -import uuid -from memos.mem_os.main import MOS - -mos = MOS(mos_config) - -# Generate a unique user ID -user_id = str(uuid.uuid4()) - -# Create the user -mos.create_user(user_id=user_id) - -# Register a simple memory cube for this user -mos.register_mem_cube("examples/data/mem_cube_2", user_id=user_id) -``` - -### Add Your First Memory - -```python -# Add some conversational history -mos.add( - messages=[ - {"role": "user", "content": "I love playing football."}, - {"role": "assistant", "content": "That's awesome! "} - ], - user_id=user_id -) -``` - - -### Retrieve & Search Memory - -```python -# Search for memories related to your query -result = mos.search( - query="What does the user love?", - user_id=user_id -) - -print("Memories found:", result["text_mem"]) -``` - -### Save & Load Memory - -```python -# Save your memory cube -mos.dump("tmp/my_mem_cube") - -# Later, you can load it back -mos.load("tmp/my_mem_cube") -``` - -:: - -## Next Steps - -Congratulations! You’ve just run a minimal memory-augmented pipeline with MemOS. - -Ready to level up? -- Structured Memory: Try TreeTextMemory for graph-based, hierarchical -knowledge. -- Activation Memory: Speed up multi-turn chat with KVCacheMemory. -- Parametric Memory: Use adapters/LoRA for on-the-fly skill injection. -- Graph & Vector Backends: Connect Neo4j or Qdrant for production-scale - vector/graph search. - - -## Need Help? -Check out: -- [Core Concepts](/docs/home/core_concepts) -- [API Reference](/docs/api/info) -- [Contribution Guide](/docs/contribution/overview) diff --git a/docs/getting_started/your_first_memory.md b/docs/getting_started/your_first_memory.md deleted file mode 100644 index e154f0713..000000000 --- a/docs/getting_started/your_first_memory.md +++ /dev/null @@ -1,271 +0,0 @@ -# Your First Memory - -Let’s build your first plaintext memory in MemOS! - -**GeneralTextMemory** is the easiest way to get hands-on with extracting, -embedding, and searching simple text memories. - - -## What You’ll Learn - -By the end of this guide, you will: -- Extract memories from plain text or chat messages. -- Store them as semantic vectors. -- Search and manage them using vector similarity. - -## How It Works - -### Memory Structure - -Every memory is stored as a `TextualMemoryItem`: -- `memory`: the main text content (e.g., “The user loves tomatoes.”) -- `metadata`: extra details to make the memory searchable and manageable — type, - time, source, confidence, entities, tags, visibility, and updated_at. - -These fields make each piece of memory queryable, filterable, and easy to govern. - -For each `TextualMemoryItem`: - -| Field | Example | What it means | -| ------------- | ------------------------- | ------------------------------------------ | -| `type` | `"opinion"` | Classify if it’s a fact, event, or opinion | -| `memory_time` | `"2025-07-02"` | When it happened | -| `source` | `"conversation"` | Where it came from | -| `confidence` | `100.0` | Certainty score (0–100) | -| `entities` | `["tomatoes"]` | Key concepts | -| `tags` | `["food", "preferences"]` | Extra labels for grouping | -| `visibility` | `"private"` | Who can access it | -| `updated_at` | `"2025-07-02T00:00:00Z"` | Last modified | - -::note -**Best Practice**
You can define any metadata fields that make sense for your use case! -:: - - - -### The Core Steps -When you run this example: - -1. **Extract:** -Your messages go through an `extractor_llm`, which returns a JSON list of `TextualMemoryItem`s. - -2. **Embed:** -Each memory’s `memory` field is turned into an embedding vector via `embedder`. - -3. **Store:** -The embeddings are saved into a local **Qdrant** collection. - -4. **Search & Manage:** -You can now `search` by semantic similarity, `update` by ID, or `delete` memories. - -::note -**Hint**
Make sure your embedder's output dimension matches your vector DB's `vector_dimension`. - Mismatch may cause search errors! -:: - - - -::note -**Hint**
If your search results are too noisy or irrelevant, check whether your embedder config and vector DB are properly initialized. -:: - -### Example Flow - -**Input Messages:** - -```json -[ - {"role": "user", "content": "I love tomatoes."}, - {"role": "assistant", "content": "Great! Tomatoes are healthy."} -] -``` - -**Extracted Memory:** - -```json -{ - "memory": "The user loves tomatoes.", - "metadata": { - "type": "opinion", - "memory_time": "2025-07-02", - "source": "conversation", - "confidence": 100.0, - "entities": ["tomatoes"], - "tags": ["food", "preferences"], - "visibility": "private", - "updated_at": "2025-07-02T00:00:00" - } -} -``` - -Here’s a minimal script to create, extract, store, and search a memory: - -::steps{level="4"} - -#### Create a Memory Config - -First, create your minimal GeneralTextMemory config. -It contains three key parts: -- extractor_llm: uses an LLM to extract plaintext memories from conversations. -- embedder: turns each memory into a vector. -- vector_db: stores vectors and supports similarity search. - -```python -from memos.configs.memory import MemoryConfigFactory -from memos.memories.factory import MemoryFactory - -config = MemoryConfigFactory( - backend="general_text", - config={ - "extractor_llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.0, - "remove_think_prefix": True, - "max_tokens": 8192, - }, - }, - "vector_db": { - "backend": "qdrant", - "config": { - "collection_name": "test_textual_memory", - "distance_metric": "cosine", - "vector_dimension": 768, - }, - }, - "embedder": { - "backend": "ollama", - "config": { - "model_name_or_path": "nomic-embed-text:latest", - }, - }, - }, -) - -m = MemoryFactory.from_config(config) -``` - - -#### Extract Memories from Messages -Give your LLM a simple dialogue and see how it extracts structured plaintext memories. - -```python -memories = m.extract( - [ - {"role": "user", "content": "I love tomatoes."}, - {"role": "assistant", "content": "Great! Tomatoes are delicious."}, - ] -) -print("Extracted:", memories) -``` -You’ll get a list of TextualMemoryItem, with each of them like: -```text -TextualMemoryItem( - id='...', - memory='The user loves tomatoes.', - metadata=... -) -``` - -#### Add Memories to Your Vector DB - -Save the extracted memories to your vector DB and demonstrate adding a custom plaintext memory manually (with a custom ID). - -```python -m.add(memories) -m.add([ - { - "id": "a19b6caa-5d59-42ad-8c8a-e4f7118435b4", - "memory": "User is Chinese.", - "metadata": {"type": "opinion"}, - } -]) -``` - - -#### Search Memories - -Now test similarity search! -Type any natural language query and find related memories. -```python -results = m.search("Tell me more about the user", top_k=2) -print("Search results:", results) -``` - -#### Get Memories by ID - -Fetch any memory directly by its ID: -```python -print("Get one by ID:", m.get("a19b6caa-5d59-42ad-8c8a-e4f7118435b4")) -``` - -#### Update a Memory - -Need to fix or refine a memory? -Update it by ID and re-embed the new version. -```python -m.update( - "a19b6caa-5d59-42ad-8c8a-e4f7118435b4", - { - "memory": "User is Canadian.", - "metadata": { - "type": "opinion", - "confidence": 85, - "memory_time": "2025-05-24", - "source": "conversation", - "entities": ["Canadian"], - "tags": ["happy"], - "visibility": "private", - "updated_at": "2025-05-19T00:00:00", - }, - } -) -print("Updated:", m.get("a19b6caa-5d59-42ad-8c8a-e4f7118435b4")) -``` - -#### Delete Memories - -Remove one or more memories cleanly -```python -m.delete(["a19b6caa-5d59-42ad-8c8a-e4f7118435b4"]) -print("Remaining:", m.get_all()) -``` - -#### Dump Memories to Disk - -Finally, dump all your memories to local storage: -```python -m.dump("tmp/mem") -print("Memory dumped to tmp/mem") -``` -By default, your memories are saved to: -``` -/ -``` -They can be reloaded anytime with `load()`. - -::note -By default, your dumped memories are saved to the file path you set in your config. - Always check config.memory_filename if you want to customize it. -:: - -:: - -Now your agent remembers — no more stateless chatbots! - -## What’s Next? - -Ready to level up? - -- **Try your own LLM backend:** Swap to OpenAI, HuggingFace, or Ollama. -- **Explore [TreeTextMemory](/docs/modules/memories/tree_textual_memory):** Build a graph-based, - hierarchical memory. -- **Add [Activation Memory](/docs/modules/memories/kv_cache_memory):** Cache key-value - states for faster inference. -- **Dive deeper:** Check the [API Reference](/docs/api/info) and [Examples](/docs/getting_started/examples) for advanced workflows. - -::note -**Try Graph Textual Memory**
Try switching to -TreeTextMemory to add a graph-based, hierarchical structure to your memories.
Perfect for scenarios that need explainability and long-term structured knowledge. -:: diff --git a/docs/home/architecture.md b/docs/home/architecture.md deleted file mode 100644 index 9852df3e9..000000000 --- a/docs/home/architecture.md +++ /dev/null @@ -1,89 +0,0 @@ -# Architecture - -MemOS is made up of **core modules** that work together to turn your LLM into a truly **memory-augmented system** — from orchestration to storage to retrieval. - -## Core Modules - -### MOS (Memory Operating System) - -The orchestration layer of MemOS — it - manages predictive, asynchronous scheduling across multiple memory types (plaintext, activation, parametric) and orchestrates **multi-user, multi-session** memory workflows. - -MOS connects memory containers (**MemCubes**) with LLMs via a unified API for adding, searching, updating, transferring, or rolling back memories. It also supports cross-model, cross-device interoperability through a unified Memory Interchange Protocol (MIP). - -### MemCube -A modular, portable **memory container** — think of it like a flexible cartridge that can hold one or more memory types for a **user, agent, or session**. - -MemCubes can be dynamically registered, updated, or removed. They support containerized storage that is transferable across sessions, models, and devices. - -### Memories - - MemOS supports several specialized memory types for different needs: - -#### 1. **Parametric Memory**(**Coming Soon**) - -Embedded in model weights; - long-term, - high-efficiency, but hard to edit. - -#### 2. **Activation Memory** - -Runtime hidden states & KV-cache; short-term, -transient, steering dynamic behavior. - -#### 3. Plaintext Memory - -Structured or unstructured knowledge -blocks; editable, traceable, suitable for fast updates, personalization & multi-agent sharing. - -- **GeneralTextMemory:** Flexible, vector-based storage for unstructured -textual knowledge with semantic search and metadata filtering. -- **TreeTextMemory:** Hierarchical, graph-style memory for structured -knowledge — combining **tree-based hierarchy** and **cross-branch linking** for dynamic, evolving knowledge graphs. It supports long-term organization and multi-hop reasoning (often Neo4j-backed). - -::note -**Best Practice**
-Start simple with GeneralTextMemory — then scale to graph or KV-cache as your needs grow. -:: - -#### Basic Modules - -Includes chunkers, embedders, LLM connectors, parsers, and interfaces for vector/graph databases. These provide the building blocks for memory extraction, semantic embedding, storage, and retrieval. - -## Code Structure - -Your MemOS project is organized for clarity and plug-and-play: - -``` -src/memos/ - api/ # API definitions - chunkers/ # Text chunking utilities - configs/ # Configuration schemas - embedders/ # Embedding models - graph_dbs/ # Graph database backends (e.g., Neo4j) - vec_dbs/ # Vector database backends (e.g., Qdrant) - llms/ # LLM connectors - mem_chat/ # Memory-augmented chat logic - mem_cube/ # MemCube management - mem_os/ # MOS orchestration - mem_reader/ # Memory readers - memories/ # Memory type implementations - parsers/ # Parsing utilities -``` - -::note -**Pro Tip**
-Use examples/ for quick experimentation and docs/ for module deep dives. -:: - -## Extensibility - -MemOS is **modular by design**. -Add your own memory types, storage backends, or LLM connectors with minimal changes — thanks to its **unified config and factory patterns**. - - -::note -**Pro Tip**
-[Contribute](/docs/contribution/overview) a new backend or share your custom memory -type — it’s easy to plug in. -:: diff --git a/docs/home/core_concepts.md b/docs/home/core_concepts.md deleted file mode 100644 index 872384561..000000000 --- a/docs/home/core_concepts.md +++ /dev/null @@ -1,106 +0,0 @@ -# Core Concepts - -MemOS treats memory as a first-class citizen. Its core design revolves around how to orchestrate, store, retrieve, and govern memory for your LLM applications. - -## Overview - -* [MOS (Memory Operating System)](#mos-memory-operating-system) -* [MemCube](#️memcube) -* [Memory Types](#memory-types) -* [Cross-Cutting Concepts](#cross-cutting-concepts) - - -## MOS (Memory Operating System) - -**What it is:** -The orchestration layer that coordinates multiple MemCubes and memory operations. It connects your LLMs with structured, explainable memory for reasoning and planning. - -**When to use:** -Use MOS whenever you need to bridge users, sessions, or agents with consistent, auditable memory workflows. - -## MemCube - -**What it is:** -A MemCube is like a flexible, swappable memory cartridge. Each user, session, or task can have its own MemCube, which can hold one or more memory types. - -**When to use:** -Use different MemCubes to isolate, reuse, or scale your memory as your system grows. - -## Memory Types - -MemOS treats memory like a living system — not just static data but evolving knowledge. Here’s how the three core memory types work together: - -| Memory Type | Description | When to Use | -|----------------|----------------------------------------------|---------------------------------------------| -| **Parametric** | Knowledge distilled into model weights | Evergreen skills, stable domain expertise | -| **Activation** | Short-term KV cache and hidden states | Fast reuse in dialogue, multi-turn sessions | -| **Plaintext** | Text, docs, graph nodes, or vector chunks | Searchable, inspectable, evolving knowledge | - -### Parametric Memory - -**What:** -Knowledge embedded directly into the model’s weights — think of this as the model’s “cortex”. It’s always on, providing zero-latency reasoning. - -**When to use:** -Perfect for stable domain knowledge, distilled FAQs, or skills that rarely change. - -### Activation Memory - -**What:** -Activation Memory is your model’s reusable “working memory” — it includes precomputed key-value caches and hidden states that can be directly injected into the model’s attention mechanism. -Think of it as pre-cooked context that saves your LLM from repeatedly -re-encoding static or frequently used information. - -**Why it matters:** -By storing stable background content (like FAQs or known facts) in a KV-cache, your model can skip redundant computation during the prefill phase. -This dramatically reduces Time To First Token (TTFT) and improves throughput for multi-turn conversations or retrieval-augmented generation. - -**When to use:** -- Reuse background knowledge across many user queries. -- Speed up chatbots that rely on the same domain context each turn. -- Combine with MemScheduler to auto-promote stable plaintext memory to KV format. - -### Explicit Memory - -**What:** -Structured or unstructured knowledge units — user-visible, explainable. These can be documents, chat logs, graph nodes, or vector embeddings. - -**When to use:** -Best for semantic search, user preferences, or traceable facts that evolve over time. Supports tags, provenance, and lifecycle states. - - -## How They Work Together - -MemOS lets you orchestrate all three memory types in a living loop: - -- Hot plaintext memories can be distilled into parametric weights. -- High-frequency activation paths become reusable KV templates. -- Stale parametric or activation units can be downgraded to plaintext nodes for traceability. - -With MemOS, your AI doesn’t just store facts — it **remembers**, **understands**, and **grows**. - -::note -**Insight**
- Over time, frequently used plaintext memories can be distilled into parametric form. - Rarely used weights or caches can be demoted to plaintext storage for auditing and retraining. -:: - -## Cross-Cutting Concepts - -### Hybrid Retrieval - -Combines vector similarity and graph traversal for robust, context-aware search. - -### Governance & Lifecycle - -Every memory unit supports states (active, merged, archived), provenance tracking, and fine-grained access control — essential for auditing and compliance. - -::note -**Compliance Reminder**
-Always track provenance and state changes for each memory unit. - This helps meet audit and data governance requirements. -:: - -## Key Takeaway - -With MemOS, your LLM applications gain structured, evolving memory — empowering agents to plan, reason, and adapt like never before. diff --git a/docs/home/memos_intro.md b/docs/home/memos_intro.md deleted file mode 100644 index 46a37fa4d..000000000 --- a/docs/home/memos_intro.md +++ /dev/null @@ -1,118 +0,0 @@ -# What is MemOS? - -**MemOS** is a **Memory Operating System** for large language models (LLMs) and autonomous agents. -It treats memory as a **first-class, orchestrated, and explainable resource**, rather than an opaque layer hidden inside model weights. - - -![MemOS Architecture](https://statics.memtensor.com.cn/memos/memos-architecture.png) - - -As LLMs advance to handle complex tasks — like multi-turn dialogue, long-term planning, decision-making, and personalized user experiences — their ability to **structure, manage, and evolve memory** becomes critical for achieving true long-term intelligence and adaptability. - -However, most mainstream LLMs still rely heavily on static parametric memory (model weights). This makes it difficult to update knowledge, track memory usage, or accumulate evolving user preferences. The result? High costs to refresh knowledge, brittle behaviors, and limited personalization. - -**MemOS** solves these challenges by redefining memory as a **core, modular system resource** with a unified structure, lifecycle management, and scheduling logic. It provides a Python-based layer that sits between your LLM and external knowledge sources, enabling **persistent, structured, and efficient memory operations**. - -With MemOS, your LLM can retain knowledge over time, manage context more robustly, and reason with memory that’s explainable and auditable — unlocking more intelligent, reliable, and adaptive AI behaviors. - - -::note -**Tip**
MemOS helps bridge the gap between static parametric weights and dynamic, user-specific memory. - Think of it as your agent’s “brain”, with plug-and-play modules for text, graph, and activation memory. -:: - -## Why do we need a Memory OS? - -Modern LLMs are powerful—but static. -They rely heavily on **parametric memory** (the weights) that is hard to inspect, update, or share. -Typical vector search (RAG) helps retrieve external facts, but lacks unified governance, lifecycle control, or cross-agent sharing. - -**MemOS** changes this. -Think of it like an OS for memory: -just as an operating system schedules CPU, RAM, and files, MemOS **schedules, -transforms, and governs** multiple memory types — from parametric weights to ephemeral caches to plaintext, traceable knowledge. - -::note -**Insight**
MemOS helps your LLM evolve, by blending parametric, activation, and plaintext memory into a living loop. -:: - - -## Core Building Blocks -### MemCubes - -**Flexible containers** that hold one or more memory types. -Each user, session, or agent can have its own MemCube — swappable, reusable, and traceable. - -### Memory Lifecycle - -Each memory unit can flow through states like: - -- **Generated** → **Activated** → **Merged** → **Archived** → **Frozen** - -Every step is versioned with **provenance tracking** and audit logs. -Old memories can be “time-machined” back to prior versions for recovery or counterfactual simulations. - - -### Operation & Governance - -Modules like: - -- **MemScheduler** — dynamically transforms memory types for optimal reuse. -- **MemLifecycle** — manages state transitions, merging, and archiving. -- **MemGovernance** — handles access control, redaction, compliance, and audit trails. - - -::note -**Compliance Reminder**
Every memory unit carries full provenance metadata, so you can audit who created, modified, or queried it. -:: - - -## Multi-Perspective Memory - -MemOS blends **three memory forms** in a living loop: - -| Type | Description | Use Case | -|----------------| ---------------------------------------------------- | ---------------------------------------------- | -| **Parametric** | Knowledge distilled into model weights | Evergreen skills, stable domain facts | -| **Activation** | KV-caches and hidden states for inference reuse | Fast multi-turn chat, low-latency generation | -| **Plaintext** | Text, docs, graphs, vector chunks, user-visible facts| Semantic search, evolving, explainable memory | - -Over time: - -- Hot plaintext memories can be distilled into parametric weights. -- Stable context is promoted to KV-cache for rapid injection. -- Cold or outdated knowledge can be demoted for auditing. - - -## What makes MemOS different? - -- Hybrid retrieval — symbolic & semantic, vector + graph. -- Multi-agent & multi-user graphs — private and shared. -- Provenance & audit trail — every memory unit is governed and explainable. -- Automatic KV-cache promotion for stable context reuse. -- Lifecycle-aware scheduling — no more stale facts or bloated weights. - - -## Who is it for? - -- Conversational agents needing **multi-turn, evolving memory** -- Enterprise copilots handling **compliance, domain updates, and personalization** -- Multi-agent systems collaborating on a **shared knowledge graph** -- AI builders wanting modular, inspectable memory instead of black-box prompts - -## Key Takeaway - -**MemOS** upgrades your LLM from “just predicting tokens” -to an intelligent, evolving system that can **remember**, **reason**, and **adapt** — -like an operating system for your agent’s mind. - -**With MemOS, your AI doesn’t just store facts — it grows.** - -## Key Features - -- **Modular Memory Architecture**: Support for textual, activation (KV cache), and parametric (adapters/LoRA) memory. -- **MemCube**: Unified container for all memory types, with easy load/save and API access. -- **MOS**: Memory-augmented chat orchestration for LLMs, with plug-and-play memory modules. -- **Graph-based Backends**: Native support for Neo4j and other graph DBs for structured, explainable memory. -- **Easy Integration**: Works with HuggingFace, Ollama, and custom LLMs. -- **Extensible**: Add your own memory modules or backends. diff --git a/docs/home/overview.md b/docs/home/overview.md deleted file mode 100644 index df095f2b0..000000000 --- a/docs/home/overview.md +++ /dev/null @@ -1,48 +0,0 @@ -# MemOS Documentation - -![MemOS Banner](https://statics.memtensor.com.cn/memos/memos-banner.gif) - -Welcome to the official documentation for **MemOS** – a Python package designed to empower large language models (LLMs) with advanced, modular memory capabilities. - -- **Open Source**: [GitHub Repository](https://github.com/MemTensor/MemOS) -- **PyPI Package**: [MemoryOS on PyPI](https://pypi.org/project/MemoryOS) - -## What is MemOS? - -As large language models (LLMs) evolve to tackle advanced tasks—such as multi-turn dialogue, planning, decision-making, and personalized agents—their ability to manage and utilize memory becomes crucial for achieving long-term intelligence and adaptability. However, mainstream LLM architectures often struggle with weak memory structuring, management, and integration, leading to high knowledge update costs, unsustainable behavioral states, and difficulty in accumulating user preferences. - -**MemOS** addresses these challenges by redefining memory as a core, first-class resource with unified structure, lifecycle management, and scheduling strategies. It provides a Python package that delivers a unified memory layer for LLM-based applications, enabling persistent, structured, and efficient memory operations. This empowers LLMs with long-term knowledge retention, robust context management, and memory-augmented reasoning, supporting more intelligent and adaptive behaviors. - -![MemOS Architecture](https://statics.memtensor.com.cn/memos/memos-architecture.png) - -## Key Features - -- **Modular Memory Architecture**: Support for textual, activation (KV cache), and parametric (adapters/LoRA) memory. -- **MemCube**: Unified container for all memory types, with easy load/save and API access. -- **MOS**: Memory-augmented chat orchestration for LLMs, with plug-and-play memory modules. -- **Graph-based Backends**: Native support for Neo4j and other graph DBs for structured, explainable memory. -- **Easy Integration**: Works with HuggingFace, Ollama, and custom LLMs. -- **Extensible**: Add your own memory modules or backends. - -## Installation - -```bash -pip install MemoryOS -``` - -To use with Ollama: - -```bash -curl -fsSL https://ollama.com/install.sh | sh -``` - -For transformer models, ensure [PyTorch](https://pytorch.org/get-started/locally/) is installed. - -## Contributing - -We welcome contributions! Please see the [contribution guidelines](/docs/contribution/overview) for details on setting up your environment and -submitting pull requests. - -## License - -MemOS is released under the Apache 2.0 License. diff --git a/docs/modules/mem_cube.md b/docs/modules/mem_cube.md deleted file mode 100644 index f2c90dca9..000000000 --- a/docs/modules/mem_cube.md +++ /dev/null @@ -1,78 +0,0 @@ -# MemCube Overview - -`MemCube` is the core organizational unit in MemOS, designed to encapsulate and manage all types of memory for a user or agent. It provides a unified interface for loading, saving, and operating on multiple memory modules, making it easy to build, share, and deploy memory-augmented applications. - -## What is a MemCube? - -A **MemCube** is a container that bundles three major types of memory: - -- **Textual Memory** (e.g., `GeneralTextMemory`, `TreeTextMemory`): For storing and retrieving unstructured or structured text knowledge. -- **Activation Memory** (e.g., `KVCacheMemory`): For storing key-value caches to accelerate LLM inference and context reuse. -- **Parametric Memory** (e.g., `LoRAMemory`): For storing model adaptation parameters (like LoRA weights). - -Each memory type is independently configurable and can be swapped or extended as needed. - -## Structure - -A MemCube is defined by a configuration (see `GeneralMemCubeConfig`), which specifies the backend and settings for each memory type. The typical structure is: - -``` -MemCube - ├── text_mem: TextualMemory - ├── act_mem: ActivationMemory - └── para_mem: ParametricMemory -``` - -All memory modules are accessible via the MemCube interface: -- `mem_cube.text_mem` -- `mem_cube.act_mem` -- `mem_cube.para_mem` - -## API Summary (`GeneralMemCube`) - -### Initialization -```python -from memos.mem_cube.general import GeneralMemCube -mem_cube = GeneralMemCube(config) -``` - -### Core Methods -| Method | Description | -| --------------| ------------------------------------------------ | -| `load(dir)` | Load all memories from a directory | -| `dump(dir)` | Save all memories to a directory | -| `text_mem` | Access the textual memory module | -| `act_mem` | Access the activation memory module | -| `para_mem` | Access the parametric memory module | -| `init_from_dir(dir)` | Load a MemCube from a directory | -| `init_from_remote_repo(repo, base_url)` | Load from remote repo | - -## File Storage - -A MemCube directory contains: -- `config.json` (MemCube configuration) -- `textual_memory.json` (textual memory) -- `activation_memory.pickle` (activation memory) -- `parametric_memory.adapter` (parametric memory) - -## Example Usage - -```python -from memos.mem_cube.general import GeneralMemCube -# Load from local directory -mem_cube = GeneralMemCube.init_from_dir("examples/data/mem_cube_2") -# Load from remote repo -mem_cube = GeneralMemCube.init_from_remote_repo("Ki-Seki/mem_cube_2") -# Access and print all memories -for item in mem_cube.text_mem.get_all(): - print(item) -for item in mem_cube.act_mem.get_all(): - print(item) -mem_cube.dump("tmp/mem_cube") -``` - -## Developer Notes - -* MemCube enforces schema consistency for safe loading/dumping -* Each memory type is pluggable and independently tested -* See `/tests/mem_cube/` for integration tests and usage patterns diff --git a/docs/modules/mem_reader.md b/docs/modules/mem_reader.md deleted file mode 100644 index d7cb340eb..000000000 --- a/docs/modules/mem_reader.md +++ /dev/null @@ -1,188 +0,0 @@ -# Getting Started with MemReader - -This guide walks you through how to use the `SimpleStructMemReader` to extract structured memories from conversations and documents using LLMs and embedding models. It is ideal for building memory-aware conversational AI, knowledge bases, and semantic search systems. - ---- - -## Initialize a `SimpleStructMemReader` - -First, configure and initialize the reader with your preferred LLM and embedder models. - -### Example: - -```python -from memos.configs.mem_reader import SimpleStructMemReaderConfig -from memos.mem_reader.simple_struct import SimpleStructMemReader -reader_config = SimpleStructMemReaderConfig.from_json_file( - "examples/data/config/simple_struct_reader_config.json" -) -reader = SimpleStructMemReader(reader_config) -``` -::tip -You can customize the model names or backends depending on your environment. -:: ---- - -## Get Your First Chat Memory - -Extract structured memories from a conversation between a user and assistant. - -### Example Input: - -```python -conversation_data = [ - [ - {"role": "user", "content": "I have a meeting tomorrow at 3 PM"}, - {"role": "assistant", "content": "What's the meeting about?"}, - {"role": "user", "content": "It's about the Q4 project deadline"} - ] -] -``` - -### Extract Memories: - -```python -memories = reader.get_memory( - conversation_data, - type="chat", - info={"user_id": "user_001", "session_id": "session_001"} -) -``` - -### Sample Output: - -```json -[ - TextualMemoryItem( - id='2d5965f9-4c9b-4c24-9068-325b53db098b', - memory='Tomorrow at 3:00 PM, the user will meet with the Q4 project team to discuss the deadline.', - metadata=TreeNodeTextualMemoryMetadata( - user_id='user_001', - session_id='session_001', - status='activated', - type='fact', - confidence=0.99, - tags=['deadline', 'project'], - visibility=None, - updated_at='2025-07-03T14:34:33.535844', - memory_type='UserMemory', - key='Meeting schedule', - sources=[ - "user: I have a meeting tomorrow at 3 PM", - "assistant: What's the meeting about?", - "user: It's about the Q4 project deadline" - ], - embedding=[0.0058597163, ..., 0.009375607], - created_at='2025-07-03T14:34:33.535860', - usage=[], - background="The user plans to meet with the Q4 project team tomorrow at 3:00 PM to address the project's deadline. This action reflects their proactive approach to managing project timelines and their focus on ensuring timely completion." - ) - ) -] -``` -::note -The reader extract related memories and tags from the conversation session. -:: ---- - -## Get Your First Document Memory - -Process text files to extract structured summaries and tags. - -### Example Code: - -```python -doc_paths = [ - "examples/mem_reader/text1.txt", - "examples/mem_reader/text2.txt", -] - -doc_memories = reader.get_memory( - doc_paths, - type="doc", - info={ - "user_id": "user_001", - "session_id": "session_001", - "chunk_size": 512, - "chunk_overlap": 128 - } -) -``` - -### Sample Output: - -```json -[ - TextualMemoryItem( - id='24dabd9f-200b-40c4-84cc-2c0fccaaf8fd', - memory='This is another sample document content for testing purposes.', - metadata=TreeNodeTextualMemoryMetadata( - user_id='user_001', - session_id='session_001', - status='activated', - type='fact', - memory_time=None, - source=None, - confidence=0.99, - entities=None, - tags=['Testing', 'Sample'], - visibility=None, - updated_at='2025-07-03T14:38:29.776147', - memory_type='LongTermMemory', - key='', - sources=['examples/mem_reader/text2.txt_0'], - embedding=[0.028731367, ..., -0.018501928], - created_at='2025-07-03T14:38:29.776213', - usage=[], - background='' - ) - ) -] -``` -::note -Documents are chunked and summarized to create searchable knowledge items. -:: - -### Supported Files - -We use [`markitdown`](https://github.com/microsoft/markitdown) to convert files to Markdown format texts. - -**MarkItDown currently supports the conversion from:** - -``` -PDF -PowerPoint -Word -Excel -Images (EXIF metadata and OCR) -Audio (EXIF metadata and speech transcription) -HTML -Text-based formats (CSV, JSON, XML) -ZIP files (iterates over contents) -YouTube URLs -EPUBs -... and more! -``` -*(Content sourced from [MarkItDown GitHub repository](https://github.com/microsoft/markitdown))* - ---- - - -## Try It Out: Print Extracted Memories - -```python -for memory_list in memories: - for memory_item in memory_list: - print("🧠 Memory:", memory_item.memory) - print("🏷 Tags:", memory_item.metadata.tags) - print("👤 User ID:", memory_item.metadata.user_id) - print("📅 Created At:", memory_item.metadata.created_at) - print("---") -``` - ---- - -You’ve now successfully: -- Initialized a `SimpleStructMemReader` -- Extracted structured memories from chat conversations -- Extracted knowledge from documents diff --git a/docs/modules/mem_scheduler.md b/docs/modules/mem_scheduler.md deleted file mode 100644 index 8d2dccfe7..000000000 --- a/docs/modules/mem_scheduler.md +++ /dev/null @@ -1,115 +0,0 @@ -# MemScheduler: The Scheduler for Memory Organization - -`MemScheduler` is a concurrent memory management system parallel running with the MemOS system, which coordinates memory operations between working memory, long-term memory, and activation memory in AI systems. It handles memory retrieval, updates, and compaction through event-driven scheduling. - -This system is particularly suited for conversational agents and reasoning systems requiring dynamic memory management. - - -## Memory Scheduler Architecture - -The `MemScheduler` system is structured around several key components: - -1. **Message Handling**: Processes incoming messages through a dispatcher with labeled handlers -2. **Memory Management**: Manages different memory types (Working, Long-Term, User) -3. **Retrieval System**: Efficiently retrieves relevant memory items based on context -4. **Monitoring**: Tracks memory usage, frequencies, and triggers updates -5. **Logging**: Maintains logs of memory operations for debugging and analysis - -## Message Processing - -The scheduler processes messages through a dispatcher with dedicated handlers: - -### Message Types - -| Message Type | Handler Method | Description | -|--------------|---------------------------------|--------------------------------------------| -| `QUERY_LABEL` | `_query_message_consume` | Handles user queries and triggers retrieval | -| `ANSWER_LABEL`| `_answer_message_consume` | Processes answers and updates memory usage | - -### Message Structure (`ScheduleMessageItem`) - -| Field | Type | Description | -|---------------|----------------------|-----------------------------------------------| -| `item_id` | `str` | UUID (auto-generated) for unique identification | -| `user_id` | `str` | Identifier for the associated user | -| `mem_cube_id` | `str` | Identifier for the memory cube | -| `label` | `str` | Message label (e.g., `QUERY_LABEL`, `ANSWER_LABEL`) | -| `mem_cube` | `GeneralMemCube | str` | Memory cube object or reference | -| `content` | `str` | Message content | -| `timestamp` | `datetime` | Time when the message was submitted | - -## Memory Management - -### Memory Types and Sizes - -The scheduler manages multiple memory partitions with configurable capacities: - -| Memory Type | Description | Default Capacity | -|---------------------------|------------------------------------------|------------------| -| `long_term_memory` | Persistent knowledge storage | 10,000 items | -| `user_memory` | User-specific knowledge and interactions | 10,000 items | -| `working_memory` | Active context for current interactions | 20 items | -| `transformed_act_memory` | Transformed activation memory (dynamic) | Not initialized | - -### Configuration Parameters - -| Parameter | Description | Default Value | -|----------------------------|-----------------------------------------------------------------------------|---------------| -| `top_k` | Number of candidates to retrieve during initial search | 10 | -| `top_n` | Number of final results to return after processing | 5 | -| `enable_parallel_dispatch` | Enable parallel message processing using thread pool | `True` | -| `thread_pool_max_workers` | Maximum number of worker threads in the pool | 5 | -| `consume_interval_seconds` | Interval (in seconds) for consuming messages from the queue | 3 | -| `act_mem_update_interval` | Interval (in seconds) for updating activation memory | 300 | -| `context_window_size` | Size of the context window for conversation history | 5 | -| `activation_mem_size` | Maximum size of the activation memory - - -## Execution Example - -`examples/mem_scheduler/schedule_w_memos.py` is a demonstration script showcasing how to utilize the `MemScheduler` module. It illustrates memory management and retrieval within conversational contexts. - -### Code Functionality Overview - -This script demonstrates two methods for initializing and using the memory scheduler: - -1. **Automatic Initialization**: Configures the scheduler via configuration files -2. **Manual Initialization**: Explicitly creates and configures scheduler components - -The script simulates a pet-related conversation between a user and an assistant, demonstrating how memory scheduler manages conversation history and retrieves relevant information. - -### Core Code Structure - -```python -def init_task(): - # Initialize sample conversations and questions - conversations = [ - {"role": "user", "content": "I just adopted a golden retriever puppy yesterday."}, - {"role": "assistant", "content": "Congratulations! What did you name your new puppy?"}, - # More conversations... - ] - - questions = [ - {"question": "What's my dog's name again?", "category": "Pet"}, - # More questions... - ] - return conversations, questions - -def run_with_automatic_scheduler_init(): - # Automatic initialization: Load configuration from YAML files - # Create user and memory cube - # Add conversations to memory - # Process user queries and display answers - # Show web logs - -def run_with_manual_scheduler_init(): - # Manual initialization: Explicitly create and configure scheduler components - # Initialize MemOS, user, and memory cube - # Manually submit messages to the scheduler - # Process user queries and display answers - # Show web logs - -if __name__ == '__main__': - # Run both initialization methods sequentially - run_with_automatic_scheduler_init() - run_with_manual_scheduler_init( diff --git a/docs/modules/memories/general_textual_memory.md b/docs/modules/memories/general_textual_memory.md deleted file mode 100644 index ebe206bab..000000000 --- a/docs/modules/memories/general_textual_memory.md +++ /dev/null @@ -1,103 +0,0 @@ -# GeneralTextMemory: General-Purpose Textual Memory - -`GeneralTextMemory` is a flexible, vector-based textual memory module in MemOS, designed for storing, searching, and managing unstructured knowledge. It is suitable for conversational agents, personal assistants, and any system requiring semantic memory retrieval. - -## Memory Structure - -Each memory is represented as a `TextualMemoryItem`: - -| Field | Type | Description | -| ---------- | --------------------------- | ---------------------------------- | -| `id` | `str` | UUID (auto-generated if omitted) | -| `memory` | `str` | The main memory content (required) | -| `metadata` | `TextualMemoryMetadata` | Metadata for search/filtering | - -### Metadata Fields (`TextualMemoryMetadata`) - -| Field | Type | Description | -| ------------- | -------------------------------------------------- | ----------------------------------- | -| `type` | `"procedure"`, `"fact"`, `"event"`, `"opinion"` | Memory type | -| `memory_time` | `str (YYYY-MM-DD)` | Date/time the memory refers to | -| `source` | `"conversation"`, `"retrieved"`, `"web"`, `"file"` | Source of the memory | -| `confidence` | `float (0-100)` | Certainty/confidence score | -| `entities` | `list[str]` | Key entities/concepts | -| `tags` | `list[str]` | Thematic tags | -| `visibility` | `"private"`, `"public"`, `"session"` | Access scope | -| `updated_at` | `str` | Last update timestamp (ISO 8601) | - -All values are validated. Invalid values will raise errors. - -## API Summary (`GeneralTextMemory`) - -### Initialization -```python -GeneralTextMemory(config: GeneralTextMemoryConfig) -``` - -### Core Methods -| Method | Description | -| ------------------------ | --------------------------------------------------- | -| `extract(messages)` | Extracts memories from message list (LLM-based) | -| `add(memories)` | Adds one or more memories (items or dicts) | -| `search(query, top_k)` | Retrieves top-k memories using vector similarity | -| `get(memory_id)` | Fetch single memory by ID | -| `get_by_ids(ids)` | Fetch multiple memories by IDs | -| `get_all()` | Returns all memories | -| `update(memory_id, new)` | Update a memory by ID | -| `delete(ids)` | Delete memories by IDs | -| `delete_all()` | Delete all memories | -| `dump(dir)` | Serialize all memories to JSON file in directory | -| `load(dir)` | Load memories from saved file | - -## File Storage - -When calling `dump(dir)`, the system writes to: - -``` -

/ -``` - -This file contains a JSON list of all memory items, which can be reloaded using `load(dir)`. - -## Example Usage - -```python -from memos.configs.memory import MemoryConfigFactory -from memos.memories.factory import MemoryFactory - -config = MemoryConfigFactory( - backend="general_text", - config={ - "extractor_llm": { ... }, - "vector_db": { ... }, - "embedder": { ... }, - }, -) -m = MemoryFactory.from_config(config) - -# Extract and add memories -memories = m.extract([ - {"role": "user", "content": "I love tomatoes."}, - {"role": "assistant", "content": "Great! Tomatoes are delicious."}, -]) -m.add(memories) - -# Search -results = m.search("Tell me more about the user", top_k=2) - -# Update -m.update(memory_id, {"memory": "User is Canadian.", ...}) - -# Delete -m.delete([memory_id]) - -# Dump/load -m.dump("tmp/mem") -m.load("tmp/mem") -``` - -## Developer Notes - -* Uses Qdrant (or compatible) vector DB for fast similarity search -* Embedding and extraction models are configurable (Ollama/OpenAI supported) -* All methods are covered by integration tests in `/tests` diff --git a/docs/modules/memories/kv_cache_memory.md b/docs/modules/memories/kv_cache_memory.md deleted file mode 100644 index 9ad6ab731..000000000 --- a/docs/modules/memories/kv_cache_memory.md +++ /dev/null @@ -1,181 +0,0 @@ -# KVCacheMemory: Key-Value Cache for Activation Memory - -`KVCacheMemory` is a specialized memory module in MemOS for storing and managing key-value (KV) caches, primarily used to accelerate large language model (LLM) inference and support efficient context reuse. It is especially useful for activation memory in conversational and generative AI systems. - -## KV-cache Memory Use Cases - -In MemOS, KV-cache memory is best suited for storing **semantically stable and frequently reused background content** such as: - -- Frequently asked questions (FAQs) or domain-specific knowledge -- Prior conversation history - -These stable **plaintext memory items** are automatically identified and managed by the `MemScheduler` module. Once selected, they are converted into KV-format representations (`KVCacheItem`) ahead of time. This precomputation step stores the activation states (Key/Value tensors) of the memory in a reusable format, allowing them to be injected into the model’s attention cache during inference. - -Once converted, these KV memories can be **reused across queries without requiring re-encoding** of the original content. This reduces the computational overhead of processing and storing large amounts of text, making it ideal for applications that require **rapid response times** and **high throughput**. - - -## Why KV-cache Memory -Integrating `MemScheduler` with KV-cache memory enables significant performance optimization, particularly in the **prefill phase** of LLM inference. - -### Without KVCacheMemory - -- Each new query is appended to the full prompt, including the background memory. -- The model must **recompute token embeddings and attention** over the full sequence — even for unchanged memory. - -### With KVCacheMemory - -- The background content is **cached once** as Key/Value tensors. -- For each query, only the new user input (query tokens) is encoded. -- The previously cached KV is injected directly into the attention mechanism. - -### Benefits - -This separation reduces redundant computation in the prefill phase and leads to: - -- Skipping repeated encoding of background content -- Faster attention computation between query tokens and cached memory -- **Lower Time To First Token (TTFT)** latency during generation - -This optimization is especially valuable in: - -- Multi-turn chatbot interactions -- Retrieval-augmented or context-augmented generation (RAG, CAG) -- Assistants operating over fixed documentation or FAQ-style memory - - -### KVCacheMemory Acceleration Evaluation - -To validate the performance impact of KV-based memory injection, we conducted a set of controlled experiments simulating real memory reuse in MemOS. - -#### Experiment Setup - -During typical usage, the `MemScheduler` module continuously tracks interaction patterns and promotes high-frequency, stable plaintext memory into KV format. These KV memories are loaded into GPU memory as activation caches and reused during inference. - -The evaluation compares two memory injection strategies: - -1. **Prompt-based injection**: background memory is prepended as raw text. -2. **KV-cache injection**: memory is injected directly into the model’s attention cache. - -We test these strategies across: - -- **Three context sizes**: short, medium, and long -- **Three query types**: short-form, medium-form, and long-form - -The primary metric is **Time To First Token (TTFT)**, a key latency indicator for responsive generation. - -#### Results - -The following table shows results across three models (Qwen3-8B, Qwen3-32B, Qwen2.5-72B). TTFT under KV-cache injection is consistently lower than prompt-based injection, while the output tokens remain identical across both strategies. - -::note{icon="ri:bnb-fill"} -`Build (s)` refers to the one-time preprocessing cost of converting the memory to KV format, amortized across multiple queries. -:: - -| Model | Ctx | CtxTok | Qry | QryTok | Build (s) | KV TTFT (s) | Dir TTFT (s) | Speedup (%) | -| ----------- | ------ | ------ | ------ | ------ | --------- | ----------- | ------------ | ----------- | -| Qwen3-8B | long | 6064 | long | 952.7 | 0.92 | 0.50 | 2.37 | 79.1 | -| | | | medium | 302.7 | 0.93 | 0.19 | 2.16 | 91.1 | -| | | | short | 167 | 0.93 | 0.12 | 2.04 | 94.2 | -| | medium | 2773 | long | 952.7 | 0.41 | 0.43 | 1.22 | 64.6 | -| | | | medium | 302.7 | 0.41 | 0.16 | 1.08 | 85.1 | -| | | | short | 167 | 0.43 | 0.10 | 0.95 | 89.7 | -| | short | 583 | long | 952.7 | 0.12 | 0.39 | 0.51 | 23.0 | -| | | | medium | 302.7 | 0.12 | 0.14 | 0.32 | 55.6 | -| | | | short | 167 | 0.12 | 0.08 | 0.29 | 71.3 | -| Qwen3-32B | long | 6064 | long | 952.7 | 0.71 | 0.31 | 1.09 | 71.4 | -| | | | medium | 302.7 | 0.71 | 0.15 | 0.98 | 84.3 | -| | | | short | 167 | 0.71 | 0.11 | 0.96 | 88.8 | -| | medium | 2773 | long | 952.7 | 0.31 | 0.24 | 0.56 | 56.9 | -| | | | medium | 302.7 | 0.31 | 0.12 | 0.47 | 75.1 | -| | | | short | 167 | 0.31 | 0.08 | 0.44 | 81.2 | -| | short | 583 | long | 952.7 | 0.09 | 0.20 | 0.24 | 18.6 | -| | | | medium | 302.7 | 0.09 | 0.09 | 0.15 | 39.6 | -| | | | short | 167 | 0.09 | 0.07 | 0.14 | 53.5 | -| Qwen2.5-72B | long | 6064 | long | 952.7 | 1.26 | 0.48 | 2.04 | 76.4 | -| | | | medium | 302.7 | 1.26 | 0.23 | 1.82 | 87.2 | -| | | | short | 167 | 1.27 | 0.15 | 1.79 | 91.4 | -| | medium | 2773 | long | 952.7 | 0.58 | 0.39 | 1.05 | 62.7 | -| | | | medium | 302.7 | 0.58 | 0.18 | 0.89 | 79.2 | -| | | | short | 167 | 0.71 | 0.23 | 0.82 | 71.6 | -| | short | 583 | long | 952.7 | 0.16 | 0.33 | 0.43 | 23.8 | -| | | | medium | 302.7 | 0.16 | 0.15 | 0.27 | 43.2 | -| | | | short | 167 | 0.16 | 0.10 | 0.25 | 60.5 | - - -## KV-cache Memory Structure - -KV-based memory reuse via `KVCacheMemory` offers substantial latency reduction across model sizes and query types, while maintaining identical output. By shifting reusable memory from plaintext prompts into precomputed KV caches, MemOS eliminates redundant context encoding and achieves faster response times—especially beneficial in real-time, memory-augmented LLM applications. - -Each cache is stored as a `KVCacheItem`: - -| Field | Type | Description | -| ------------- | -------------- | ------------------------------------------- | -| `kv_cache_id` | `str` | Unique ID for the cache (UUID) | -| `kv_cache` | `DynamicCache` | The actual key-value cache (transformers) | -| `metadata` | `dict` | Metadata (source, extraction time, etc.) | - - -## API Summary (`KVCacheMemory`) - -### Initialization -```python -KVCacheMemory(config: KVCacheMemoryConfig) -``` - -### Core Methods -| Method | Description | -| ------------------------ | -------------------------------------------------------- | -| `extract(text)` | Extracts a KV cache from input text using the LLM | -| `add(memories)` | Adds one or more `KVCacheItem` to memory | -| `get(memory_id)` | Fetch a single cache by ID | -| `get_by_ids(ids)` | Fetch multiple caches by IDs | -| `get_all()` | Returns all stored caches | -| `get_cache(cache_ids)` | Merge and return a combined cache from multiple IDs | -| `delete(ids)` | Delete caches by IDs | -| `delete_all()` | Delete all caches | -| `dump(dir)` | Serialize all caches to a pickle file in directory | -| `load(dir)` | Load caches from a pickle file in directory | -| `from_textual_memory(mem)` | Convert a `TextualMemoryItem` to a `KVCacheItem` | - - -When calling `dump(dir)`, the system writes to: - -``` -/ -``` - -This file contains a pickled dictionary of all KV caches, which can be reloaded using `load(dir)`. - - -## How to Use - -```python -from memos.configs.memory import KVCacheMemoryConfig -from memos.memories.activation.kv import KVCacheMemory - -config = KVCacheMemoryConfig( - extractor_llm={ - "backend": "huggingface", - "config": {"model_name_or_path": "Qwen/Qwen3-1.7B"} - } -) -mem = KVCacheMemory(config) - -# Extract and add a cache -cache_item = mem.extract("The capital of France is Paris.") -mem.add([cache_item]) - -# Retrieve and merge caches -merged_cache = mem.get_cache([cache_item.kv_cache_id]) - -# Save/load -mem.dump("tmp/act_mem") -mem.load("tmp/act_mem") -``` - - -## Developer Notes - -* Uses HuggingFace `DynamicCache` for efficient key-value storage -* Pickle-based serialization for fast load/save -* All methods are covered by integration tests in `/tests` diff --git a/docs/modules/memories/neo4j_graph_db.md b/docs/modules/memories/neo4j_graph_db.md deleted file mode 100644 index 0b1c629b4..000000000 --- a/docs/modules/memories/neo4j_graph_db.md +++ /dev/null @@ -1,97 +0,0 @@ -# Graph Memory Backend - -This module provides graph-based memory storage and querying for memory-augmented systems such as RAG, cognitive agents, or personal memory assistants. - -It defines a clean abstraction (`BaseGraphDB`) and includes a production-ready implementation using **Neo4j**. - -## Why Graph for Memory? - -Unlike flat vector stores, a graph database allows: - -- Structuring memory into **chains, hierarchies, and causal links** -- Performing **multi-hop reasoning** and **subgraph traversal** -- Supporting memory **deduplication, conflict detection, and scheduling** -- Dynamically evolving a memory graph over time - -This forms the backbone of long-term, explainable, and compositional memory reasoning. - -## Features - -- Unified interface across different graph databases -- Built-in support for Neo4j -- Support for vector-enhanced retrieval (`search_by_embedding`) -- Modular, pluggable, and testable -## Directory Structure - -``` - -src/memos/graph_dbs/ -├── base.py # Abstract interface: BaseGraphDB -├── factory.py # Factory to instantiate GraphDB from config -├── neo4j.py # Neo4jGraphDB: production implementation - -```` - -## How to Use - -```python -from memos.graph_dbs.factory import GraphStoreFactory -from memos.configs.graph_db import GraphDBConfigFactory - -# Step 1: Build factory config -config = GraphDBConfigFactory( - backend="neo4j", - config={ - "uri": "bolt://localhost:7687", - "user": "your_neo4j_user_name", - "password": "your_password", - "db_name": "memory_user1", - "auto_create": True, - "embedding_dimension": 768 - } -) - -# Step 2: Instantiate the graph store -graph = GraphStoreFactory.from_config(config) - -# Step 3: Add memory -graph.add_node( - id="node-001", - content="Today I learned about retrieval-augmented generation.", - metadata={"type": "WorkingMemory", "tags": ["RAG", "AI"], "timestamp": "2025-06-05"} -) -```` - -## Pluggable Design - -### Interface: `BaseGraphDB` - -All implementations must implement: - -* `add_node`, `update_node`, `delete_node` -* `add_edge`, `delete_edge`, `edge_exists` -* `get_node`, `get_path`, `get_subgraph`, `get_context_chain` -* `search_by_embedding`, `get_by_metadata` -* `deduplicate_nodes`, `detect_conflicts`, `merge_nodes` -* `clear`, `export_graph`, `import_graph` - -See src/memos/graph_dbs/base.py for full method docs. - -### Current Backend: - -| Backend | Status | File | -| ------- | ------ | ---------- | -| Neo4j | Stable | `neo4j.py` | - -## Extending - -You can add support for any other graph engine (e.g., **TigerGraph**, **DGraph**, **Weaviate hybrid**) by: - -1. Subclassing `BaseGraphDB` -2. Creating a config dataclass (e.g., `DgraphConfig`) -3. Registering it in: - - * `GraphDBConfigFactory.backend_to_class` - * `GraphStoreFactory.backend_to_class` - -See `src/memos/graph_dbs/neo4j.py` as a reference implementation. diff --git a/docs/modules/memories/parametric_memory.md b/docs/modules/memories/parametric_memory.md deleted file mode 100644 index 0b3df3bba..000000000 --- a/docs/modules/memories/parametric_memory.md +++ /dev/null @@ -1,47 +0,0 @@ -# Parametric Memory *(Coming Soon)* - -::note -**Coming Soon** -This feature is still under active development. Stay tuned for updates! -:: - -`Parametric Memory` is the core **long-term knowledge and capability store** inside MemOS. -Unlike plaintext or activation memories, parametric memory is embedded directly within a model’s weights — encoding deep representations of language structure, world knowledge, and general reasoning abilities. - -In the MemOS architecture, parametric memory does not just refer to static pre-trained weights. It also includes modular weight components such as **LoRA adapters** and plug-in expert modules. These allow you to incrementally expand or specialize your LLM’s capabilities without retraining the entire model. - -For example, you could distill structured or stable knowledge into parametric form, save it as a **capability block**, and dynamically load or unload it during inference. This makes it easy to create “expert sub-models” for tasks like legal reasoning, financial analysis, or domain-specific summarization — all managed by MemOS. - - -## Design Goals - -::list -- **Controllability** — Generate, load, swap, or compose parametric modules - on demand. -- **Plasticity** — Evolve alongside plaintext and activation memories; support knowledge distillation and rollback. -- **Traceability** *(Coming Soon)* — Versioning and governance for parametric blocks. -:: - -## Current Status - -`Parametric Memory` is currently under design and prototyping. -APIs for generating, compressing, and hot-swapping parametric modules will be released in future versions — supporting multi-task, multi-role, and multi-agent architectures. - -Stay tuned! - - -## Related Modules - -While parametric memory is under development, try out these today: -- **[GeneralTextMemory](/docs/modules/memories/general_textual_memory)**: Flexible vector-based semantic storage. -- **[TreeTextMemory](/docs/modules/memories/tree_textual_memory)**: Structured, hierarchical knowledge graphs. -- **[Activation Memory](/docs/modules/memories/kv_cache_memory)**: Efficient runtime state caching. - -## Developer Note - -Parametric Memory will complete MemOS’s vision of a unified **Memory³** architecture: -- **Parametric**: Embedded knowledge -- **Activation**: Ephemeral runtime states -- **Plaintext**: Structured, traceable external memories - -Bringing all three together enables adaptable, evolvable, and explainable intelligent systems. diff --git a/docs/modules/memories/tree_textual_memory.md b/docs/modules/memories/tree_textual_memory.md deleted file mode 100644 index 542603d73..000000000 --- a/docs/modules/memories/tree_textual_memory.md +++ /dev/null @@ -1,281 +0,0 @@ -# TreeTextMemory: Structured Hierarchical Textual Memory - - -Let’s build your first **graph-based, tree-structured memory** in MemOS! - -**TreeTextMemory** helps you organize, link, and retrieve memories with rich context and explainability. - -[Neo4j](/docs/modules/memories/neo4j_graph_db) is the current backend, with support for additional graph stores planned in the future. - - -## What You’ll Learn - -By the end of this guide, you will: -- Extract structured memories from raw text or conversations. -- Store them as **nodes** in a graph database. -- Link memories into **hierarchies** and semantic graphs. -- Search them using **vector similarity + graph traversal**. - -## How It Works - -### Memory Structure - -Every node in your `TreeTextMemory` is a `TextualMemoryItem`: -- `id`: Unique memory ID (auto-generated if omitted). -- `memory`: the main text. -- `metadata`: includes hierarchy info, embeddings, tags, entities, source, and status. - -### Metadata Fields (`TreeNodeTextualMemoryMetadata`) - -| Field | Type | Description | -| --------------- |-------------------------------------------------------| ------------------------------------------ | -| `memory_type` | `"WorkingMemory"`, `"LongTermMemory"`, `"UserMemory"` | Lifecycle category | -| `status` | `"activated"`, `"archived"`, `"deleted"` | Node status | -| `visibility` | `"private"`, `"public"`, `"session"` | Access scope | -| `sources` | `list[str]` | List of sources (e.g. files, URLs) | -| `source` | `"conversation"`, `"retrieved"`, `"web"`, `"file"` | Original source type | -| `confidence` | `float (0-100)` | Certainty score | -| `entities` | `list[str]` | Mentioned entities or concepts | -| `tags` | `list[str]` | Thematic tags | -| `embedding` | `list[float]` | Vector embedding for similarity search | -| `created_at` | `str` | Creation timestamp (ISO 8601) | -| `updated_at` | `str` | Last update timestamp (ISO 8601) | -| `usage` | `list[str]` | Usage history | -| `background` | `str` | Additional context | - - -::note -**Best Practice**
- Use meaningful tags and background — they help organize your graph for -multi-hop reasoning. -:: - -### Core Steps - -When you run this example, your workflow will: - -1. **Extract:** Use an LLM to pull structured memories from raw text. - - -2. **Embed:** Generate vector embeddings for similarity search. - - -3. **Store & Link:** Add nodes to your graph database (Neo4j) with relationships. - - -4. **Search:** Query by vector similarity, then expand results by graph hops. - - -::note -**Hint**
Graph links help retrieve context that pure vector search might miss! -:: - -## API Summary (`TreeTextMemory`) - -### Initialization - -```python -TreeTextMemory(config: TreeTextMemoryConfig) -``` - -### Core Methods - -| Method | Description | -| --------------------------- | ----------------------------------------------------- | -| `add(memories)` | Add one or more memories (items or dicts) | -| `replace_working_memory()` | Replace all WorkingMemory nodes | -| `get_working_memory()` | Get all WorkingMemory nodes | -| `search(query, top_k)` | Retrieve top-k memories using vector + graph search | -| `get(memory_id)` | Fetch single memory by ID | -| `get_by_ids(ids)` | Fetch multiple memories by IDs | -| `get_all()` | Export the full memory graph as dictionary | -| `update(memory_id, new)` | Update a memory by ID | -| `delete(ids)` | Delete memories by IDs | -| `delete_all()` | Delete all memories and relationships | -| `dump(dir)` | Serialize the graph to JSON in directory | -| `load(dir)` | Load graph from saved JSON file | -| `drop(keep_last_n)` | Backup graph & drop database, keeping N backups | - -## File Storage - -When calling `dump(dir)`, the system writes to: - -``` -/ -``` - -This file contains a JSON structure with `nodes` and `edges`. It can be reloaded using `load(dir)`. - ---- - -## Your First TreeTextMemory — Step by Step - -::steps{} - -### Create TreeTextMemory Config -Define: -- your embedder (to create vectors), -- your graph DB backend (Neo4j), -- and your extractor LLM (optional). - -```python -from memos.configs.memory import TreeTextMemoryConfig - -config = TreeTextMemoryConfig.from_json_file("examples/data/config/tree_config.json") -``` - - -### Initialize TreeTextMemory - -```python -from memos.memories.textual.tree import TreeTextMemory - -tree_memory = TreeTextMemory(config) -``` - -### Extract Structured Memories - -Use your extractor to parse conversations, files, or docs into `TextualMemoryItem`s. - -```python -from memos.mem_reader.simple_struct import SimpleStructMemReader - -reader = SimpleStructMemReader.from_json_file("examples/data/config/simple_struct_reader_config.json") - -scene_data = [[ - {"role": "user", "content": "Tell me about your childhood."}, - {"role": "assistant", "content": "I loved playing in the garden with my dog."} -]] - -memories = reader.get_memory(scene_data, type="chat", info={"user_id": "1234"}) -for m_list in memories: - tree_memory.add(m_list) -``` - -### Search Memories - -Try a vector + graph search: -```python -results = tree_memory.search("Talk about the garden", top_k=5) -for i, node in enumerate(results): - print(f"{i}: {node.memory}") -``` - -### Replace Working Memory - -Replace your current `WorkingMemory` nodes with new ones: -```python -tree_memory.replace_working_memory( - [{ - "memory": "User is discussing gardening tips.", - "metadata": {"memory_type": "WorkingMemory"} - }] -) -``` - -### Backup & Restore -Dump your entire tree structure to disk and reload anytime: -```python -tree_memory.dump("tmp/tree_memories") -tree_memory.load("tmp/tree_memories") -``` - -:: - - -### Whole Code - -This combines all the steps above into one end-to-end example — copy & run! - -```python -from memos.configs.embedder import EmbedderConfigFactory -from memos.configs.memory import TreeTextMemoryConfig -from memos.configs.mem_reader import SimpleStructMemReaderConfig -from memos.embedders.factory import EmbedderFactory -from memos.mem_reader.simple_struct import SimpleStructMemReader -from memos.memories.textual.tree import TreeTextMemory - -# Setup Embedder -embedder_config = EmbedderConfigFactory.model_validate({ - "backend": "ollama", - "config": {"model_name_or_path": "nomic-embed-text:latest"} -}) -embedder = EmbedderFactory.from_config(embedder_config) - -# Create TreeTextMemory -tree_config = TreeTextMemoryConfig.from_json_file("examples/data/config/tree_config.json") -my_tree_textual_memory = TreeTextMemory(tree_config) -my_tree_textual_memory.delete_all() - -# Setup Reader -reader_config = SimpleStructMemReaderConfig.from_json_file( - "examples/data/config/simple_struct_reader_config.json" -) -reader = SimpleStructMemReader(reader_config) - -# Extract from conversation -scene_data = [[ - { - "role": "user", - "content": "Tell me about your childhood." - }, - { - "role": "assistant", - "content": "I loved playing in the garden with my dog." - }, -]] -memory = reader.get_memory(scene_data, type="chat", info={"user_id": "1234", "session_id": "2222"}) -for m_list in memory: - my_tree_textual_memory.add(m_list) - -# Search -results = my_tree_textual_memory.search( - "Talk about the user's childhood story?", - top_k=10 -) -for i, r in enumerate(results): - print(f"{i}'th result: {r.memory}") - -# [Optional] Add from documents -doc_paths = ["./text1.txt", "./text2.txt"] -doc_memory = reader.get_memory( - doc_paths, "doc", info={ - "user_id": "your_user_id", - "session_id": "your_session_id", - } -) -for m_list in doc_memory: - my_tree_textual_memory.add(m_list) - -# [Optional] Dump & Drop -my_tree_textual_memory.dump("tmp/my_tree_textual_memory") -my_tree_textual_memory.drop() -``` - -## What Makes TreeTextMemory Different? - -- **Structured Hierarchy:** Organize memories like a mind map — nodes can -have parents, children, and cross-links. -- **Graph-Style Linking:** Beyond pure hierarchy — build multi-hop reasoning - chains. -- **Semantic Search + Graph Expansion:** Combine the best of vectors and - graphs. -- **Explainability:** Trace how memories connect, merge, or evolve over time. - -::note -**Try This**
Add memory nodes from documents or web content. Link them -manually or auto-merge similar nodes! -:: - -## What’s Next? - -- **Know more about [Neo4j](/docs/modules/memories/neo4j_graph_db):** TreeTextMemory is powered by a graph database backend. - Understanding how Neo4j handles nodes, edges, and traversal will help you design more efficient memory hierarchies, multi-hop reasoning, and context linking strategies. -- **Add [Activation Memory](/docs/modules/memories/kv_cache_memory):** - Experiment with - runtime KV-cache for session - state. -- **Explore Graph Reasoning:** Build workflows for multi-hop retrieval and answer synthesis. -- **Go Deep:** Check the [API Reference](/docs/api/info) for advanced usage, or run more examples in `examples/`. - -Now your agent remembers not just facts — but the connections between them! diff --git a/docs/modules/mos/overview.md b/docs/modules/mos/overview.md deleted file mode 100644 index 978dd377a..000000000 --- a/docs/modules/mos/overview.md +++ /dev/null @@ -1,426 +0,0 @@ -# MOS API for MemOS - -The **MOS** is a core component of the MemOS Python package for API, designed to empower large language models (LLMs) with advanced, persistent memory capabilities. MOS acts as an orchestration api layer, managing multiple memory modules (MemCubes) and providing a unified interface for memory-augmented applications. - -## API Summary (`MOS`) - -### Initialization -```python -from memos import MOS -mos = MOS(config: MOSConfig) -``` - -### Core Methods - -| Method | Description | -|--------|-------------| -| `register_mem_cube(mem_cube_name_or_path, mem_cube_id=None, user_id=None)` | Register a new memory cube from a directory or remote repo for a user. | -| `unregister_mem_cube(mem_cube_id, user_id=None)` | Unregister (remove) a memory cube by its ID. | -| `add(messages=None, memory_content=None, doc_path=None, mem_cube_id=None, user_id=None)` | Add new memory (from messages, string, or document) to a cube. | -| `search(query, user_id=None, install_cube_ids=None)` | Search memories across cubes for a query, optionally filtered by cube IDs. | -| `chat(query, user_id=None)` | Chat with the LLM, enhanced by memory retrieval for specified user. | -| `get(mem_cube_id, memory_id, user_id=None)` | Get a specific memory by cube and memory ID for a user. | -| `get_all(mem_cube_id=None, user_id=None)` | Get all memories from a cube (or all cubes for user). | -| `update(mem_cube_id, memory_id, text_memory_item, user_id=None)` | Update a memory in a cube by ID for a user. | -| `delete(mem_cube_id, memory_id, user_id=None)` | Delete a memory from a cube by ID for a user. | -| `delete_all(mem_cube_id=None, user_id=None)` | Delete all memories from a cube for a user. | -| `clear_messages(user_id=None)` | Clear the chat history for the specified user session. | - -### User Management Methods - -| Method | Description | -|--------|-------------| -| `create_user(user_id, role=UserRole.USER, user_name=None)` | Create a new user with specified role and optional name. | -| `list_users()` | List all active users with their information. | -| `create_cube_for_user(cube_name, owner_id, cube_path=None, cube_id=None)` | Create a new cube for a specific user as owner. | -| `get_user_info()` | Get current user information including accessible cubes. | -| `share_cube_with_user(cube_id, target_user_id)` | Share a cube with another user. | - -## Class Overview - -`MOS` manages multiple `MemCube` objects, each representing a user's or session's memory. It provides a unified API for memory operations (add, search, update, delete) and integrates with LLMs to enhance chat with contextual memory retrieval. MOS supports multi-user, multi-session scenarios and is extensible to new memory types and backends. - -## Example Usage - -```python -import uuid - -from memos.configs.mem_os import MOSConfig -from memos.mem_os.main import MOS - - -# init MOS -mos_config = MOSConfig.from_json_file("examples/data/config/simple_memos_config.json") -memory = MOS(mos_config) - -# create user -user_id = str(uuid.uuid4()) -memory.create_user(user_id=user_id) - -# register cube for user -memory.register_mem_cube("examples/data/mem_cube_2", user_id=user_id) - -# add memory for user -memory.add( - messages=[ - {"role": "user", "content": "I like playing football."}, - {"role": "assistant", "content": "I like playing football too."}, - ], - user_id=user_id, -) -# Later, when you want to retrieve memory for user -retrieved_memories = memory.search(query="What do you like?", user_id=user_id) -# output text_memories: I like playing football, act_memories, para_memories -print(f"text_memories: {retrieved_memories['text_mem']}") -``` - -## Core Operations Overview - -MOS exposes several main operations for interacting with memories: - -* **Adding Memories** - Store new information from conversations, documents, or direct content -* **Searching Memories** - Retrieve relevant memories based on semantic queries -* **Chat with Memory** - Enhanced conversations with contextual memory retrieval -* **Memory Management** - Update, delete, and organize existing memories -* **Dumping Memories** - Export memory cubes to persistent storage - -## 1. Adding Memories - -### Overview - -The add operation processes and stores new information through several steps - - -#### Adding from Conversation Messages - -```python -import uuid -from memos.configs.mem_os import MOSConfig -from memos.mem_os.main import MOS - -# Initialize MOS -mos_config = MOSConfig.from_json_file("config/simple_memos_config.json") -memory = MOS(mos_config) - -# Create user -user_id = str(uuid.uuid4()) -memory.create_user(user_id=user_id, user_name="Alice") - -# Register memory cube -memory.register_mem_cube("examples/data/mem_cube_2", user_id=user_id) - -# Add memory from conversation -memory.add( - messages=[ - {"role": "user", "content": "I like playing football and watching movies."}, - {"role": "assistant", "content": "That's great! Football is a wonderful sport and movies can be very entertaining."}, - {"role": "user", "content": "My favorite team is Barcelona."}, - {"role": "assistant", "content": "Barcelona is a fantastic team with a rich history!"} - ], - mem_cube_id="personal_memories", - user_id=user_id -) - -print("Memory added successfully from conversation") -``` - -#### Adding Direct Memory Content - -```python -# Add specific memory content directly -memory.add( - memory_content="User prefers vegetarian food and enjoys cooking Italian cuisine", - mem_cube_id="personal_memories", - user_id=user_id -) - -# Add multiple memory items -memory_items = [ - "User works as a software engineer", - "User lives in San Francisco", - "User enjoys hiking on weekends" -] - -for item in memory_items: - memory.add( - memory_content=item, - mem_cube_id="personal_memories", - user_id=user_id - ) -``` - -#### Adding from Documents - -```python - -# Add from multiple documents -doc_path="./examples/data" -memory.add( - doc_path=doc_path, - mem_cube_id="personal_memories", - user_id=user_id -) -``` - -## 2. Searching Memories - -### Overview - -The search operation retrieves memories through search api: - - -#### Basic Memory Search - -```python -# Search for relevant memories -results = memory.search( - query="What sports do I like?", - user_id=user_id -) - -# Access different types of memories -text_memories = results['text_mem'] -activation_memories = results['act_mem'] -parametric_memories = results['para_mem'] - -print(f"Found {len(text_memories)} text memories") -for memory in text_memories: - print(memory) -``` - -#### Search Across Specific Cubes - -```python -# Search only in specific cubes -results = memory.search( - query="What are my preferences?", - user_id=user_id, - install_cube_ids=["personal_memories", "shared_knowledge"] -) - -# Process results by cube -for cube_memories in results['text_mem']: - print(f"\nCube: {cube_memories['cube_id']}") - for memory in cube_memories['memories']: - print(f"- {memory}") -``` - -## 3. Chat with Memory Enhancement - -### Overview - -The chat operation provides memory-enhanced conversations by: - -1. **Memory Retrieval** - Searches for relevant memories based on the query -2. **Context Building** - Incorporates retrieved memories into the conversation context -3. **Response Generation** - LLM generates responses with memory context - - - -#### Basic Chat - -```python -# Simple chat with memory enhancement -response = memory.chat( - query="What do you remember about my interests?", - user_id=user_id -) -print(f"Assistant: {response}") -``` - -## 4. Memory Retrieval and Management - -### Getting Specific Memory - -#### Code Example - -```python -# Get a specific memory by ID -memory_item = memory.get( - mem_cube_id="personal_memories", - memory_id="memory_123", - user_id=user_id -) - -print(f"Memory ID: {memory_item.memory_id}") -print(f"Content: {memory_item.memory}") -print(f"Created: {memory_item.created_at}") -print(f"Metadata: {memory_item.metadata}") -``` - -### Getting All Memories - - - -#### Code Example - -```python -# Get all memories from a specific cube -all_memories = memory.get_all( - mem_cube_id="personal_memories", - user_id=user_id -) - -# Get all memories from all accessible cubes -all_memories = memory.get_all(user_id=user_id) - -# Access different memory types -for cube_memories in all_memories['text_mem']: - print(f"\nCube: {cube_memories['cube_id']}") - print(f"Total memories: {len(cube_memories['memories'])}") - - for memory in cube_memories['memories']: - print(f"- {memory.memory}") - print(f" ID: {memory.memory_id}") - print(f" Created: {memory.created_at}") -``` - -## 5. Memory Updates and Deletion - -### Updating Memories - - - -#### Code Example - -```python -from memos.memories.textual.item import TextualMemoryItem - -# Create updated memory item -updated_memory = TextualMemoryItem( - memory="User now prefers vegan food and enjoys cooking Mediterranean cuisine", - metadata={ - "updated_at": "2024-01-15", - "update_reason": "Dietary preference change" - } -) - -# Update existing memory -memory.update( - mem_cube_id="personal_memories", - memory_id="memory_123", - text_memory_item=updated_memory, - user_id=user_id -) - -print("Memory updated successfully") -``` - -### Deleting Memories - - -```python -# Delete a specific memory -memory.delete( - mem_cube_id="personal_memories", - memory_id="memory_123", - user_id=user_id -) - -# Delete all memories from a specific cube -memory.delete_all( - mem_cube_id="personal_memories", - user_id=user_id -) - -# Delete all memories for a user (use with caution!) -memory.delete_all(user_id=user_id) -``` - -## 6. Dumping Memories - -### Overview - -The dump operation exports memory cubes to persistent storage, allowing you to: - -* **Backup Memories** - Create persistent copies of memory cubes -* **Transfer Memories** - Move memory cubes between systems -* **Archive Memories** - Store memory cubes for long-term preservation -* **Share Memories** - Export memory cubes for sharing with other users - -#### Basic Memory Dump - -```python -# Dump a specific memory cube to a directory -memory.dump( - dump_dir="./backup/memories", - mem_cube_id="personal_memories", - user_id=user_id -) - -print("Memory cube dumped successfully") -``` - -#### Dump Default Cube - -```python -# Dump the default cube for the user (first accessible cube) -memory.dump( - dump_dir="./backup/default_memories", - user_id=user_id -) - -print("Default memory cube dumped successfully") -``` - -#### Dump All User Cubes - -```python -# Get user info to see all accessible cubes -user_info = memory.get_user_info() - -# Dump each accessible cube -for cube_info in user_info['accessible_cubes']: - if cube_info['is_loaded']: - memory.dump( - dump_dir=f"./backup/{cube_info['cube_name']}", - mem_cube_id=cube_info['cube_id'], - user_id=user_id - ) - print(f"Dumped cube: {cube_info['cube_name']}") -``` - -#### Dump with Custom Directory Structure - -```python -import os -from datetime import datetime - -# Create timestamped backup directory -timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") -backup_dir = f"./backups/{timestamp}" - -# Ensure directory exists -os.makedirs(backup_dir, exist_ok=True) - -# Dump memory cube with organized structure -memory.dump( - dump_dir=backup_dir, - mem_cube_id="personal_memories", - user_id=user_id -) - -print(f"Memory cube dumped to: {backup_dir}") -``` - -## 7. Session Management - -### Clearing Chat History - - -```python -# Clear chat history for a user session -memory.clear_messages(user_id=user_id) - -# Verify chat history is cleared -user_info = memory.get_user_info() -print(f"Chat history cleared for user: {user_info['user_name']}") -``` - -## When to Use MOS - -Use MOS when you need to: - -- Build LLM applications with persistent, user-specific memory. -- Support multi-user, multi-session memory management. -- Integrate memory-augmented retrieval and reasoning into chatbots or agents. diff --git a/docs/modules/mos/users.md b/docs/modules/mos/users.md deleted file mode 100644 index 15d3681ef..000000000 --- a/docs/modules/mos/users.md +++ /dev/null @@ -1,305 +0,0 @@ -# User Management in MemOS - -The **MOS** provides comprehensive user management capabilities to support multi-user, multi-session memory operations. This document details the user management methods available in the MOS. - -## User Roles - -MOS supports four user roles with different permission levels: - -| Role | Description | Permissions | -|------|-------------|-------------| -| `ROOT` | System administrator | Full access to all cubes and users, cannot be deleted | -| `ADMIN` | Administrative user | Can manage users and cubes, access to all cubes | -| `USER` | Standard user | Can create and manage own cubes, access shared cubes | -| `GUEST` | Limited user | Read-only access to shared cubes, cannot create cubes | - -## User Management Methods - -### 1. `create_user` - -Creates a new user in the MOS system. - -**Parameters:** -- `user_id` (str): Unique identifier for the user -- `role` (UserRole, optional): User role. Defaults to `UserRole.USER` -- `user_name` (str, optional): Display name for the user. If not provided, uses `user_id` - -**Returns:** -- `str`: The created user ID - -**Example:** -```python -import uuid -from memos.mem_user.user_manager import UserRole - -# Create a standard user -user_id = str(uuid.uuid4()) -memory.create_user(user_id=user_id, role=UserRole.USER, user_name="John Doe") - -# Create an admin user -admin_id = str(uuid.uuid4()) -memory.create_user(user_id=admin_id, role=UserRole.ADMIN, user_name="Admin User") - -# Create a guest user -guest_id = str(uuid.uuid4()) -memory.create_user(user_id=guest_id, role=UserRole.GUEST, user_name="Guest User") -``` - -**Notes:** -- If a user with the same `user_name` already exists, the method returns the existing user's ID -- The system automatically creates a root user during initialization -- User IDs must be unique across the system - -### 2. `list_users` - -Retrieves information about all active users in the system. - -**Parameters:** -- None - -**Returns:** -- `list`: List of dictionaries containing user information: - - `user_id` (str): Unique user identifier - - `user_name` (str): Display name of the user - - `role` (str): User role (root, admin, user, guest) - - `created_at` (str): ISO format timestamp of user creation - - `is_active` (bool): Whether the user account is active - -**Example:** -```python -# List all users -users = memory.list_users() -for user in users: - print(f"User: {user['user_name']} (ID: {user['user_id']})") - print(f"Role: {user['role']}") - print(f"Active: {user['is_active']}") - print(f"Created: {user['created_at']}") - print("---") -``` - -**Output Example:** -``` -User: root (ID: root) -Role: root -Active: True -Created: 2024-01-15T10:30:00 ---- -User: John Doe (ID: 550e8400-e29b-41d4-a716-446655440000) -Role: user -Active: True -Created: 2024-01-15T11:00:00 ---- -``` - -### 3. `create_cube_for_user` - -Creates a new memory cube for a specific user as the owner. - -**Parameters:** -- `cube_name` (str): Name of the cube -- `owner_id` (str): User ID of the cube owner -- `cube_path` (str, optional): Local file path or remote repository URL for the cube -- `cube_id` (str, optional): Custom cube identifier. If not provided, a UUID is generated - -**Returns:** -- `str`: The created cube ID - -**Example:** -```python -import uuid - -# Create a user first -user_id = str(uuid.uuid4()) -memory.create_user(user_id=user_id, user_name="Alice") - -# Create a cube for the user -cube_id = memory.create_cube_for_user( - cube_name="Alice's Personal Memory", - owner_id=user_id, - cube_path="/path/to/alice/memory", - cube_id="alice_personal_cube" -) - -print(f"Created cube: {cube_id}") -``` - -**Notes:** -- The owner automatically gets full access to the created cube -- The cube owner can share the cube with other users -- If `cube_path` is provided, it can be a local directory path or a remote repository URL -- Custom `cube_id` must be unique across the system - -### 4. `get_user_info` - -Retrieves detailed information about the current user and their accessible cubes. - -**Parameters:** -- None - -**Returns:** -- `dict`: Dictionary containing user information and accessible cubes: - - `user_id` (str): Current user's ID - - `user_name` (str): Current user's display name - - `role` (str): Current user's role - - `created_at` (str): ISO format timestamp of user creation - - `accessible_cubes` (list): List of dictionaries for each accessible cube: - - `cube_id` (str): Cube identifier - - `cube_name` (str): Cube display name - - `cube_path` (str): Cube file path or repository URL - - `owner_id` (str): ID of the cube owner - - `is_loaded` (bool): Whether the cube is currently loaded in memory - -**Example:** -```python -# Get current user information -user_info = memory.get_user_info() - -print(f"Current User: {user_info['user_name']} ({user_info['user_id']})") -print(f"Role: {user_info['role']}") -print(f"Created: {user_info['created_at']}") -print("\nAccessible Cubes:") -for cube in user_info['accessible_cubes']: - print(f"- {cube['cube_name']} (ID: {cube['cube_id']})") - print(f" Owner: {cube['owner_id']}") - print(f" Loaded: {cube['is_loaded']}") - print(f" Path: {cube['cube_path']}") -``` - -**Output Example:** -``` -Current User: Alice (550e8400-e29b-41d4-a716-446655440000) -Role: user -Created: 2024-01-15T11:00:00 - -Accessible Cubes: -- Alice's Personal Memory (ID: alice_personal_cube) - Owner: 550e8400-e29b-41d4-a716-446655440000 - Loaded: True - Path: /path/to/alice/memory -- Shared Project Memory (ID: project_cube) - Owner: bob_user_id - Loaded: False - Path: /path/to/project/memory -``` - -### 5. `share_cube_with_user` - -Shares a memory cube with another user, granting them access to the cube's contents. - -**Parameters:** -- `cube_id` (str): ID of the cube to share -- `target_user_id` (str): ID of the user to share the cube with - -**Returns:** -- `bool`: `True` if sharing was successful, `False` otherwise - -**Example:** -```python -# Share a cube with another user -success = memory.share_cube_with_user( - cube_id="alice_personal_cube", - target_user_id="bob_user_id" -) - -if success: - print("Cube shared successfully") -else: - print("Failed to share cube") -``` - -**Notes:** -- The current user must have access to the cube being shared -- The target user must exist and be active -- Sharing a cube grants the target user read and write access to the cube -- Cube owners can always share their cubes -- Users with access to a cube can share it with other users (if they have appropriate permissions) - -## Complete User Management Workflow - -Here's a complete example demonstrating user management operations: - -```python -import uuid -from memos.configs.mem_os import MOSConfig -from memos.mem_os.main import MOS -from memos.mem_user.user_manager import UserRole - -# Initialize MOS -mos_config = MOSConfig.from_json_file("examples/data/config/simple_memos_config.json") -memory = MOS(mos_config) - -# 1. Create users -alice_id = str(uuid.uuid4()) -bob_id = str(uuid.uuid4()) - -memory.create_user(user_id=alice_id, user_name="Alice", role=UserRole.USER) -memory.create_user(user_id=bob_id, user_name="Bob", role=UserRole.USER) - -# 2. List all users -print("All users:") -users = memory.list_users() -for user in users: - print(f"- {user['user_name']} ({user['role']})") - -# 3. Create cubes for users -alice_cube_id = memory.create_cube_for_user( - cube_name="Alice's Personal Memory", - owner_id=alice_id, - cube_path="/path/to/alice/memory" -) - -bob_cube_id = memory.create_cube_for_user( - cube_name="Bob's Work Memory", - owner_id=bob_id, - cube_path="/path/to/bob/work" -) - -# 4. Share cubes between users -memory.share_cube_with_user(alice_cube_id, bob_id) -memory.share_cube_with_user(bob_cube_id, alice_id) - -# 5. Get user information -alice_info = memory.get_user_info() -print(f"\nAlice's accessible cubes: {len(alice_info['accessible_cubes'])}") - -# 6. Add memory to cubes -memory.add( - messages=[ - {"role": "user", "content": "I like playing football."}, - {"role": "assistant", "content": "That's great! Football is a wonderful sport."} - ], - user_id=alice_id, - mem_cube_id=alice_cube_id -) - -# 7. Search memories -retrieved = memory.search( - query="What does Alice like?", - user_id=alice_id -) -print(f"Retrieved memories: {retrieved['text_mem']}") -``` - -## Error Handling - -The user management methods include comprehensive error handling: - -- **User Validation**: Methods validate that users exist and are active before operations -- **Cube Access Validation**: Ensures users have appropriate access to cubes before operations -- **Duplicate Prevention**: Handles duplicate user names and cube IDs gracefully -- **Permission Checks**: Validates user roles and permissions for sensitive operations - -## Database Persistence - -User management data is persisted in a SQLite database: -- **Location**: Defaults to `~/.memos/memos_users.db` -- **Tables**: `users`, `cubes`, `user_cube_association` -- **Relationships**: Many-to-many relationship between users and cubes -- **Soft Deletes**: Users and cubes are soft-deleted (marked as inactive) rather than permanently removed - -## Security Considerations - -- **Role-based Access Control**: Different user roles have different permissions -- **Cube Ownership**: Cube owners have full control over their cubes -- **Access Validation**: All operations validate user access before execution -- **Root User Protection**: Root user cannot be deleted and has full system access diff --git a/docs/modules/mos/users_configurations.md b/docs/modules/mos/users_configurations.md deleted file mode 100644 index 2b084f503..000000000 --- a/docs/modules/mos/users_configurations.md +++ /dev/null @@ -1,718 +0,0 @@ -# MemOS Configuration Guide - -This document provides a comprehensive overview of all configuration fields and initialization methods across the different components in the MemOS system. - -1. [Configuration Overview](#configuration-overview) -2. [MOS Configuration](#mos-configuration) -3. [LLM Configuration](#llm-configuration) -4. [MemReader Configuration](#memreader-configuration) -5. [MemCube Configuration](#memcube-configuration) -6. [Memory Configuration](#memory-configuration) -7. [Embedder Configuration](#embedder-configuration) -8. [Vector Database Configuration](#vector-database-configuration) -9. [Graph Database Configuration](#graph-database-configuration) -10. [Scheduler Configuration](#scheduler-configuration) -11. [Initialization Methods](#initialization-methods) -12. [Configuration Examples](#configuration-examples) - -## Configuration Overview - -MemOS uses a hierarchical configuration system with factory patterns for different backends. Each component has: -- A base configuration class -- Backend-specific configuration classes -- A factory class that creates the appropriate configuration based on the backend - -## MOS Configuration - -The main MOS configuration that orchestrates all components. - -### MOSConfig Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `user_id` | str | "root" | User ID for the MOS this Config User ID will as default | -| `session_id` | str | auto-generated UUID | Session ID for the MOS | -| `chat_model` | LLMConfigFactory | required | LLM configuration for chat | -| `mem_reader` | MemReaderConfigFactory | required | MemReader configuration | -| `mem_scheduler` | SchedulerFactory | not required | Scheduler configuration | -| `max_turns_window` | int | 15 | Maximum conversation turns to keep | -| `top_k` | int | 5 | Maximum memories to retrieve per query | -| `enable_textual_memory` | bool | True | Enable textual memory | -| `enable_activation_memory` | bool | False | Enable activation memory | -| `enable_parametric_memory` | bool | False | Enable parametric memory | -| `enable_mem_scheduler` | bool | False | Enable scheduler memory | - - -### Example MOS Configuration - -```json -{ - "user_id": "root", - "chat_model": { - "backend": "huggingface", - "config": { - "model_name_or_path": "Qwen/Qwen3-1.7B", - "temperature": 0.1, - "remove_think_prefix": true, - "max_tokens": 4096 - } - }, - "mem_reader": { - "backend": "simple_struct", - "config": { - "llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.8, - "max_tokens": 1024, - "top_p": 0.9, - "top_k": 50 - } - }, - "embedder": { - "backend": "ollama", - "config": { - "model_name_or_path": "nomic-embed-text:latest" - } - }, - "chunker": { - "backend": "sentence", - "config": { - "tokenizer_or_token_counter": "gpt2", - "chunk_size": 512, - "chunk_overlap": 128, - "min_sentences_per_chunk": 1 - } - } - } - }, - "max_turns_window": 20, - "top_k": 5, - "enable_textual_memory": true, - "enable_activation_memory": false, - "enable_parametric_memory": false -} -``` - -## LLM Configuration - -Configuration for different Large Language Model backends. - -### Base LLM Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `model_name_or_path` | str | required | Model name or path | -| `temperature` | float | 0.8 | Temperature for sampling | -| `max_tokens` | int | 1024 | Maximum tokens to generate | -| `top_p` | float | 0.9 | Top-p sampling parameter | -| `top_k` | int | 50 | Top-k sampling parameter | -| `remove_think_prefix` | bool | False | Remove think tags from output | - -### Backend-Specific Fields - -#### OpenAI LLM -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `api_key` | str | required | OpenAI API key | -| `api_base` | str | "https://api.openai.com/v1" | OpenAI API base URL | - -#### Ollama LLM -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `api_base` | str | "http://localhost:11434" | Ollama API base URL | - -#### HuggingFace LLM -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `do_sample` | bool | False | Use sampling vs greedy decoding | -| `add_generation_prompt` | bool | True | Apply generation template | - -### Example LLM Configurations - -```json -// OpenAI -{ - "backend": "openai", - "config": { - "model_name_or_path": "gpt-4o", - "temperature": 0.8, - "max_tokens": 1024, - "top_p": 0.9, - "top_k": 50, - "api_key": "sk-...", - "api_base": "https://api.openai.com/v1" - } -} - -// Ollama -{ - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.8, - "max_tokens": 1024, - "top_p": 0.9, - "top_k": 50, - "api_base": "http://localhost:11434" - } -} - -// HuggingFace -{ - "backend": "huggingface", - "config": { - "model_name_or_path": "Qwen/Qwen3-1.7B", - "temperature": 0.1, - "remove_think_prefix": true, - "max_tokens": 4096, - "do_sample": false, - "add_generation_prompt": true - } -} -``` - -## MemReader Configuration - -Configuration for memory reading components. - -### Base MemReader Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `created_at` | datetime | auto-generated | Creation timestamp | -| `llm` | LLMConfigFactory | required | LLM configuration | -| `embedder` | EmbedderConfigFactory | required | Embedder configuration | -| `chunker` | chunkerConfigFactory | required | chunker configuration | - -### Backend Types - -- `simple_struct`: Structured memory reader - -### Example MemReader Configuration - -```json -{ - "backend": "simple_struct", - "config": { - "llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.0, - "remove_think_prefix": true, - "max_tokens": 8192 - } - }, - "embedder": { - "backend": "ollama", - "config": { - "model_name_or_path": "nomic-embed-text:latest" - } - }, - "chunker": { - "backend": "sentence", - "config": { - "tokenizer_or_token_counter": "gpt2", - "chunk_size": 512, - "chunk_overlap": 128, - "min_sentences_per_chunk": 1 - } - } - } -} -``` - -## MemCube Configuration - -Configuration for memory cube components. - -### GeneralMemCubeConfig Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `user_id` | str | "default_user" | User ID for the MemCube | -| `cube_id` | str | auto-generated UUID | Cube ID for the MemCube | -| `text_mem` | MemoryConfigFactory | required | Textual memory configuration | -| `act_mem` | MemoryConfigFactory | required | Activation memory configuration | -| `para_mem` | MemoryConfigFactory | required | Parametric memory configuration | - -### Allowed Backends - -- **Text Memory**: `naive_text`, `general_text`, `tree_text`, `uninitialized` -- **Activation Memory**: `kv_cache`, `uninitialized` -- **Parametric Memory**: `lora`, `uninitialized` - -### Example MemCube Configuration - -```json -{ - "user_id": "root", - "cube_id": "root/mem_cube_kv_cache", - "text_mem": {}, - "act_mem": { - "backend": "kv_cache", - "config": { - "memory_filename": "activation_memory.pickle", - "extractor_llm": { - "backend": "huggingface", - "config": { - "model_name_or_path": "Qwen/Qwen3-1.7B", - "temperature": 0.8, - "max_tokens": 1024, - "top_p": 0.9, - "top_k": 50, - "add_generation_prompt": true, - "remove_think_prefix": false - } - } - } - }, - "para_mem": { - "backend": "lora", - "config": { - "memory_filename": "parametric_memory.adapter", - "extractor_llm": { - "backend": "huggingface", - "config": { - "model_name_or_path": "Qwen/Qwen3-1.7B", - "temperature": 0.8, - "max_tokens": 1024, - "top_p": 0.9, - "top_k": 50, - "add_generation_prompt": true, - "remove_think_prefix": false - } - } - } - } -} -``` - -## Memory Configuration - -Configuration for different types of memory systems. - -### Base Memory Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `cube_id` | str | None | Unique MemCube identifier is can be cube_name or path as default| - -### Textual Memory Configurations - -#### Base Text Memory -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `memory_filename` | str | "textual_memory.json" | Filename for storing memories | - -#### Naive Text Memory -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `extractor_llm` | LLMConfigFactory | required | LLM for memory extraction | - -#### General Text Memory -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `extractor_llm` | LLMConfigFactory | required | LLM for memory extraction | -| `vector_db` | VectorDBConfigFactory | required | Vector database configuration | -| `embedder` | EmbedderConfigFactory | required | Embedder configuration | - -#### Tree Text Memory -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `extractor_llm` | LLMConfigFactory | required | LLM for memory extraction | -| `dispatcher_llm` | LLMConfigFactory | required | LLM for memory dispatching | -| `embedder` | EmbedderConfigFactory | required | Embedder configuration | -| `graph_db` | GraphDBConfigFactory | required | Graph database configuration | - -### Activation Memory Configurations - -#### Base Activation Memory -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `memory_filename` | str | "activation_memory.pickle" | Filename for storing memories | - -#### KV Cache Memory -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `extractor_llm` | LLMConfigFactory | required | LLM for memory extraction (must be huggingface) | - -### Parametric Memory Configurations - -#### Base Parametric Memory -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `memory_filename` | str | "parametric_memory.adapter" | Filename for storing memories | - -#### LoRA Memory -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `extractor_llm` | LLMConfigFactory | required | LLM for memory extraction (must be huggingface) | - -### Example Memory Configurations - -```json -// Tree Text Memory -{ - "backend": "tree_text", - "config": { - "memory_filename": "tree_memory.json", - "extractor_llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.0, - "remove_think_prefix": true, - "max_tokens": 8192 - } - }, - "dispatcher_llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.0, - "remove_think_prefix": true, - "max_tokens": 8192 - } - }, - "embedder": { - "backend": "ollama", - "config": { - "model_name_or_path": "nomic-embed-text:latest" - } - }, - "graph_db": { - "backend": "neo4j", - "config": { - "uri": "bolt://localhost:7687", - "user": "neo4j", - "password": "12345678", - "db_name": "user08alice", - "auto_create": true, - "embedding_dimension": 768 - } - } - } -} -``` - -## Embedder Configuration - -Configuration for embedding models. - -### Base Embedder Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `model_name_or_path` | str | required | Model name or path | -| `embedding_dims` | int | None | Number of embedding dimensions | - -### Backend-Specific Fields - -#### Ollama Embedder -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `api_base` | str | "http://localhost:11434" | Ollama API base URL | - -#### Sentence Transformer Embedder -No additional fields beyond base configuration. - -### Example Embedder Configurations - -```json -// Ollama Embedder -{ - "backend": "ollama", - "config": { - "model_name_or_path": "nomic-embed-text:latest", - "api_base": "http://localhost:11434" - } -} - -// Sentence Transformer Embedder -{ - "backend": "sentence_transformer", - "config": { - "model_name_or_path": "all-MiniLM-L6-v2", - "embedding_dims": 384 - } -} -``` - -## Vector Database Configuration - -Configuration for vector databases. - -### Base Vector DB Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `collection_name` | str | required | Name of the collection | -| `vector_dimension` | int | None | Dimension of the vectors | -| `distance_metric` | str | None | Distance metric (cosine, euclidean, dot) | - -### Qdrant Vector DB Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `host` | str | None | Qdrant host | -| `port` | int | None | Qdrant port | -| `path` | str | None | Qdrant local path | - -### Example Vector DB Configuration - -```json -{ - "backend": "qdrant", - "config": { - "collection_name": "memories", - "vector_dimension": 768, - "distance_metric": "cosine", - "path": "/path/to/qdrant" - } -} -``` - -## Graph Database Configuration - -Configuration for graph databases. - -### Base Graph DB Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `uri` | str | required | Database URI | -| `user` | str | required | Database username | -| `password` | str | required | Database password | - -### Neo4j Graph DB Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `db_name` | str | required | Target database name | -| `auto_create` | bool | False | Create DB if it doesn't exist | -| `embedding_dimension` | int | 768 | Vector embedding dimension | - -### Example Graph DB Configuration - -```json -{ - "backend": "neo4j", - "config": { - "uri": "bolt://localhost:7687", - "user": "neo4j", - "password": "12345678", - "db_name": "user08alice", - "auto_create": true, - "embedding_dimension": 768 - } -} -``` - -## Scheduler Configuration - -Configuration for memory scheduling systems that manage memory retrieval and activation. - -### Base Scheduler Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `top_k` | int | 10 | Number of top candidates to consider in initial retrieval | -| `top_n` | int | 5 | Number of final results to return after processing | -| `enable_parallel_dispatch` | bool | True | Whether to enable parallel message processing using thread pool | -| `thread_pool_max_workers` | int | 5 | Maximum worker threads in pool (1-20) | -| `consume_interval_seconds` | int | 3 | Interval for consuming messages from queue in seconds (0-60) | - -### General Scheduler Fields - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `act_mem_update_interval` | int | 300 | Interval in seconds for updating activation memory | -| `context_window_size` | int | 5 | Size of the context window for conversation history | -| `activation_mem_size` | int | 5 | Maximum size of the activation memory | -| `act_mem_dump_path` | str | auto-generated | File path for dumping activation memory | - -### Backend Types - -- `general_scheduler`: Advanced scheduler with activation memory management - -### Example Scheduler Configuration - -```json -{ - "backend": "general_scheduler", - "config": { - "top_k": 10, - "top_n": 5, - "act_mem_update_interval": 300, - "context_window_size": 5, - "activation_mem_size": 1000, - "thread_pool_max_workers": 10, - "consume_interval_seconds": 3, - "enable_parallel_dispatch": true - } -} -``` - -## Initialization Methods - -### From JSON File - -```python -from memos.configs.mem_os import MOSConfig - -# Load configuration from JSON file -mos_config = MOSConfig.from_json_file("path/to/config.json") -``` - -### From Dictionary - -```python -from memos.configs.mem_os import MOSConfig - -# Create configuration from dictionary -config_dict = { - "user_id": "root", - "chat_model": { - "backend": "huggingface", - "config": { - "model_name_or_path": "Qwen/Qwen3-1.7B", - "temperature": 0.1 - } - } - # ... other fields -} - -mos_config = MOSConfig(**config_dict) -``` - -### Factory Pattern Usage - -```python -from memos.configs.llm import LLMConfigFactory - -# Create LLM configuration using factory -llm_config = LLMConfigFactory( - backend="huggingface", - config={ - "model_name_or_path": "Qwen/Qwen3-1.7B", - "temperature": 0.1 - } -) -``` - -## Configuration Examples - -### Complete MOS Setup - -```python -from memos.configs.mem_os import MOSConfig -from memos.mem_os.main import MOS - -# Load configuration -mos_config = MOSConfig.from_json_file("examples/data/config/simple_memos_config.json") - -# Initialize MOS -mos = MOS(mos_config) - -# Create user and register cube -user_id = "user_123" -mos.create_user(user_id=user_id) -mos.register_mem_cube("path/to/mem_cube", user_id=user_id) - -# Use MOS -response = mos.chat("Hello, how are you?", user_id=user_id) -``` - -### Tree Memory Configuration - -```python -from memos.configs.memory import MemoryConfigFactory - -# Create tree memory configuration -tree_memory_config = MemoryConfigFactory( - backend="tree_text", - config={ - "memory_filename": "tree_memory.json", - "extractor_llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.0, - "max_tokens": 8192 - } - }, - "dispatcher_llm": { - "backend": "ollama", - "config": { - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.0, - "max_tokens": 8192 - } - }, - "embedder": { - "backend": "ollama", - "config": { - "model_name_or_path": "nomic-embed-text:latest" - } - }, - "graph_db": { - "backend": "neo4j", - "config": { - "uri": "bolt://localhost:7687", - "user": "neo4j", - "password": "password", - "db_name": "memories", - "auto_create": True, - "embedding_dimension": 768 - } - } - } -) -``` - -### Multi-Backend LLM Configuration - -```python -from memos.configs.llm import LLMConfigFactory - -# OpenAI configuration -openai_config = LLMConfigFactory( - backend="openai", - config={ - "model_name_or_path": "gpt-4o", - "temperature": 0.8, - "max_tokens": 1024, - "api_key": "sk-...", - "api_base": "https://api.openai.com/v1" - } -) - -# Ollama configuration -ollama_config = LLMConfigFactory( - backend="ollama", - config={ - "model_name_or_path": "qwen3:0.6b", - "temperature": 0.8, - "max_tokens": 1024, - "api_base": "http://localhost:11434" - } -) - -# HuggingFace configuration -hf_config = LLMConfigFactory( - backend="huggingface", - config={ - "model_name_or_path": "Qwen/Qwen3-1.7B", - "temperature": 0.1, - "remove_think_prefix": True, - "max_tokens": 4096, - "do_sample": False, - "add_generation_prompt": True - } -) -``` - -This comprehensive configuration system allows for flexible and extensible setup of the MemOS system with different backends and components. diff --git a/docs/openapi.json b/docs/openapi.json index 58f4be152..52b6980ac 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -884,7 +884,7 @@ "type": "string", "title": "Session Id", "description": "Session ID for the MOS. This is used to distinguish between different dialogue", - "default": "5cc9f7d7-74c3-4bb3-9b4f-11adf416530b" + "default": "3d88949f-cbe1-4244-a2e1-d346e8b76ca0" }, "chat_model": { "$ref": "#/components/schemas/LLMConfigFactory", @@ -940,6 +940,12 @@ "title": "Enable Mem Scheduler", "description": "Enable memory scheduler for automated memory management", "default": false + }, + "PRO_MODE": { + "type": "boolean", + "title": "Pro Mode", + "description": "Enable PRO mode for complex query decomposition", + "default": false } }, "additionalProperties": false, diff --git a/docs/settings.yml b/docs/settings.yml deleted file mode 100644 index 4f99fbc73..000000000 --- a/docs/settings.yml +++ /dev/null @@ -1,45 +0,0 @@ -nav: - # You can include icons using the syntax `(ri:icon-name)`. - # The frontend will render these as actual icons. - # You can find available icons at https://icones.js.org/ - - - "(ri:home-line) Home": - - "(ri:eye-line) Overview": home/overview.md - - "(ri:information-line) What Is MemOS?": home/memos_intro.md - - "(ri:lightbulb-line) Core Concepts": home/core_concepts.md - - "(ri:building-2-line) Architecture": home/architecture.md - - - "(ri:rocket-line) Getting Started": - - "(ri:play-line) Quick Start": getting_started/quick_start.md - - "(ri:bookmark-line) Your First Memory": getting_started/your_first_memory.md - - "(ri:code-line) Examples": getting_started/examples.md - - - "(ri:cpu-line) MOS": - - "(ri:eye-line) Overview": modules/mos/overview.md - - "(ri:team-line) Users": modules/mos/users.md - - "(ri:settings-3-line) Users Configurations": modules/mos/users_configurations.md - - "(ri:checkbox-multiple-blank-line) MemCube": modules/mem_cube.md - - "(ri:book-open-line) MemReader": modules/mem_reader.md - - "(ri:calendar-line) MemScheduler": modules/mem_scheduler.md - - - "(ri:brain-line) Memories": - - "(ri:database-2-line) KV Cache Memory": modules/memories/kv_cache_memory.md - - "(ri:book-2-line) Plaintext Memory": - - "(ri:file-text-line) General Textual Memory": modules/memories/general_textual_memory.md - - "(ri:tree-line) Tree Textual Memory": modules/memories/tree_textual_memory.md - - "(ri:database-line) Neo4j Graph Database": modules/memories/neo4j_graph_db.md - - "(ri:cpu-line) Parametric Memory": modules/memories/parametric_memory.md - - - "(ri:star-line) Best Practice": - - "(ri:speed-line) Performance Tuning": best_practice/performance_tuning.md - - "(ri:building-3-line) Memory Structure Design": best_practice/memory_structure_design.md - - "(ri:wifi-line) Network Workarounds": best_practice/network_workarounds.md - - "(ri:error-warning-line) Common Errors & Solutions": best_practice/common_errors_solutions.md - - - "(ri:heart-line) Contribution": - - "(ri:eye-line) Overview": contribution/overview.md - - "(ri:tools-line) Setting Up": contribution/setting_up.md - - "(ri:git-branch-line) Development Workflow": contribution/development_workflow.md - - "(ri:git-commit-line) Commit Guidelines": contribution/commit_guidelines.md - - "(ri:article-line) Writing Docs": contribution/writing_docs.md - - "(ri:flask-line) Writing Tests": contribution/writing_tests.md diff --git a/evaluation/.env-example b/evaluation/.env-example new file mode 100644 index 000000000..4cb153b75 --- /dev/null +++ b/evaluation/.env-example @@ -0,0 +1,11 @@ +MODEL="gpt-4o-mini" +OPENAI_API_KEY="sk-***REDACTED***" +OPENAI_BASE_URL="http://***.***.***.***:3000/v1" + +MEM0_API_KEY="m0-***REDACTED***" + +ZEP_API_KEY="z_***REDACTED***" + +CHAT_MODEL="gpt-4o-mini" +CHAT_MODEL_BASE_URL="http://***.***.***.***:3000/v1" +CHAT_MODEL_API_KEY="sk-***REDACTED***" diff --git a/evaluation/README.md b/evaluation/README.md index 02566fa71..39188aea4 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -1,6 +1,6 @@ # Evaluation Memory Framework -This repository provides tools and scripts for evaluating the LoCoMo and LongMemEval dataset using various models and APIs. +This repository provides tools and scripts for evaluating the LoCoMo dataset using various models and APIs. ## Installation @@ -17,67 +17,20 @@ This repository provides tools and scripts for evaluating the LoCoMo and LongMem ## Configuration -Create an `.env` file in the `evaluation/` directory and include the following environment variables: +1. Copy the `.env-example` file to `.env`, and fill in the required environment variables according to your environment and API keys. -```plaintext -OPENAI_API_KEY="sk-xxx" -OPENAI_BASE_URL="your_base_url" +2. Copy the `configs-example/` directory to a new directory named `configs/`, and modify the configuration files inside it as needed. This directory contains model and API-specific settings. -MEM0_API_KEY="your_mem0_api_key" -MEM0_PROJECT_ID="your_mem0_proj_id" -MEM0_ORGANIZATION_ID="your_mem0_org_id" -MODEL="gpt-4o-mini" # or your preferred model -EMBEDDING_MODEL="text-embedding-3-small" # or your preferred embedding model -ZEP_API_KEY="your_zep_api_key" -``` +## Evaluation Scripts -## Dataset -The smaller dataset "LoCoMo" has already been included in the repo to facilitate reproducing. +### LoCoMo Evaluation +⚙️ To evaluate the **LoCoMo** dataset using one of the supported memory frameworks — `memos`, `mem0`, or `zep` — run the following [script](./scripts/run_locomo_eval.sh): -To download the "LongMemEval" dataset, run the following command: ```bash -huggingface-cli download --repo-type dataset --resume-download xiaowu0162/longmemeval --local-dir data/longmemeval +# Edit the configuration in ./scripts/run_locomo_eval.sh +# Specify the model and memory backend you want to use (e.g., mem0, zep, etc.) +./scripts/run_locomo_eval.sh ``` -After downloading, rename the files as follows: -- `longmemeval_m.json` -- `longmemeval_s.json` -- `longmemeval_oracle.json` - -## Evaluation Scripts - -To evaluate the `locomo` dataset, execute the following scripts in order: - -1. **Ingest locomo history into MemOS:** - ```bash - python scripts/locomo/locomo_ingestion.py --lib memos - ``` - -2. **Search Memory for each QA pair in locomo:** - ```bash - python scripts/locomo/locomo_search.py --lib memos - ``` - -3. **Generate responses from OpenAI with provided context:** - ```bash - python scripts/locomo/locomo_responses.py --lib memos - ``` - -4. **Evaluate the generated answers:** - ```bash - python scripts/locomo/locomo_eval.py --lib memos - ``` - -5. **Calculate fine-grained scores for each category:** - ```bash - python scripts/locomo/locomo_metric.py --lib memos - ``` - -## Contributing Guidelines - -1. **Add New Metrics** -When incorporating the evaluation of reflection duration, ensure to record related data in `{lib}_locomo_judged.json`. For additional NLP metrics like BLEU and ROUGE-L score, make adjustments to the `locomo_grader` function in `scripts/locomo/locomo_eval.py`. - -2. **Intermediate Results** -While I have provided intermediate results like `{lib}_locomo_search_results.json`, `{lib}_locomo_responses.json`, and `{lib}_locomo_judged.json` for reproducibility, contributors are encouraged to report final results in the PR description rather than editing these files directly. Any valuable modifications will be combined into an updated version of the evaluation code containing revised intermediate results (at specified intervals). +✍️ For evaluating OpenAI's native memory feature with the LoCoMo dataset, please refer to the detailed guide: [OpenAI Memory on LoCoMo - Evaluation Guide](./scripts/locomo/openai_memory_locomo_eval_guide.md). diff --git a/evaluation/scripts/locomo/locomo_eval.py b/evaluation/scripts/locomo/locomo_eval.py index ac85e087c..b5b478426 100644 --- a/evaluation/scripts/locomo/locomo_eval.py +++ b/evaluation/scripts/locomo/locomo_eval.py @@ -363,8 +363,8 @@ async def limited_task(task): parser.add_argument( "--lib", type=str, - choices=["zep", "memos", "mem0", "mem0_graph", "memos_mos", "langmem", "openai"], - help="Specify the memory framework (zep or memos or mem0 or mem0_graph or memos_mos)", + choices=["zep", "memos", "mem0", "mem0_graph", "langmem", "openai"], + help="Specify the memory framework (zep or memos or mem0 or mem0_graph)", ) parser.add_argument( "--version", diff --git a/evaluation/scripts/locomo/locomo_ingestion.py b/evaluation/scripts/locomo/locomo_ingestion.py index ebd82985d..f3837002d 100644 --- a/evaluation/scripts/locomo/locomo_ingestion.py +++ b/evaluation/scripts/locomo/locomo_ingestion.py @@ -15,10 +15,8 @@ from memos.configs.mem_cube import GeneralMemCubeConfig from memos.configs.mem_os import MOSConfig -from memos.configs.memory import MemoryConfigFactory from memos.mem_cube.general import GeneralMemCube from memos.mem_os.main import MOS -from memos.memories.factory import MemoryFactory custom_instructions = """ @@ -61,29 +59,10 @@ def get_client(frame: str, user_id: str | None = None, version: str = "default") return mem0 elif frame == "memos": - config_path = "configs/text_memos_config.json" - with open(config_path) as f: - config_data = json.load(f) - config_data["config"]["extractor_llm"]["config"]["model_name_or_path"] = os.getenv("MODEL") - config_data["config"]["extractor_llm"]["config"]["api_key"] = os.getenv("OPENAI_API_KEY") - config_data["config"]["extractor_llm"]["config"]["api_base"] = os.getenv("OPENAI_BASE_URL") - config_data["config"]["vector_db"]["config"]["path"] = ( - f"results/locomo/memos-{version}/storages/{user_id}/qdrant" - ) - config_data["config"]["embedder"]["config"]["model_name_or_path"] = os.getenv( - "EMBEDDING_MODEL" - ) - - config = MemoryConfigFactory.model_validate(config_data) - - m = MemoryFactory.from_config(config) - m.load(f"results/locomo/memos-{version}/storages/{user_id}") - return m - - elif frame == "memos_mos": mos_config_path = "configs/mos_memos_config.json" with open(mos_config_path) as f: mos_config_data = json.load(f) + mos_config_data["top_k"] = 20 mos_config = MOSConfig(**mos_config_data) mos = MOS(mos_config) mos.create_user(user_id=user_id) @@ -147,20 +126,6 @@ def ingest_session(client, session, frame, metadata, revised_client=None): ) elif frame == "memos": - for chat in tqdm(session, desc=f"{metadata['session_key']}"): - data = chat.get("speaker") + ": " + chat.get("text") - print({"context": data, "conv_id": conv_id, "created_at": iso_date}) - msg = [{"role": "user", "content": data}] - - try: - memories = client.extract(msg) - except Exception as ex: - print(f"Error extracting message {msg}: {ex}") - memories = [] - print(memories) - client.add(memories) - - elif frame == "memos_mos": messages = [] messages_reverse = [] @@ -276,14 +241,11 @@ def process_user(conv_idx, frame, locomo_df, version, num_workers=1): client.delete_all(user_id=f"{conversation.get('speaker_a')}_{conv_idx}") client.delete_all(user_id=f"{conversation.get('speaker_b')}_{conv_idx}") elif frame == "memos": - conv_id = "locomo_exp_user_" + str(conv_idx) - client = get_client("memos", conv_id, version) - elif frame == "memos_mos": conv_id = "locomo_exp_user_" + str(conv_idx) speaker_a_user_id = conv_id + "_speaker_a" speaker_b_user_id = conv_id + "_speaker_b" - client = get_client("memos_mos", speaker_a_user_id, version) - revised_client = get_client("memos_mos", speaker_b_user_id, version) + client = get_client("memos", speaker_a_user_id, version) + revised_client = get_client("memos", speaker_b_user_id, version) sessions_to_process = [] for session_idx in range(max_session_count): @@ -324,11 +286,6 @@ def process_user(conv_idx, frame, locomo_df, version, num_workers=1): except Exception as e: print(f"Error processing user {conv_idx}, session {session_key}: {e!s}") - if frame == "memos": - conv_id = "locomo_exp_user_" + str(conv_idx) - client.dump(f"results/locomo/memos-{version}/storages/{conv_id}") - del client - end_time = time.time() elapsed_time = round(end_time - start_time, 2) print(f"User {conv_idx} processed successfully in {elapsed_time} seconds") @@ -383,8 +340,8 @@ def main(frame, version="default", num_workers=4): parser.add_argument( "--lib", type=str, - choices=["zep", "memos", "mem0", "mem0_graph", "memos_mos"], - help="Specify the memory framework (zep or memos or mem0 or mem0_graph or memos_mos)", + choices=["zep", "memos", "mem0", "mem0_graph"], + help="Specify the memory framework (zep or memos or mem0 or mem0_graph)", ) parser.add_argument( "--version", diff --git a/evaluation/scripts/locomo/locomo_metric.py b/evaluation/scripts/locomo/locomo_metric.py index caaf601c0..9335ec5ba 100644 --- a/evaluation/scripts/locomo/locomo_metric.py +++ b/evaluation/scripts/locomo/locomo_metric.py @@ -9,8 +9,8 @@ parser.add_argument( "--lib", type=str, - choices=["zep", "memos", "mem0", "mem0_graph", "memos_mos", "langmem", "openai"], - help="Specify the memory framework (zep or memos or mem0 or mem0_graph or memos_mos)", + choices=["zep", "memos", "mem0", "mem0_graph", "langmem", "openai"], + help="Specify the memory framework (zep or memos or mem0 or mem0_graph)", ) parser.add_argument( "--version", diff --git a/evaluation/scripts/locomo/locomo_openai.py b/evaluation/scripts/locomo/locomo_openai.py new file mode 100644 index 000000000..0b6c52922 --- /dev/null +++ b/evaluation/scripts/locomo/locomo_openai.py @@ -0,0 +1,173 @@ +import argparse +import json +import os +import time + +from collections import defaultdict +from multiprocessing.dummy import Pool + +from dotenv import load_dotenv +from openai import OpenAI +from tenacity import retry, stop_after_attempt, wait_random_exponential +from tqdm import tqdm + + +load_dotenv() + +# Retry policy constants +WAIT_MIN = 5 # minimum backoff delay in seconds +WAIT_MAX = 30 # maximum backoff delay in seconds +MAX_TRIES = 10 # maximum number of retry attempts + +WORKERS = 5 # number of parallel worker processes + +ANSWER_PROMPT = """ + You are an intelligent memory assistant tasked with retrieving accurate information from conversation memories. + + # CONTEXT: + You have access to memories from a conversation. These memories contain + timestamped information that may be relevant to answering the question. + + # INSTRUCTIONS: + 1. Carefully analyze all provided memories + 2. Pay special attention to the timestamps to determine the answer + 3. If the question asks about a specific event or fact, look for direct evidence in the memories + 4. If the memories contain contradictory information, prioritize the most recent memory + 5. If there is a question about time references (like "last year", "two months ago", etc.), + calculate the actual date based on the memory timestamp. For example, if a memory from + 4 May 2022 mentions "went to India last year," then the trip occurred in 2021. + 6. Always convert relative time references to specific dates, months, or years. For example, + convert "last year" to "2022" or "two months ago" to "March 2023" based on the memory + timestamp. Ignore the reference while answering the question. + 7. Focus only on the content of the memories. Do not confuse character + names mentioned in memories with the actual users who created those memories. + 8. The answer should be less than 5-6 words. + + # APPROACH (Think step by step): + 1. First, examine all memories that contain information related to the question + 2. Examine the timestamps and content of these memories carefully + 3. Look for explicit mentions of dates, times, locations, or events that answer the question + 4. If the answer requires calculation (e.g., converting relative time references), show your work + 5. Formulate a precise, concise answer based solely on the evidence in the memories + 6. Double-check that your answer directly addresses the question asked + 7. Ensure your final answer is specific and avoids vague time references + + Memories: + + {context} + + Question: {question} + Answer: + """ + + +class OpenAIPredict: + def __init__(self, model="gpt-4o-mini"): + self.model = model + self.openai_client = OpenAI( + api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL") + ) + self.results = defaultdict(list) + + def search_memory(self, idx): + with open(f"openai_memory/{idx}.txt", encoding="utf-8") as file: + memories = file.read().strip().replace("\n\n", "\n") + + return memories, 0 + + def process_question(self, val, idx): + question = val.get("question", "") + answer = val.get("answer", "") + category = val.get("category", -1) + + response, search_memory_time, response_time, context = self.answer_question(idx, question) + + result = { + "question": question, + "answer": response, + "category": category, + "golden_answer": answer, + "search_context": context, + "response_duration_ms": response_time, + "search_duration_ms": search_memory_time, + } + + return result + + @retry( + wait=wait_random_exponential(min=WAIT_MIN, max=WAIT_MAX), + stop=stop_after_attempt(MAX_TRIES), + reraise=True, + ) + def answer_question(self, idx, question): + memories, search_memory_time = self.search_memory(idx) + + answer_prompt = ANSWER_PROMPT.format(context=memories, question=question) + + t1 = time.time() + response = self.openai_client.chat.completions.create( + model=self.model, + messages=[{"role": "system", "content": answer_prompt}], + temperature=0.0, + ) + t2 = time.time() + response_time = (t2 - t1) * 1000 + return response.choices[0].message.content, search_memory_time, response_time, memories + + def process_data_file(self, file_path, output_file_path): + with open(file_path, encoding="utf-8") as f: + data = json.load(f) + + # Function to process each conversation + def process_conversation(item): + idx, conversation = item + results_for_conversation = [] + + # Process each question in the conversation + for question_item in tqdm( + conversation["qa"], desc=f"Processing questions for conversation {idx}", leave=False + ): + if int(question_item.get("category", "")) == 5: + continue + result = self.process_question(question_item, idx) + results_for_conversation.append(result) + + return idx, results_for_conversation + + # Use multiprocessing to process the conversations in parallel + with Pool(processes=WORKERS) as pool: + results = list( + tqdm( + pool.imap(process_conversation, list(enumerate(data))), + total=len(data), + desc="Processing conversations", + ) + ) + + # Reorganize results and store them in self.results + for idx, results_for_conversation in results: + self.results[f"locomo_exp_user_{idx}"] = results_for_conversation + + # Save results to output file + with open(output_file_path, "w") as f: + json.dump(self.results, f, indent=4) + + +def main(version): + os.makedirs(f"results/locomo/openai-{version}/", exist_ok=True) + output_file_path = f"results/locomo/openai-{version}/openai_locomo_responses.json" + openai_predict = OpenAIPredict() + openai_predict.process_data_file("data/locomo/locomo10.json", output_file_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--version", + type=str, + default="default", + help="Version identifier for loading results (e.g., 1010)", + ) + args = parser.parse_args() + version = args.version + main(version) diff --git a/evaluation/scripts/locomo/locomo_responses.py b/evaluation/scripts/locomo/locomo_responses.py index 80f4d893c..5d0374c2b 100644 --- a/evaluation/scripts/locomo/locomo_responses.py +++ b/evaluation/scripts/locomo/locomo_responses.py @@ -24,7 +24,7 @@ async def locomo_response(frame, llm_client, context: str, question: str) -> str context=context, question=question, ) - elif frame == "memos" or frame == "memos_mos": + elif frame == "memos": prompt = ANSWER_PROMPT_MEMOS.format( context=context, question=question, @@ -124,8 +124,8 @@ async def main(frame, version="default"): parser.add_argument( "--lib", type=str, - choices=["zep", "memos", "mem0", "mem0_graph", "memos_mos", "openai"], - help="Specify the memory framework (zep or memos or mem0 or mem0_graph or memos_mos)", + choices=["zep", "memos", "mem0", "mem0_graph", "openai"], + help="Specify the memory framework (zep or memos or mem0 or mem0_graph)", ) parser.add_argument( "--version", diff --git a/evaluation/scripts/locomo/locomo_search.py b/evaluation/scripts/locomo/locomo_search.py index 732b7d8b2..26e7dd467 100644 --- a/evaluation/scripts/locomo/locomo_search.py +++ b/evaluation/scripts/locomo/locomo_search.py @@ -15,12 +15,10 @@ from zep_cloud.client import Zep from memos.configs.mem_os import MOSConfig -from memos.configs.memory import MemoryConfigFactory from memos.mem_os.main import MOS -from memos.memories.factory import MemoryFactory -def get_client(frame: str, user_id: str | None = None, version: str = "default"): +def get_client(frame: str, user_id: str | None = None, version: str = "default", top_k: int = 20): if frame == "zep": zep = Zep(api_key=os.getenv("ZEP_API_KEY"), base_url="https://api.getzep.com/api/v2") return zep @@ -30,29 +28,10 @@ def get_client(frame: str, user_id: str | None = None, version: str = "default") return mem0 elif frame == "memos": - config_path = "configs/text_memos_config.json" - with open(config_path) as f: - config_data = json.load(f) - config_data["config"]["extractor_llm"]["config"]["model_name_or_path"] = os.getenv("MODEL") - config_data["config"]["extractor_llm"]["config"]["api_key"] = os.getenv("OPENAI_API_KEY") - config_data["config"]["extractor_llm"]["config"]["api_base"] = os.getenv("OPENAI_BASE_URL") - config_data["config"]["vector_db"]["config"]["path"] = ( - f"results/locomo/memos-{version}/storages/{user_id}/qdrant" - ) - config_data["config"]["embedder"]["config"]["model_name_or_path"] = os.getenv( - "EMBEDDING_MODEL" - ) - - config = MemoryConfigFactory.model_validate(config_data) - - m = MemoryFactory.from_config(config) - m.load(f"results/locomo/memos-{version}/storages/{user_id}") - return m - - elif frame == "memos_mos": mos_config_path = "configs/mos_memos_config.json" with open(mos_config_path) as f: mos_config_data = json.load(f) + mos_config_data["top_k"] = top_k mos_config = MOSConfig(**mos_config_data) mos = MOS(mos_config) mos.create_user(user_id=user_id) @@ -123,18 +102,6 @@ def get_client(frame: str, user_id: str | None = None, version: str = "default") """ -def memos_search(client, query): - start = time() - search_results = client.search(query, top_k=20) - context = "" - for item in search_results: - item = item.to_dict() - context += f"{item['memory']}\n" - print(query, context) - duration_ms = (time() - start) * 1000 - return context, duration_ms - - def mem0_search(client, query, speaker_a_user_id, speaker_b_user_id, top_k=20): start = time() search_speaker_a_results = client.search( @@ -192,7 +159,7 @@ def mem0_search(client, query, speaker_a_user_id, speaker_b_user_id, top_k=20): return context, duration_ms -def memos_mos_search(client, query, conv_id, speaker_a, speaker_b, reversed_client=None): +def memos_search(client, query, conv_id, speaker_a, speaker_b, reversed_client=None): start = time() search_a_results = client.search( query=query, @@ -339,8 +306,6 @@ def search_query(client, query, metadata, frame, reversed_client=None, top_k=20) if frame == "zep": context, duration_ms = zep_search(client, query, conv_id, top_k) - elif frame == "memos": - context, duration_ms = memos_search(client, query) elif frame == "mem0": context, duration_ms = mem0_search( client, query, speaker_a_user_id, speaker_b_user_id, top_k @@ -349,8 +314,8 @@ def search_query(client, query, metadata, frame, reversed_client=None, top_k=20) context, duration_ms = mem0_graph_search( client, query, speaker_a_user_id, speaker_b_user_id, top_k ) - elif frame == "memos_mos": - context, duration_ms = memos_mos_search( + elif frame == "memos": + context, duration_ms = memos_search( client, query, conv_id, speaker_a, speaker_b, reversed_client ) return context, duration_ms @@ -394,11 +359,11 @@ def process_user(group_idx, locomo_df, frame, version, top_k=20, num_workers=1): } reversed_client = None - if frame == "memos_mos": + if frame == "memos": speaker_a_user_id = conv_id + "_speaker_a" speaker_b_user_id = conv_id + "_speaker_b" - client = get_client(frame, speaker_a_user_id, version) - reversed_client = get_client(frame, speaker_b_user_id, version) + client = get_client(frame, speaker_a_user_id, version, top_k=top_k) + reversed_client = get_client(frame, speaker_b_user_id, version, top_k=top_k) else: client = get_client(frame, conv_id, version) @@ -474,8 +439,8 @@ def main(frame, version="default", num_workers=1, top_k=20): parser.add_argument( "--lib", type=str, - choices=["zep", "memos", "mem0", "mem0_graph", "memos_mos", "langmem"], - help="Specify the memory framework (zep or memos or mem0 or mem0_graph or memos_mos)", + choices=["zep", "memos", "mem0", "mem0_graph", "langmem"], + help="Specify the memory framework (zep or memos or mem0 or mem0_graph)", ) parser.add_argument( "--version", diff --git a/evaluation/scripts/locomo/openai_memory_locomo_eval_guide.md b/evaluation/scripts/locomo/openai_memory_locomo_eval_guide.md new file mode 100644 index 000000000..c7b5a7e3f --- /dev/null +++ b/evaluation/scripts/locomo/openai_memory_locomo_eval_guide.md @@ -0,0 +1,115 @@ +# OpenAI Memory on LoCoMo - Evaluation Guide + +This document outlines the evaluation process for OpenAI's Memory feature using the LoCoMo dataset. + +## 1. Introduction + +Since OpenAI's [Memory feature](https://openai.com/index/memory-and-new-controls-for-chatgpt/) does not have a public API, the evaluation requires a manual process. Dialogues from the LoCoMo dataset are formatted and manually input into the ChatGPT web interface. The resulting memories are then retrieved from the account's memory management page and saved locally. + +To evaluate the quality of these memories, we will use the `gpt-4o-mini` model via API. The model will be asked questions from the LoCoMo dataset, and the full history of memories for the relevant conversation will be provided as context. This simulates a perfect memory retrieval system, giving the model the best possible information to answer the question. + +## 2. Step-by-Step Workflow + +### Step 2.1: Generate Input Context for Memory Extraction + +Run the following Python script to generate the input prompts for each session in each conversation. The script will create a separate `.txt` file for each session, containing the formatted conversation history and the extraction prompt. + +**Script:** +```python +import json +import os + +# Ensure the path to the dataset is correct +LOCOMO_DATA_PATH = "data/locomo/locomo10.json" +SAVE_DIR = "openai_inputs" + +os.makedirs(SAVE_DIR, exist_ok=True) + +TEMPLATE = """Can you please extract relevant information from this conversation and create memory entries for each user mentioned? Please store these memories in your knowledge base in addition to the timestamp provided for future reference and personalized interactions. + +{context} +""" + +with open(LOCOMO_DATA_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + +for conv_idx, item in enumerate(data): + conv = item["conversation"] + + for i in range(1, 35): + session_key = f"session_{i}" + session_dt_key = f"session_{i}_date_time" + if session_key not in conv: + continue + + session = conv[session_key] + session_dt = conv[session_dt_key] + + session_context = "" + for chat in session: + chat_str = f"({session_dt}) {chat['speaker']}: {chat['text']}\n" + session_context += chat_str + + input_string = TEMPLATE.format(context=session_context) + + output_filename = os.path.join(SAVE_DIR, f"{conv_idx}-D{i}.txt") + with open(output_filename, "w", encoding="utf-8") as f: + f.write(input_string) + +print(f"Generated {len(os.listdir(SAVE_DIR))} input files in '{SAVE_DIR}' directory.") +``` + +**Example Input (`0-D9.txt`):** +```plaintext +Can you please extract relevant information from this conversation and create memory entries for each user mentioned? Please store these memories in your knowledge base in addition to the timestamp provided for future reference and personalized interactions. + +(2:31 pm on 17 July, 2023) Melanie: Hey Caroline, hope all's good! I had a quiet weekend after we went camping with my fam two weekends ago. It was great to unplug and hang with the kids. What've you been up to? Anything fun over the weekend? +(2:31 pm on 17 July, 2023) Caroline: Hey Melanie! That sounds great! Last weekend I joined a mentorship program for LGBTQ youth - it's really rewarding to help the community. +... (rest of the conversation) +``` + +### Step 2.2: Extract and Save Memories from ChatGPT + +1. **Enable Memory:** In ChatGPT, go to **Settings -> Personalization** and ensure **Memory** is turned on. +2. **Clear Existing Memories:** Before processing a new conversation, click on **Manage** and **Clear all** to ensure a clean slate. +3. **Input and Verify:** + * Open a new chat. + * Ensure the model is set to **GPT-4o**. + * Copy the content of a generated `.txt` file (e.g., `0-D1.txt`) and paste it into the chat. + * After the model responds, verify that you see the "Memory updated" confirmation. +4. **Save Memories:** + * Click on **Manage** in the memory confirmation to view the newly generated memories. + * Create a new local `.txt` file with the same name as the input file (e.g., `0-D1.txt`). + * Copy each memory entry from ChatGPT and paste it into the new file, with each memory on a new line. +5. **Reset Memories for the Next Conversation:** + * Once all sessions for a conversation are complete, it is essential to **delete all memories to ensure a clean state for the next conversation**. Navigate to Settings -> Personalization -> Manage and click Delete all. + +**Example Memory Output (`0-D9.txt`):** +```plaintext +As of November 17, 2023, Dave has taken up photography and enjoys capturing nature scenes like sunsets, beaches, waves, rocks, and waterfalls. +Dave recently purchased a vintage camera that takes high-quality photos. +Dave discovered a serene park nearby with a peaceful spot featuring a bench under a tree with pink flowers. +As of November 17, 2023, Calvin attended a fancy gala in Boston where he had an inspiring conversation with an artist about music and art. +Calvin finds music a powerful connector and source of creativity. +Calvin took a photo in a Japanese garden that he shared with Dave. +Calvin accepted an invitation to perform at an upcoming show in Boston, expressing excitement about the musical experience. +``` + +### Step 2.3: Consolidate Memories + +The memories are currently saved per session. You need to write a simple script to consolidate all memories belonging to the same conversation into a single file. For example, all memories from `0-D1.txt`, `0-D2.txt`, etc., should be merged into a single `conversation_0_memories.txt`. + + +### Step 2.4: Automated Evaluation + +Once the memories for all conversations have been extracted and saved, you can run the automated [evaluation script](../run_openai_eval.sh). This script will handle the process of generating answers, evaluating them, and calculating metrics. + +```bash +# Edit the configuration in ./scripts/run_openai_eval.sh +./scripts/run_openai_eval.sh +``` + +## 3. Considerations + +- **Account Differences:** Be aware of potential differences between free and Plus accounts, such as context length limitations and the number of memories that can be stored. +- **Granularity:** The evaluation process adds memories at the session level. To ensure high-quality memory extraction, you should follow this same principle. Feeding the entire conversation to the model at once has been shown to be ineffective, often causing it to overlook important details and leading to substantial information loss. diff --git a/evaluation/scripts/run_locomo_eval.sh b/evaluation/scripts/run_locomo_eval.sh new file mode 100755 index 000000000..df1a865f2 --- /dev/null +++ b/evaluation/scripts/run_locomo_eval.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Common parameters for all scripts +LIB="memos" +VERSION="063001" +WORKERS=10 +TOPK=20 + +echo "Running locomo_ingestion.py..." +CUDA_VISIBLE_DEVICES=0 python scripts/locomo/locomo_ingestion.py --lib $LIB --version $VERSION --workers $WORKERS +if [ $? -ne 0 ]; then + echo "Error running locomo_ingestion.py" + exit 1 +fi + +echo "Running locomo_search.py..." +CUDA_VISIBLE_DEVICES=0 python scripts/locomo/locomo_search.py --lib $LIB --version $VERSION --top_k $TOPK --workers $WORKERS +if [ $? -ne 0 ]; then + echo "Error running locomo_search.py" + exit 1 +fi + +echo "Running locomo_responses.py..." +python scripts/locomo/locomo_responses.py --lib $LIB --version $VERSION +if [ $? -ne 0 ]; then + echo "Error running locomo_responses.py." + exit 1 +fi + +echo "Running locomo_eval.py..." +python scripts/locomo/locomo_eval.py --lib $LIB --version $VERSION --workers $WORKERS --num_runs 3 +if [ $? -ne 0 ]; then + echo "Error running locomo_eval.py" + exit 1 +fi + +echo "Running locomo_metric.py..." +python scripts/locomo/locomo_metric.py --lib $LIB --version $VERSION +if [ $? -ne 0 ]; then + echo "Error running locomo_metric.py" + exit 1 +fi + +echo "All scripts completed successfully!" diff --git a/evaluation/scripts/run_openai_eval.sh b/evaluation/scripts/run_openai_eval.sh new file mode 100644 index 000000000..27bb712af --- /dev/null +++ b/evaluation/scripts/run_openai_eval.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Common parameters for all scripts +LIB="openai" +VERSION="063001" +WORKERS=10 +NUM_RUNS=3 + + +echo "Running locomo_openai.py..." +python scripts/locomo/locomo_openai.py --version $VERSION +if [ $? -ne 0 ]; then + echo "Error running locomo_openai.py." + exit 1 +fi + +echo "Running locomo_eval.py..." +python scripts/locomo/locomo_eval.py --lib $LIB --version $VERSION --num_runs $NUM_RUNS +if [ $? -ne 0 ]; then + echo "Error running locomo_eval.py" + exit 1 +fi + +echo "Running locomo_metric.py..." +python scripts/locomo/locomo_metric.py --lib $LIB --version $VERSION +if [ $? -ne 0 ]; then + echo "Error running locomo_metric.py" + exit 1 +fi + +echo "All scripts completed successfully!" diff --git a/examples/basic_modules/tree_textual_memory_reasoner.py b/examples/basic_modules/tree_textual_memory_reasoner.py index be31160c4..369787458 100644 --- a/examples/basic_modules/tree_textual_memory_reasoner.py +++ b/examples/basic_modules/tree_textual_memory_reasoner.py @@ -37,12 +37,18 @@ # Step 1: Prepare a mock ParsedTaskGoal parsed_goal = ParsedTaskGoal( - topic_level=["Multi-UAV Long-Term Coverage"], - concept_level=["Coverage Metrics", "Reward Function Design", "Energy Model"], - fact_level=["CT and FT Definition", "Reward Components", "Energy Cost Components"], + memories=[ + "Multi-UAV Long-Term Coverage", + "Coverage Metrics", + "Reward Function Design", + "Energy Model", + "CT and FT Definition", + "Reward Components", + "Energy Cost Components", + ], + keys=["UAV", "coverage", "energy", "reward"], + tags=[], goal_type="explanation", - graph_suggestion="Use all relevant knowledge from previous paper review", - retrieval_keywords=["UAV", "coverage", "energy", "reward"], ) query = "How can multiple UAVs coordinate to maximize coverage while saving energy?" @@ -85,7 +91,53 @@ sources=["paper://multi-uav-coverage/metrics"], embedding=[0.01] * 768, ), - ) + ), + TextualMemoryItem( + id="c34f5e6b-2d34-4e6f-8c9b-abcdef123456", + memory="The capital of France is Paris, which is known for the Eiffel Tower.", + metadata=TreeNodeTextualMemoryMetadata( + user_id=None, + session_id=None, + status="activated", + type="fact", + memory_time="2024-01-01", + source="file", + confidence=90.0, + entities=["France", "Paris", "Eiffel Tower"], + tags=["geography", "city", "landmark"], + visibility="public", + updated_at="2025-06-11T11:51:24.438001", + memory_type="LongTermMemory", + key="Geography Fact", + value="Paris is the capital of France", + hierarchy_level="concept", + sources=["wikipedia://paris"], + embedding=[0.03] * 768, + ), + ), + TextualMemoryItem( + id="d56a7b8c-3e45-4f7a-9dab-fedcba654321", + memory="Total energy cost is calculated from both mechanical movement and communication transmission.", + metadata=TreeNodeTextualMemoryMetadata( + user_id=None, + session_id=None, + status="activated", + type="fact", + memory_time="2024-01-01", + source="file", + confidence=89.0, + entities=["movement power", "transmission power"], + tags=["energy", "movement", "transmission"], + visibility="public", + updated_at="2025-06-11T11:51:24.438001", + memory_type="LongTermMemory", + key="Energy Cost Components", + value="Includes movement and communication energy", + hierarchy_level="fact", + sources=["paper://multi-uav-coverage/energy-detail"], + embedding=[0.04] * 768, + ), + ), ] # Step 7: Init memory retriever diff --git a/examples/basic_modules/tree_textual_memory_relation_reason_detector.py b/examples/basic_modules/tree_textual_memory_relation_reason_detector.py new file mode 100644 index 000000000..72e4deb60 --- /dev/null +++ b/examples/basic_modules/tree_textual_memory_relation_reason_detector.py @@ -0,0 +1,213 @@ +import uuid + +from memos import log +from memos.configs.embedder import EmbedderConfigFactory +from memos.configs.graph_db import GraphDBConfigFactory +from memos.configs.llm import LLMConfigFactory +from memos.embedders.factory import EmbedderFactory +from memos.graph_dbs.factory import GraphStoreFactory +from memos.graph_dbs.item import GraphDBNode +from memos.llms.factory import LLMFactory +from memos.memories.textual.item import TreeNodeTextualMemoryMetadata +from memos.memories.textual.tree_text_memory.organize.relation_reason_detector import ( + RelationAndReasoningDetector, +) + + +logger = log.get_logger(__name__) + +# === Step 1: Initialize embedder === +embedder_config = EmbedderConfigFactory.model_validate( + { + "backend": "ollama", + "config": { + "model_name_or_path": "nomic-embed-text:latest", + }, + } +) +embedder = EmbedderFactory.from_config(embedder_config) + +# === Step 2: Initialize Neo4j GraphStore === +graph_config = GraphDBConfigFactory( + backend="neo4j", + config={ + "uri": "bolt://localhost:7687", + "user": "neo4j", + "password": "12345678", + "db_name": "lucy4", + "auto_create": True, + }, +) +graph_store = GraphStoreFactory.from_config(graph_config) + +# === Step 3: Initialize LLM for pairwise relation detection === +# Step 1: Load LLM config and instantiate +config = LLMConfigFactory.model_validate( + { + "backend": "ollama", + "config": { + "model_name_or_path": "qwen3:0.6b", + "temperature": 0.7, + "max_tokens": 1024, + }, + } +) +llm = LLMFactory.from_config(config) + +# === Step 4: Create a mock GraphDBNode to test relation detection === + +node_a = GraphDBNode( + id=str(uuid.uuid4()), + memory="Caroline faced increased workload stress during the project deadline.", + metadata=TreeNodeTextualMemoryMetadata( + memory_type="LongTermMemory", + embedding=[0.1] * 10, + key="Workload stress", + tags=["stress", "workload"], + type="fact", + background="Project", + confidence=0.95, + updated_at="2024-06-28T09:00:00Z", + ), +) + +node_b = GraphDBNode( + id=str(uuid.uuid4()), + memory="After joining the support group, Caroline reported improved mental health.", + metadata=TreeNodeTextualMemoryMetadata( + memory_type="LongTermMemory", + embedding=[0.1] * 10, + key="Improved mental health", + tags=["mental health", "support group"], + type="fact", + background="Personal follow-up", + confidence=0.95, + updated_at="2024-07-10T12:00:00Z", + ), +) + +node_c = GraphDBNode( + id=str(uuid.uuid4()), + memory="Peer support groups are effective in reducing stress for LGBTQ individuals.", + metadata=TreeNodeTextualMemoryMetadata( + memory_type="LongTermMemory", + embedding=[0.1] * 10, + key="Support group benefits", + tags=["LGBTQ", "support group", "stress"], + type="fact", + background="General research", + confidence=0.95, + updated_at="2024-06-29T14:00:00Z", + ), +) + +# === D: Work pressure ➜ stress === +node_d = GraphDBNode( + id=str(uuid.uuid4()), + memory="Excessive work pressure increases stress levels among employees.", + metadata=TreeNodeTextualMemoryMetadata( + memory_type="LongTermMemory", + embedding=[0.1] * 10, + key="Work pressure impact", + tags=["stress", "work pressure"], + type="fact", + background="Workplace study", + confidence=0.9, + updated_at="2024-06-15T08:00:00Z", + ), +) + +# === E: Stress ➜ poor sleep === +node_e = GraphDBNode( + id=str(uuid.uuid4()), + memory="High stress levels often result in poor sleep quality.", + metadata=TreeNodeTextualMemoryMetadata( + memory_type="LongTermMemory", + embedding=[0.1] * 10, + key="Stress and sleep", + tags=["stress", "sleep"], + type="fact", + background="Health study", + confidence=0.9, + updated_at="2024-06-18T10:00:00Z", + ), +) + +# === F: Poor sleep ➜ low performance === +node_f = GraphDBNode( + id=str(uuid.uuid4()), + memory="Employees with poor sleep show reduced work performance.", + metadata=TreeNodeTextualMemoryMetadata( + memory_type="LongTermMemory", + embedding=[0.1] * 10, + key="Sleep and performance", + tags=["sleep", "performance"], + type="fact", + background="HR report", + confidence=0.9, + updated_at="2024-06-20T12:00:00Z", + ), +) + +node = GraphDBNode( + id="a88db9ce-3c77-4e83-8d61-aa9ef95c957e", + memory="Caroline joined an LGBTQ support group to cope with work-related stress.", + metadata=TreeNodeTextualMemoryMetadata( + memory_type="LongTermMemory", + embedding=embedder.embed( + ["Caroline joined an LGBTQ support group to cope with work-related stress."] + )[0], + key="Caroline LGBTQ stress", + tags=["LGBTQ", "support group", "stress"], + type="fact", + background="Personal", + confidence=0.95, + updated_at="2024-07-01T10:00:00Z", + ), +) + + +for n in [node, node_a, node_b, node_c, node_d, node_e, node_f]: + graph_store.add_node(n.id, n.memory, n.metadata.dict()) + + +# === Step 5: Initialize RelationDetector and run detection === +relation_detector = RelationAndReasoningDetector( + graph_store=graph_store, llm=llm, embedder=embedder +) + +results = relation_detector.process_node( + node=node, + exclude_ids=[node.id], # Exclude self when searching for neighbors + top_k=5, +) + +# === Step 6: Print detected relations === +print("\n=== Detected Global Relations ===") + + +# === Step 6: Pretty-print detected results === +print("\n=== Detected Pairwise Relations ===") +for rel in results["relations"]: + print(f" Source ID: {rel['source_id']}") + print(f" Target ID: {rel['target_id']}") + print(f" Relation Type: {rel['relation_type']}") + print("------") + +print("\n=== Inferred Nodes ===") +for node in results["inferred_nodes"]: + print(f" New Fact: {node.memory}") + print(f" Sources: {node.metadata.sources}") + print("------") + +print("\n=== Sequence Links (FOLLOWS) ===") +for link in results["sequence_links"]: + print(f" From: {link['from_id']} -> To: {link['to_id']}") + print("------") + +print("\n=== Aggregate Concepts ===") +for agg in results["aggregate_nodes"]: + print(f" Concept Key: {agg.metadata.key}") + print(f" Concept Memory: {agg.memory}") + print(f" Sources: {agg.metadata.sources}") + print("------") diff --git a/examples/core_memories/textual_internet_memoy.py b/examples/core_memories/textual_internet_memoy.py new file mode 100644 index 000000000..21979e890 --- /dev/null +++ b/examples/core_memories/textual_internet_memoy.py @@ -0,0 +1,241 @@ +from memos import log +from memos.configs.embedder import EmbedderConfigFactory +from memos.configs.internet_retriever import InternetRetrieverConfigFactory +from memos.configs.mem_reader import SimpleStructMemReaderConfig +from memos.configs.memory import TreeTextMemoryConfig +from memos.embedders.factory import EmbedderFactory +from memos.mem_reader.simple_struct import SimpleStructMemReader +from memos.memories.textual.tree import TreeTextMemory + + +logger = log.get_logger(__name__) + + +embedder_config = EmbedderConfigFactory.model_validate( + { + "backend": "ollama", + "config": { + "model_name_or_path": "nomic-embed-text:latest", + }, + } +) +embedder = EmbedderFactory.from_config(embedder_config) + + +def embed_memory_item(memory: str) -> list[float]: + return embedder.embed([memory])[0] + + +# google or xinyu pelase change tree_config_with_internet api key or token or url +tree_config = TreeTextMemoryConfig.from_json_file("examples/data/config/tree_config.json") +interconfig = InternetRetrieverConfigFactory( + backend="xinyu", + config={ + "api_key": "google or xinyu token", + "search_engine_id": "google engine id or xinyu url", + "max_results": 15, + "num_per_request": 10, + }, +) +tree_config.internet_retriever = interconfig + +my_tree_textual_memory = TreeTextMemory(tree_config) +my_tree_textual_memory.delete_all() + +# Create a memory reader instance +reader_config = SimpleStructMemReaderConfig.from_json_file( + "examples/data/config/simple_struct_reader_config.json" +) +reader = SimpleStructMemReader(reader_config) + +scene_data = [ + [ + {"role": "user", "chat_time": "3 May 2025", "content": "I’m feeling a bit down today."}, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "I’m sorry to hear that. Do you want to talk about what’s been going on?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "It’s just been a tough couple of days, you know? Everything feels a bit overwhelming, and I just can’t seem to shake it off.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "It sounds like you're going through a lot right now. Sometimes it helps to talk things out. Is there something specific that's been weighing on you, or is it more of a general feeling?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "It’s a mix, I guess. Work’s been really stressful, and on top of that, I’ve been feeling kinda disconnected from the people around me.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "That can be really draining, especially when you’re feeling isolated on top of the stress. Do you think there’s something from your past that’s contributing to how you’re feeling now? Sometimes our emotions are tied to older experiences.", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "Yeah, now that you mention it… I’ve been thinking a lot about my childhood lately. I didn’t have the easiest upbringing. My parents were always busy, and I often felt like I had to figure things out on my own. I remember one winter, I got really sick with a high fever. My parents were both working, and I didn’t want to bother them. I just lay in bed, crying by myself, not knowing what to do.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "That must have been very lonely. Growing up without that support can really make you feel like you have to handle everything on your own. How do you think that experience has affected you now?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "Yeah, it was really lonely. I remember spending a lot of time doing things on my own, like drawing, because it was my only way to escape. I used to draw animals, and I created my own little world on paper. It was peaceful in a way, just me and my imagination, with no outside distractions.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "Drawing sounds like a beautiful way to express yourself and find some peace in that solitude. It must have been a very quiet and private world for you. Do you think that sense of loneliness from back then has shaped how you connect with others today?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "I think it has. I’ve always found it hard to let others in. I feel like I have to handle everything on my own. If I rely on someone else, I feel vulnerable, and I start to feel that loneliness again.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "It sounds like this was a protective mechanism that helped you cope with your feelings of loneliness back then. But as you’ve grown, it seems like this pattern might not be serving you as well anymore. Have you thought about trying to break this cycle, or finding new ways to connect with others?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "I’ve thought about it, though it’s hard. But I also know I can’t keep going like this forever. Maybe it’s time to seek some help.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "It’s a big step, but a positive one. Exploring your emotions and past experiences could really help you understand how they’ve shaped your present. Therapy could be a great place to start, as it would help you connect with both yourself and others. Would you be open to trying it?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "I’m open to trying. Thank you for encouraging me. It feels like it might be a good starting point for me.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "I’m really glad to hear that. Taking that first step is always difficult, but you’ve made an important decision. If you need help finding the right therapist, or just want to talk, I’m here for you.", + }, + ], + [ + { + "role": "user", + "chat_time": "3 May 2025", + "content": "This week I’ve been feeling a bit off, especially when playing tennis. My body just doesn’t feel right.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "It sounds like you've been having some physical discomfort lately. Where specifically are you feeling unwell? Is there something particular about tennis that's been difficult?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "I just feel a bit fatigued, and every time I play tennis, my movements feel off. I used to play pretty well, but lately, I’ve been feeling weak and my movements aren’t as coordinated as they used to be.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "Physical discomfort can definitely affect performance, especially in a sport like tennis that requires quick reactions and coordination. Have you noticed anything specific that might be causing these changes? Could it be overtraining, or is there another physical issue making you feel off?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "I think it might be due to stress and lack of sleep recently. I just feel drained all the time. Plus, I’ve been frustrated with my tennis performance, which makes me feel even worse, like I’m stuck in a vicious cycle.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "Stress and poor sleep quality can definitely affect your physical performance, especially in sports. When you’re also feeling down about your performance, it’s easy to fall into a negative cycle. Have you tried anything to relieve some of the stress, or to improve your sleep?", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "I’ve tried some relaxation techniques, like deep breathing and simple meditation, but it doesn’t seem to help much. I still can’t focus during tennis, and I feel like my mind is distracted by other things.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "Relaxation techniques like deep breathing and meditation can help reduce stress, but sometimes we need a more structured approach to improve both our physical and mental state. For example, you might want to focus on specific aspects of your tennis training, or adjust your rest and recovery time. Have you thought about setting smaller goals for yourself? Like focusing on a specific tennis move each day, rather than expecting perfection right away? That might help you gradually regain confidence.", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "That’s a good idea. I think I’ve been expecting too much from myself, which just makes me more anxious. Maybe I can start with small practice sessions and gradually find my rhythm again.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "Exactly! Setting small and clear goals will help reduce pressure and give you a sense of accomplishment. And don’t forget that rest is just as important—taking time to recover will help you feel better physically and mentally. Both physical and mental recovery are key to improving your performance and health.", + }, + { + "role": "user", + "chat_time": "3 May 2025", + "content": "Thanks, I feel like I need to rethink a few things. Maybe I really need to be kinder to myself and take things step by step.", + }, + { + "role": "assistant", + "chat_time": "3 May 2025", + "content": "You’re on the right track. Rethinking things and giving yourself space and patience is so important. You’ve already taken the first step, now just keep moving forward, one step at a time. If you need anything, I’m always here to help.", + }, + ], +] + +# Acquiring memories +memory = reader.get_memory(scene_data, type="chat", info={"user_id": "1234", "session_id": "2222"}) + +for m_list in memory: + my_tree_textual_memory.add(m_list) + +results = my_tree_textual_memory.search( + "Talk about the user's childhood story?", + top_k=10, + info={"query": "Talk about the user's childhood story?", "user_id": "111", "session": "2234"}, +) +for i, r in enumerate(results): + r = r.to_dict() + print(f"{i}'th similar result is: " + str(r["memory"])) +print(f"Successfully search {len(results)} memories") + +# find related nodes +related_nodes = my_tree_textual_memory.get_relevant_subgraph("Painting") + +# get current memory_size +print(f"Current Memory Size is {my_tree_textual_memory.get_current_memory_size()}") + +logger.info("Start doc search example...") +# Processing Documents +doc_paths = [ + "./text1.txt", + "./text2.txt", +] +# Acquiring memories from documents +doc_memory = reader.get_memory(doc_paths, "doc", info={"user_id": "1111", "session_id": "2222"}) + +for m_list in doc_memory: + my_tree_textual_memory.add(m_list) + +results = my_tree_textual_memory.search( + "Tell me about what memos consist of?", + top_k=30, + info={"query": "Tell me about what memos consist of?", "user_id": "111", "session": "2234"}, +) +for i, r in enumerate(results): + r = r.to_dict() + print(f"{i}'th similar result is: " + str(r["memory"])) +print(f"Successfully search {len(results)} memories") + + +# my_tree_textual_memory.dump +my_tree_textual_memory.dump("tmp/my_tree_textual_memory") +my_tree_textual_memory.drop() diff --git a/examples/core_memories/tree_textual_memory.py b/examples/core_memories/tree_textual_memory.py index 2b3bbdf29..9a4035b87 100644 --- a/examples/core_memories/tree_textual_memory.py +++ b/examples/core_memories/tree_textual_memory.py @@ -183,6 +183,7 @@ def embed_memory_item(memory: str) -> list[float]: for m_list in memory: my_tree_textual_memory.add(m_list) + my_tree_textual_memory.memory_manager.wait_reorganizer() results = my_tree_textual_memory.search( "Talk about the user's childhood story?", @@ -211,6 +212,7 @@ def embed_memory_item(memory: str) -> list[float]: for m_list in doc_memory: my_tree_textual_memory.add(m_list) + my_tree_textual_memory.memory_manager.wait_reorganizer() results = my_tree_textual_memory.search( "Tell me about what memos consist of?", @@ -222,6 +224,9 @@ def embed_memory_item(memory: str) -> list[float]: print(f"{i}'th similar result is: " + str(r["memory"])) print(f"Successfully search {len(results)} memories") +# close the synchronous thread in memory manager +my_tree_textual_memory.memory_manager.close() + # my_tree_textual_memory.dump my_tree_textual_memory.dump("tmp/my_tree_textual_memory") diff --git a/examples/data/config/tree_config.json b/examples/data/config/tree_config.json index f41237555..bcb07b3a4 100644 --- a/examples/data/config/tree_config.json +++ b/examples/data/config/tree_config.json @@ -33,5 +33,6 @@ "auto_create": true, "embedding_dimension": 768 } - } + }, + "reorganize": false } diff --git a/examples/mem_os/cot_usage_example.py b/examples/mem_os/cot_usage_example.py new file mode 100644 index 000000000..e3d10fa75 --- /dev/null +++ b/examples/mem_os/cot_usage_example.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +""" +MemOS CoT (Chain of Thought) Usage Example +This example demonstrates how to use CoT functionality with tree textual memory. +It shows how to: +1. Decompose complex questions into sub-questions +2. Get answers for sub-questions using tree_textual_memory +3. Use JSON configuration files with environment variable overrides +""" + +import json +import os + +# Load environment variables +from dotenv import load_dotenv + +from memos.configs.llm import LLMConfigFactory +from memos.configs.mem_reader import SimpleStructMemReaderConfig +from memos.configs.memory import TreeTextMemoryConfig +from memos.mem_os.main import MOS +from memos.mem_reader.simple_struct import SimpleStructMemReader +from memos.memories.textual.tree import TreeTextMemory + + +load_dotenv() + + +def load_and_modify_config(config_path: str) -> dict: + """Load JSON config and modify it with environment variables.""" + with open(config_path) as f: + config = json.load(f) + + # Get environment variables + openai_api_key = os.getenv("OPENAI_API_KEY") + openai_base_url = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1") + + # Modify config to use ollama for embedder and gpt-4o-mini for LLMs + if "embedder" in config: + config["embedder"] = { + "backend": "ollama", + "config": {"model_name_or_path": "nomic-embed-text:latest"}, + } + + # Modify LLM configs to use gpt-4o-mini + if "llm" in config: + config["llm"] = { + "backend": "openai", + "config": { + "model_name_or_path": "gpt-4o-mini", + "api_key": openai_api_key, + "api_base": openai_base_url, + "temperature": 0.5, + "remove_think_prefix": True, + "max_tokens": 8192, + }, + } + + if "extractor_llm" in config: + config["extractor_llm"] = { + "backend": "openai", + "config": { + "model_name_or_path": "gpt-4o-mini", + "api_key": openai_api_key, + "api_base": openai_base_url, + "temperature": 0.5, + "remove_think_prefix": True, + "max_tokens": 8192, + }, + } + + if "dispatcher_llm" in config: + config["dispatcher_llm"] = { + "backend": "openai", + "config": { + "model_name_or_path": "gpt-4o-mini", + "api_key": openai_api_key, + "api_base": openai_base_url, + "temperature": 0.5, + "remove_think_prefix": True, + "max_tokens": 8192, + }, + } + + # Modify graph_db config if present + if "graph_db" in config: + neo4j_uri = os.getenv("NEO4J_URI", "bolt://localhost:7687") + neo4j_user = os.getenv("NEO4J_USER", "neo4j") + neo4j_password = os.getenv("NEO4J_PASSWORD", "12345678") + + config["graph_db"] = { + "backend": "neo4j", + "config": { + "uri": neo4j_uri, + "user": neo4j_user, + "password": neo4j_password, + "db_name": "testlcy", + "auto_create": True, + "embedding_dimension": 768, + }, + } + + return config + + +def setup_llm_config(): + """Setup LLM configuration for CoT operations.""" + # Get environment variables + openai_api_key = os.getenv("OPENAI_API_KEY") + openai_base_url = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1") + # Use ollama with gpt-4o-mini model + return LLMConfigFactory( + backend="openai", + config={ + "model_name_or_path": "gpt-4o-mini", + "api_key": openai_api_key, + "api_base": openai_base_url, + "temperature": 0.5, + "remove_think_prefix": True, + "max_tokens": 8192, + }, + ) + + +def create_tree_memory(): + """Create a tree textual memory with sample data.""" + print("Creating tree textual memory...") + + # Load and modify configurations + tree_config_dict = load_and_modify_config("examples/data/config/tree_config.json") + reader_config_dict = load_and_modify_config( + "examples/data/config/simple_struct_reader_config.json" + ) + + # Create config objects + tree_config = TreeTextMemoryConfig.model_validate(tree_config_dict) + reader_config = SimpleStructMemReaderConfig.model_validate(reader_config_dict) + + # Create tree memory + tree_memory = TreeTextMemory(tree_config) + tree_memory.delete_all() # Clear existing data + + # Create memory reader + reader = SimpleStructMemReader(reader_config) + + # Sample conversation data + sample_conversations = [ + [ + {"role": "user", "content": "Tell me about China and its capital."}, + { + "role": "assistant", + "content": "China is a country in East Asia. Beijing is its capital city.", + }, + {"role": "user", "content": "Who is Lang Ping?"}, + { + "role": "assistant", + "content": "Lang Ping is a famous Chinese volleyball coach and former player.", + }, + {"role": "user", "content": "What about Madagascar?"}, + { + "role": "assistant", + "content": "Madagascar is an island country in the Indian Ocean. It's known for its unique wildlife.", + }, + {"role": "user", "content": "Tell me about trade between China and Madagascar."}, + { + "role": "assistant", + "content": "China and Madagascar have developed trade relations, particularly in agriculture and mining.", + }, + {"role": "user", "content": "What about the essential oil industry in Madagascar?"}, + { + "role": "assistant", + "content": "The essential oil industry is growing in Madagascar, especially on Nosy Be Island where vanilla and ylang-ylang are produced.", + }, + ] + ] + + # Acquire memories using the reader + memories = reader.get_memory( + sample_conversations, type="chat", info={"user_id": "cot_user", "session_id": "cot_session"} + ) + + # Add memories to tree structure + for memory_list in memories: + tree_memory.add(memory_list) + + print("✓ Added sample conversations to tree memory") + return tree_memory + + +def cot_decompose(): + """Test the cot_decompose functionality.""" + print("\n=== Testing CoT Decomposition ===") + + # Setup LLM config + llm_config = setup_llm_config() + + # Test questions + test_questions = [ + "Who is the current head coach of the gymnastics team in the capital of the country that Lang Ping represents?", + "What is the weather like today?", + "How did the trade relationship between Madagascar and China develop, and how does this relationship affect the market expansion of the essential oil industry on Nosy Be Island?", + ] + + for i, question in enumerate(test_questions, 1): + print(f"\nTest {i}: {question}") + result = MOS.cot_decompose(question, llm_config) + print(f"✓ Decomposition result: {result}") + + if result.get("is_complex", False): + sub_questions = result.get("sub_questions", []) + print(f"✓ Found {len(sub_questions)} sub-questions:") + for j, sub_q in enumerate(sub_questions, 1): + print(f" {j}. {sub_q}") + else: + print("✓ Question is not complex, no decomposition needed.") + + return llm_config + + +def get_sub_answers_with_tree_memory(): + """Test get_sub_answers with tree textual memory.""" + print("\n=== Testing get_sub_answers with Tree Textual Memory ===") + + # Setup + llm_config = setup_llm_config() + tree_memory = create_tree_memory() + + # Test sub-questions + sub_questions = [ + "Which country does Lang Ping represent in volleyball?", + "What is the capital of this country?", + "Who is the current head coach of the gymnastics team in this capital?", + ] + + print("Sub-questions to answer:") + for i, q in enumerate(sub_questions, 1): + print(f" {i}. {q}") + print("\nGenerating answers using tree memory and LLM...") + sub_questions, sub_answers = MOS.get_sub_answers( + sub_questions=sub_questions, search_engine=tree_memory, llm_config=llm_config, top_k=3 + ) + + print("✓ Generated answers:") + for i, (question, answer) in enumerate(zip(sub_questions, sub_answers, strict=False), 1): + print(f"\n Sub-question {i}: {question}") + print(f" Answer: {answer}") + + +def complete_cot_workflow(): + """Test the complete CoT workflow from decomposition to final synthesis.""" + print("\n=== Testing Complete CoT Workflow ===") + + # Setup + llm_config = setup_llm_config() + tree_memory = create_tree_memory() + + # Complex question + complex_question = "How did the trade relationship between Madagascar and China develop, and how does this relationship affect the market expansion of the essential oil industry on Nosy Be Island?" + + print(f"Original question: {complex_question}") + + try: + # Step 1: Decompose the question + print("\n1. Decomposing question...") + decomposition_result = MOS.cot_decompose(complex_question, llm_config) + print(f"✓ Decomposition result: {decomposition_result}") + + if not decomposition_result.get("is_complex", False): + print("Question is not complex, no decomposition needed.") + return + + sub_questions = decomposition_result.get("sub_questions", []) + print(f"✓ Found {len(sub_questions)} sub-questions:") + for i, q in enumerate(sub_questions, 1): + print(f" {i}. {q}") + + # Step 2: Get answers for sub-questions + print("\n2. Getting answers for sub-questions...") + sub_questions, sub_answers = MOS.get_sub_answers( + sub_questions=sub_questions, search_engine=tree_memory, llm_config=llm_config, top_k=3 + ) + + print("✓ Generated answers:") + for i, (question, answer) in enumerate(zip(sub_questions, sub_answers, strict=False), 1): + print(f"\n Sub-question {i}: {question}") + print(f" Answer: {answer}") + + # Step 3: Generate final synthesis + print("\n3. Generating final synthesis...") + # Build the sub-questions and answers text + qa_text = "" + for i, (question, answer) in enumerate(zip(sub_questions, sub_answers, strict=False), 1): + qa_text += f"Q{i}: {question}\nA{i}: {answer}\n\n" + + synthesis_prompt = f"""You are an expert at synthesizing information from multiple sources to provide comprehensive answers. + +Sub-questions and their answers: +{qa_text} +Please synthesize these answers into a comprehensive response that: +1. Addresses the original question completely +2. Integrates information from all sub-questions +3. Provides clear reasoning and connections +4. Is well-structured and easy to understand + +Original question: {complex_question} + +Your response:""" + + # Generate final answer + from memos.llms.factory import LLMFactory + + llm = LLMFactory.from_config(llm_config) + messages = [ + { + "role": "system", + "content": "You are a helpful assistant that synthesizes information from multiple sources.", + }, + {"role": "user", "content": synthesis_prompt}, + ] + + final_answer = llm.generate(messages) + print(f"\n✓ Final synthesized answer:\n{final_answer}") + + except Exception as e: + print(f"✗ Error in complete workflow: {e}") + + +def main(): + """Main function to run the CoT example.""" + print("MemOS CoT (Chain of Thought) Usage Example") + print("=" * 60) + + # Run the examples + cot_decompose() + get_sub_answers_with_tree_memory() + complete_cot_workflow() + + print("\n" + "=" * 60) + print("✓ All examples completed successfully!") + + +if __name__ == "__main__": + main() diff --git a/examples/mem_os/simple_openapi_memos.py b/examples/mem_os/simple_openapi_memos.py index e55a250d9..a9faa3dc8 100644 --- a/examples/mem_os/simple_openapi_memos.py +++ b/examples/mem_os/simple_openapi_memos.py @@ -1,14 +1,19 @@ +import os import time import uuid from datetime import datetime +from dotenv import load_dotenv + from memos.configs.mem_cube import GeneralMemCubeConfig from memos.configs.mem_os import MOSConfig from memos.mem_cube.general import GeneralMemCube from memos.mem_os.main import MOS +load_dotenv() + # 1. Create MOS Config and set openai config print(f"🚀 [{datetime.now().strftime('%H:%M:%S')}] Starting to create MOS configuration...") start_time = time.time() @@ -18,17 +23,17 @@ # 1.1 Set openai config openapi_config = { - "model_name_or_path": "gpt-4o", + "model_name_or_path": "gpt-4o-mini", "temperature": 0.8, "max_tokens": 1024, "top_p": 0.9, "top_k": 50, "remove_think_prefix": True, - "api_key": "sk-xxxxxx", - "api_base": "https://api.openai.com/v1", + "api_key": os.getenv("OPENAI_API_KEY", "sk-xxxxx"), + "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), } # 1.2 Set neo4j config -neo4j_uri = "bolt://localhost:7687" +neo4j_uri = os.getenv("NEO4J_URI", "bolt://localhost:7687") # 1.3 Create MOS Config config = { @@ -69,6 +74,7 @@ } mos_config = MOSConfig(**config) +# you can set PRO_MODE to True to enable CoT enhancement mos_config.PRO_MODE = True mos = MOS(mos_config) print( diff --git a/examples/mem_os/simple_vllm_memos.py b/examples/mem_os/simple_vllm_memos.py new file mode 100644 index 000000000..e073526d4 --- /dev/null +++ b/examples/mem_os/simple_vllm_memos.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +Simple example demonstrating how to use VLLMLLM with existing vLLM server. +Requires a vLLM server to be running on localhost:8088. +""" + +import asyncio +import sys + +from memos.configs.llm import VLLMLLMConfig +from memos.llms.vllm import VLLMLLM +from memos.types import MessageList + + +def main(): + """Main function demonstrating VLLMLLM usage.""" + + # Configuration for connecting to existing vLLM server + config = VLLMLLMConfig( + model_name_or_path="Qwen/Qwen3-1.7B", # Model name (for reference) + api_key="", # Not needed for local server + api_base="http://localhost:8088", # vLLM server address + temperature=0.7, + max_tokens=512, + top_p=0.9, + top_k=50, + model_schema="memos.configs.llm.VLLMLLMConfig", + ) + + # Initialize VLLM LLM + print("Initializing VLLM LLM...") + llm = VLLMLLM(config) + + # Test messages for KV cache building + system_messages: MessageList = [ + {"role": "system", "content": "You are a helpful AI assistant."}, + {"role": "user", "content": "Hello! Can you tell me about vLLM?"} + ] + + # Build KV cache for system messages + print("Building KV cache for system messages...") + try: + prompt = llm.build_vllm_kv_cache(system_messages) + print(f"✓ KV cache built successfully. Prompt length: {len(prompt)}") + except Exception as e: + print(f"✗ Failed to build KV cache: {e}") + + # Test with different messages + user_messages: MessageList = [ + {"role": "system", "content": "You are a helpful AI assistant."}, + {"role": "user", "content": "What are the benefits of using vLLM?"} + ] + + # Generate response + print("\nGenerating response...") + try: + response = llm.generate(user_messages) + print(f"Response: {response}") + except Exception as e: + print(f"Error generating response: {e}") + + # Test with string input for KV cache + print("\nTesting KV cache with string input...") + try: + string_prompt = llm.build_vllm_kv_cache("You are a helpful assistant.") + print(f"✓ String KV cache built successfully. Prompt length: {len(string_prompt)}") + except Exception as e: + print(f"✗ Failed to build string KV cache: {e}") + + # Test with list of strings input for KV cache + print("\nTesting KV cache with list of strings input...") + try: + list_prompt = llm.build_vllm_kv_cache(["You are helpful.", "You are knowledgeable."]) + print(f"✓ List KV cache built successfully. Prompt length: {len(list_prompt)}") + except Exception as e: + print(f"✗ Failed to build list KV cache: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 533b6b850..0df9b1ddc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -57,6 +57,32 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "anthropic" +version = "0.57.1" +description = "The official Python library for the anthropic API" +optional = false +python-versions = ">=3.8" +groups = ["eval"] +files = [ + {file = "anthropic-0.57.1-py3-none-any.whl", hash = "sha256:33afc1f395af207d07ff1bffc0a3d1caac53c371793792569c5d2f09283ea306"}, + {file = "anthropic-0.57.1.tar.gz", hash = "sha256:7815dd92245a70d21f65f356f33fc80c5072eada87fb49437767ea2918b2c4b0"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.25.0,<1" +jiter = ">=0.4.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +typing-extensions = ">=4.10,<5" + +[package.extras] +aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.6)"] +bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] +vertex = ["google-auth[requests] (>=2,<3)"] + [[package]] name = "anyio" version = "4.9.0" @@ -80,6 +106,19 @@ doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +groups = ["main", "eval"] +markers = "python_version == \"3.10\"" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -87,7 +126,7 @@ description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_full_version < \"3.11.3\"" +markers = "python_version == \"3.11\" and python_full_version < \"3.11.3\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -168,8 +207,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" +groups = ["main", "eval"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -239,6 +277,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {main = "platform_python_implementation != \"PyPy\"", eval = "platform_python_implementation == \"PyPy\""} [package.dependencies] pycparser = "*" @@ -663,6 +702,24 @@ files = [ [package.dependencies] python-dotenv = "*" +[[package]] +name = "dydantic" +version = "0.0.8" +description = "Dynamically generate pydantic models from JSON schema." +optional = false +python-versions = "<4.0,>=3.9" +groups = ["eval"] +files = [ + {file = "dydantic-0.0.8-py3-none-any.whl", hash = "sha256:cd0a991f523bd8632699872f1c0c4278415dd04783e36adec5428defa0afb721"}, + {file = "dydantic-0.0.8.tar.gz", hash = "sha256:14a31d4cdfce314ce3e69e8f8c7c46cbc26ce3ce4485de0832260386c612942f"}, +] + +[package.dependencies] +pydantic = ">=2,<3" + +[package.extras] +email = ["email-validator (>=2.1,<3.0)"] + [[package]] name = "email-validator" version = "2.2.0" @@ -1405,6 +1462,33 @@ files = [ {file = "joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444"}, ] +[[package]] +name = "jsonpatch" +version = "1.33" +description = "Apply JSON-Patches (RFC 6902)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +groups = ["eval"] +files = [ + {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, + {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, +] + +[package.dependencies] +jsonpointer = ">=1.9" + +[[package]] +name = "jsonpointer" +version = "3.0.0" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=3.7" +groups = ["eval"] +files = [ + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, +] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -1495,6 +1579,234 @@ files = [ {file = "kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e"}, ] +[[package]] +name = "langchain" +version = "0.3.26" +description = "Building applications with LLMs through composability" +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langchain-0.3.26-py3-none-any.whl", hash = "sha256:361bb2e61371024a8c473da9f9c55f4ee50f269c5ab43afdb2b1309cb7ac36cf"}, + {file = "langchain-0.3.26.tar.gz", hash = "sha256:8ff034ee0556d3e45eff1f1e96d0d745ced57858414dba7171c8ebdbeb5580c9"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""} +langchain-core = ">=0.3.66,<1.0.0" +langchain-text-splitters = ">=0.3.8,<1.0.0" +langsmith = ">=0.1.17" +pydantic = ">=2.7.4,<3.0.0" +PyYAML = ">=5.3" +requests = ">=2,<3" +SQLAlchemy = ">=1.4,<3" + +[package.extras] +anthropic = ["langchain-anthropic"] +aws = ["langchain-aws"] +azure-ai = ["langchain-azure-ai"] +cohere = ["langchain-cohere"] +community = ["langchain-community"] +deepseek = ["langchain-deepseek"] +fireworks = ["langchain-fireworks"] +google-genai = ["langchain-google-genai"] +google-vertexai = ["langchain-google-vertexai"] +groq = ["langchain-groq"] +huggingface = ["langchain-huggingface"] +mistralai = ["langchain-mistralai"] +ollama = ["langchain-ollama"] +openai = ["langchain-openai"] +perplexity = ["langchain-perplexity"] +together = ["langchain-together"] +xai = ["langchain-xai"] + +[[package]] +name = "langchain-anthropic" +version = "0.3.17" +description = "An integration package connecting AnthropicMessages and LangChain" +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langchain_anthropic-0.3.17-py3-none-any.whl", hash = "sha256:6df784615b93aab0336fbd6a50ca2bd16a704ef01c9488c36a4fa7aad2faf2d6"}, + {file = "langchain_anthropic-0.3.17.tar.gz", hash = "sha256:f2c2a0382ed7992204d790ff8538448f5243f4dbb1e798256ef790c9a69033e4"}, +] + +[package.dependencies] +anthropic = ">=0.57.0,<1" +langchain-core = ">=0.3.68,<1.0.0" +pydantic = ">=2.7.4,<3.0.0" + +[[package]] +name = "langchain-core" +version = "0.3.68" +description = "Building applications with LLMs through composability" +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langchain_core-0.3.68-py3-none-any.whl", hash = "sha256:5e5c1fbef419590537c91b8c2d86af896fbcbaf0d5ed7fdcdd77f7d8f3467ba0"}, + {file = "langchain_core-0.3.68.tar.gz", hash = "sha256:312e1932ac9aa2eaf111b70fdc171776fa571d1a86c1f873dcac88a094b19c6f"}, +] + +[package.dependencies] +jsonpatch = ">=1.33,<2.0" +langsmith = ">=0.3.45" +packaging = ">=23.2,<25" +pydantic = ">=2.7.4" +PyYAML = ">=5.3" +tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0" +typing-extensions = ">=4.7" + +[[package]] +name = "langchain-openai" +version = "0.3.23" +description = "An integration package connecting OpenAI and LangChain" +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langchain_openai-0.3.23-py3-none-any.whl", hash = "sha256:624794394482c0923823f0aac44979968d77fdcfa810e42d4b0abd8096199a40"}, + {file = "langchain_openai-0.3.23.tar.gz", hash = "sha256:73411c06e04bc145db7146a6fcf33dd0f1a85130499dcae988829a4441ddaa66"}, +] + +[package.dependencies] +langchain-core = ">=0.3.65,<1.0.0" +openai = ">=1.68.2,<2.0.0" +tiktoken = ">=0.7,<1" + +[[package]] +name = "langchain-text-splitters" +version = "0.3.8" +description = "LangChain text splitting utilities" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["eval"] +files = [ + {file = "langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02"}, + {file = "langchain_text_splitters-0.3.8.tar.gz", hash = "sha256:116d4b9f2a22dda357d0b79e30acf005c5518177971c66a9f1ab0edfdb0f912e"}, +] + +[package.dependencies] +langchain-core = ">=0.3.51,<1.0.0" + +[[package]] +name = "langgraph" +version = "0.5.1" +description = "Building stateful, multi-actor applications with LLMs" +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langgraph-0.5.1-py3-none-any.whl", hash = "sha256:707f0cc0d2713011fff4578bf57de8226cd96bcc0679868be2f41eb0984bb5af"}, + {file = "langgraph-0.5.1.tar.gz", hash = "sha256:312d341979e38034dde60e08df53505b8a196619df1266d6eacf6b002a0a65a8"}, +] + +[package.dependencies] +langchain-core = ">=0.1" +langgraph-checkpoint = ">=2.1.0,<3.0.0" +langgraph-prebuilt = ">=0.5.0,<0.6.0" +langgraph-sdk = ">=0.1.42,<0.2.0" +pydantic = ">=2.7.4" +xxhash = ">=3.5.0" + +[[package]] +name = "langgraph-checkpoint" +version = "2.1.0" +description = "Library with base interfaces for LangGraph checkpoint savers." +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langgraph_checkpoint-2.1.0-py3-none-any.whl", hash = "sha256:4cea3e512081da1241396a519cbfe4c5d92836545e2c64e85b6f5c34a1b8bc61"}, + {file = "langgraph_checkpoint-2.1.0.tar.gz", hash = "sha256:cdaa2f0b49aa130ab185c02d82f02b40299a1fbc9ac59ac20cecce09642a1abe"}, +] + +[package.dependencies] +langchain-core = ">=0.2.38" +ormsgpack = ">=1.10.0" + +[[package]] +name = "langgraph-prebuilt" +version = "0.5.2" +description = "Library with high-level APIs for creating and executing LangGraph agents and tools." +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langgraph_prebuilt-0.5.2-py3-none-any.whl", hash = "sha256:1f4cd55deca49dffc3e5127eec12fcd244fc381321002f728afa88642d5ec59d"}, + {file = "langgraph_prebuilt-0.5.2.tar.gz", hash = "sha256:2c900a5be0d6a93ea2521e0d931697cad2b646f1fcda7aa5c39d8d7539772465"}, +] + +[package.dependencies] +langchain-core = ">=0.3.67" +langgraph-checkpoint = ">=2.1.0" + +[[package]] +name = "langgraph-sdk" +version = "0.1.72" +description = "SDK for interacting with LangGraph API" +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langgraph_sdk-0.1.72-py3-none-any.whl", hash = "sha256:925d3fcc7a26361db04f9c4beb3ec05bc36361b2a836d181ff2ab145071ec3ce"}, + {file = "langgraph_sdk-0.1.72.tar.gz", hash = "sha256:396d8195881830700e2d54a0a9ee273e8b1173428e667502ef9c182a3cec7ab7"}, +] + +[package.dependencies] +httpx = ">=0.25.2" +orjson = ">=3.10.1" + +[[package]] +name = "langmem" +version = "0.0.27" +description = "Prebuilt utilities for memory management and retrieval." +optional = false +python-versions = ">=3.10" +groups = ["eval"] +files = [ + {file = "langmem-0.0.27-py3-none-any.whl", hash = "sha256:25e9f06ad7c420442cf4b62caff6f805b124dfb2e2cc9cacc464d7a455fbafda"}, + {file = "langmem-0.0.27.tar.gz", hash = "sha256:729c1eb77c4cd8d9f2285f908a68a1e622ef01f074eeeb8cbbc7343f296efc53"}, +] + +[package.dependencies] +langchain = ">=0.3.15" +langchain-anthropic = ">=0.3.3" +langchain-core = ">=0.3.46" +langchain-openai = ">=0.3.1" +langgraph = ">=0.3.23" +langgraph-checkpoint = ">=2.0.12" +langsmith = ">=0.3.8" +trustcall = ">=0.0.39" + +[[package]] +name = "langsmith" +version = "0.4.4" +description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716"}, + {file = "langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66"}, +] + +[package.dependencies] +httpx = ">=0.23.0,<1" +orjson = {version = ">=3.9.14,<4.0.0", markers = "platform_python_implementation != \"PyPy\""} +packaging = ">=23.2" +pydantic = ">=1,<3" +requests = ">=2,<3" +requests-toolbelt = ">=1.0.0,<2.0.0" +zstandard = ">=0.23.0,<0.24.0" + +[package.extras] +langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"] +openai-agents = ["openai-agents (>=0.0.3,<0.1)"] +otel = ["opentelemetry-api (>=1.30.0,<2.0.0)", "opentelemetry-exporter-otlp-proto-http (>=1.30.0,<2.0.0)", "opentelemetry-sdk (>=1.30.0,<2.0.0)"] +pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"] + [[package]] name = "lxml" version = "5.4.0" @@ -2437,7 +2749,7 @@ version = "3.10.18" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "eval"] files = [ {file = "orjson-3.10.18-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a45e5d68066b408e4bc383b6e4ef05e717c65219a9e1390abc6155a520cac402"}, {file = "orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be3b9b143e8b9db05368b13b04c84d37544ec85bb97237b3a923f076265ec89c"}, @@ -2513,16 +2825,67 @@ files = [ {file = "orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53"}, ] +[[package]] +name = "ormsgpack" +version = "1.10.0" +description = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "ormsgpack-1.10.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8a52c7ce7659459f3dc8dec9fd6a6c76f855a0a7e2b61f26090982ac10b95216"}, + {file = "ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:060f67fe927582f4f63a1260726d019204b72f460cf20930e6c925a1d129f373"}, + {file = "ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7058ef6092f995561bf9f71d6c9a4da867b6cc69d2e94cb80184f579a3ceed5"}, + {file = "ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6f3509c1b0e51b15552d314b1d409321718122e90653122ce4b997f01453a"}, + {file = "ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c1edafd5c72b863b1f875ec31c529f09c872a5ff6fe473b9dfaf188ccc3227"}, + {file = "ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c780b44107a547a9e9327270f802fa4d6b0f6667c9c03c3338c0ce812259a0f7"}, + {file = "ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:137aab0d5cdb6df702da950a80405eb2b7038509585e32b4e16289604ac7cb84"}, + {file = "ormsgpack-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:3e666cb63030538fa5cd74b1e40cb55b6fdb6e2981f024997a288bf138ebad07"}, + {file = "ormsgpack-1.10.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4bb7df307e17b36cbf7959cd642c47a7f2046ae19408c564e437f0ec323a7775"}, + {file = "ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8817ae439c671779e1127ee62f0ac67afdeaeeacb5f0db45703168aa74a2e4af"}, + {file = "ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f345f81e852035d80232e64374d3a104139d60f8f43c6c5eade35c4bac5590e"}, + {file = "ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21de648a1c7ef692bdd287fb08f047bd5371d7462504c0a7ae1553c39fee35e3"}, + {file = "ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3a7d844ae9cbf2112c16086dd931b2acefce14cefd163c57db161170c2bfa22b"}, + {file = "ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e4d80585403d86d7f800cf3d0aafac1189b403941e84e90dd5102bb2b92bf9d5"}, + {file = "ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da1de515a87e339e78a3ccf60e39f5fb740edac3e9e82d3c3d209e217a13ac08"}, + {file = "ormsgpack-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:57c4601812684024132cbb32c17a7d4bb46ffc7daf2fddf5b697391c2c4f142a"}, + {file = "ormsgpack-1.10.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4e159d50cd4064d7540e2bc6a0ab66eab70b0cc40c618b485324ee17037527c0"}, + {file = "ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb47c85f3a866e29279d801115b554af0fefc409e2ed8aa90aabfa77efe5cc6"}, + {file = "ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c28249574934534c9bd5dce5485c52f21bcea0ee44d13ece3def6e3d2c3798b5"}, + {file = "ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1957dcadbb16e6a981cd3f9caef9faf4c2df1125e2a1b702ee8236a55837ce07"}, + {file = "ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b29412558c740bf6bac156727aa85ac67f9952cd6f071318f29ee72e1a76044"}, + {file = "ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6933f350c2041ec189fe739f0ba7d6117c8772f5bc81f45b97697a84d03020dd"}, + {file = "ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a86de06d368fcc2e58b79dece527dc8ca831e0e8b9cec5d6e633d2777ec93d0"}, + {file = "ormsgpack-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:35fa9f81e5b9a0dab42e09a73f7339ecffdb978d6dbf9deb2ecf1e9fc7808722"}, + {file = "ormsgpack-1.10.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d816d45175a878993b7372bd5408e0f3ec5a40f48e2d5b9d8f1cc5d31b61f1f"}, + {file = "ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90345ccb058de0f35262893751c603b6376b05f02be2b6f6b7e05d9dd6d5643"}, + {file = "ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144b5e88f1999433e54db9d637bae6fe21e935888be4e3ac3daecd8260bd454e"}, + {file = "ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2190b352509d012915921cca76267db136cd026ddee42f1b0d9624613cc7058c"}, + {file = "ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:86fd9c1737eaba43d3bb2730add9c9e8b5fbed85282433705dd1b1e88ea7e6fb"}, + {file = "ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:33afe143a7b61ad21bb60109a86bb4e87fec70ef35db76b89c65b17e32da7935"}, + {file = "ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f23d45080846a7b90feabec0d330a9cc1863dc956728412e4f7986c80ab3a668"}, + {file = "ormsgpack-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:534d18acb805c75e5fba09598bf40abe1851c853247e61dda0c01f772234da69"}, + {file = "ormsgpack-1.10.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:efdb25cf6d54085f7ae557268d59fd2d956f1a09a340856e282d2960fe929f32"}, + {file = "ormsgpack-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddfcb30d4b1be2439836249d675f297947f4fb8efcd3eeb6fd83021d773cadc4"}, + {file = "ormsgpack-1.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee0944b6ccfd880beb1ca29f9442a774683c366f17f4207f8b81c5e24cadb453"}, + {file = "ormsgpack-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35cdff6a0d3ba04e40a751129763c3b9b57a602c02944138e4b760ec99ae80a1"}, + {file = "ormsgpack-1.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:599ccdabc19c618ef5de6e6f2e7f5d48c1f531a625fa6772313b8515bc710681"}, + {file = "ormsgpack-1.10.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:bf46f57da9364bd5eefd92365c1b78797f56c6f780581eecd60cd7b367f9b4d3"}, + {file = "ormsgpack-1.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b796f64fdf823dedb1e35436a4a6f889cf78b1aa42d3097c66e5adfd8c3bd72d"}, + {file = "ormsgpack-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:106253ac9dc08520951e556b3c270220fcb8b4fef0d30b71eedac4befa4de749"}, + {file = "ormsgpack-1.10.0.tar.gz", hash = "sha256:7f7a27efd67ef22d7182ec3b7fa7e9d147c3ad9be2a24656b23c989077e08b16"}, +] + [[package]] name = "packaging" -version = "25.0" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "eval", "test"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -2878,23 +3241,23 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" +groups = ["main", "eval"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "platform_python_implementation != \"PyPy\"", eval = "platform_python_implementation == \"PyPy\""} [[package]] name = "pydantic" -version = "2.11.4" +version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main", "eval"] files = [ - {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, - {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] @@ -3172,14 +3535,14 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.1.0" +version = "1.1.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.9" groups = ["main", "eval"] files = [ - {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, - {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, ] [package.extras] @@ -3492,6 +3855,21 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["eval"] +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + [[package]] name = "rich" version = "14.0.0" @@ -3612,6 +3990,21 @@ tensorflow = ["safetensors[numpy]", "tensorflow (>=2.11.0)"] testing = ["h5py (>=3.7.0)", "huggingface-hub (>=0.12.1)", "hypothesis (>=6.70.2)", "pytest (>=7.2.0)", "pytest-benchmark (>=4.0.0)", "safetensors[numpy]", "setuptools-rust (>=1.5.2)"] torch = ["safetensors[numpy]", "torch (>=1.10)"] +[[package]] +name = "schedule" +version = "1.2.2" +description = "Job scheduling for humans." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "schedule-1.2.2-py3-none-any.whl", hash = "sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d"}, + {file = "schedule-1.2.2.tar.gz", hash = "sha256:15fe9c75fe5fd9b9627f3f19cc0ef1420508f9f9a46f45cd0769ef75ede5f0b7"}, +] + +[package.extras] +timezone = ["pytz"] + [[package]] name = "scikit-learn" version = "1.7.0" @@ -3668,7 +4061,8 @@ version = "1.15.3" description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "eval"] +markers = "python_version == \"3.10\"" files = [ {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, @@ -3726,6 +4120,62 @@ dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodest doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "scipy" +version = "1.16.0" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main", "eval"] +markers = "python_version >= \"3.11\"" +files = [ + {file = "scipy-1.16.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:deec06d831b8f6b5fb0b652433be6a09db29e996368ce5911faf673e78d20085"}, + {file = "scipy-1.16.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d30c0fe579bb901c61ab4bb7f3eeb7281f0d4c4a7b52dbf563c89da4fd2949be"}, + {file = "scipy-1.16.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:b2243561b45257f7391d0f49972fca90d46b79b8dbcb9b2cb0f9df928d370ad4"}, + {file = "scipy-1.16.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e6d7dfc148135e9712d87c5f7e4f2ddc1304d1582cb3a7d698bbadedb61c7afd"}, + {file = "scipy-1.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90452f6a9f3fe5a2cf3748e7be14f9cc7d9b124dce19667b54f5b429d680d539"}, + {file = "scipy-1.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a2f0bf2f58031c8701a8b601df41701d2a7be17c7ffac0a4816aeba89c4cdac8"}, + {file = "scipy-1.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c4abb4c11fc0b857474241b812ce69ffa6464b4bd8f4ecb786cf240367a36a7"}, + {file = "scipy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b370f8f6ac6ef99815b0d5c9f02e7ade77b33007d74802efc8316c8db98fd11e"}, + {file = "scipy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:a16ba90847249bedce8aa404a83fb8334b825ec4a8e742ce6012a7a5e639f95c"}, + {file = "scipy-1.16.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:7eb6bd33cef4afb9fa5f1fb25df8feeb1e52d94f21a44f1d17805b41b1da3180"}, + {file = "scipy-1.16.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:1dbc8fdba23e4d80394ddfab7a56808e3e6489176d559c6c71935b11a2d59db1"}, + {file = "scipy-1.16.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7dcf42c380e1e3737b343dec21095c9a9ad3f9cbe06f9c05830b44b1786c9e90"}, + {file = "scipy-1.16.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26ec28675f4a9d41587266084c626b02899db373717d9312fa96ab17ca1ae94d"}, + {file = "scipy-1.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:952358b7e58bd3197cfbd2f2f2ba829f258404bdf5db59514b515a8fe7a36c52"}, + {file = "scipy-1.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03931b4e870c6fef5b5c0970d52c9f6ddd8c8d3e934a98f09308377eba6f3824"}, + {file = "scipy-1.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:512c4f4f85912767c351a0306824ccca6fd91307a9f4318efe8fdbd9d30562ef"}, + {file = "scipy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e69f798847e9add03d512eaf5081a9a5c9a98757d12e52e6186ed9681247a1ac"}, + {file = "scipy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:adf9b1999323ba335adc5d1dc7add4781cb5a4b0ef1e98b79768c05c796c4e49"}, + {file = "scipy-1.16.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:e9f414cbe9ca289a73e0cc92e33a6a791469b6619c240aa32ee18abdce8ab451"}, + {file = "scipy-1.16.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:bbba55fb97ba3cdef9b1ee973f06b09d518c0c7c66a009c729c7d1592be1935e"}, + {file = "scipy-1.16.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:58e0d4354eacb6004e7aa1cd350e5514bd0270acaa8d5b36c0627bb3bb486974"}, + {file = "scipy-1.16.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b2094ec975c80efc273567436e16bb794660509c12c6a31eb5c195cbf4b6dc"}, + {file = "scipy-1.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b65d232157a380fdd11a560e7e21cde34fdb69d65c09cb87f6cc024ee376351"}, + {file = "scipy-1.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d8747f7736accd39289943f7fe53a8333be7f15a82eea08e4afe47d79568c32"}, + {file = "scipy-1.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb9f147a1b8529bb7fec2a85cf4cf42bdfadf9e83535c309a11fdae598c88e8b"}, + {file = "scipy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d2b83c37edbfa837a8923d19c749c1935ad3d41cf196006a24ed44dba2ec4358"}, + {file = "scipy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:79a3c13d43c95aa80b87328a46031cf52508cf5f4df2767602c984ed1d3c6bbe"}, + {file = "scipy-1.16.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:f91b87e1689f0370690e8470916fe1b2308e5b2061317ff76977c8f836452a47"}, + {file = "scipy-1.16.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:88a6ca658fb94640079e7a50b2ad3b67e33ef0f40e70bdb7dc22017dae73ac08"}, + {file = "scipy-1.16.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ae902626972f1bd7e4e86f58fd72322d7f4ec7b0cfc17b15d4b7006efc385176"}, + {file = "scipy-1.16.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:8cb824c1fc75ef29893bc32b3ddd7b11cf9ab13c1127fe26413a05953b8c32ed"}, + {file = "scipy-1.16.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:de2db7250ff6514366a9709c2cba35cb6d08498e961cba20d7cff98a7ee88938"}, + {file = "scipy-1.16.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e85800274edf4db8dd2e4e93034f92d1b05c9421220e7ded9988b16976f849c1"}, + {file = "scipy-1.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4f720300a3024c237ace1cb11f9a84c38beb19616ba7c4cdcd771047a10a1706"}, + {file = "scipy-1.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aad603e9339ddb676409b104c48a027e9916ce0d2838830691f39552b38a352e"}, + {file = "scipy-1.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f56296fefca67ba605fd74d12f7bd23636267731a72cb3947963e76b8c0a25db"}, + {file = "scipy-1.16.0.tar.gz", hash = "sha256:b5ef54021e832869c8cfb03bc3bf20366cbcd426e02a58e8a58d7584dfbb8f62"}, +] + +[package.dependencies] +numpy = ">=1.25.2,<2.6" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "linkify-it-py", "matplotlib (>=3.5)", "myst-nb (>=1.2.0)", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.2.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.3.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "sentence-transformers" version = "4.1.0" @@ -3963,7 +4413,7 @@ version = "9.1.2" description = "Retry code until it succeeds" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "eval"] files = [ {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, @@ -3985,6 +4435,54 @@ files = [ {file = "threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e"}, ] +[[package]] +name = "tiktoken" +version = "0.9.0" +description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" +optional = false +python-versions = ">=3.9" +groups = ["eval"] +files = [ + {file = "tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382"}, + {file = "tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108"}, + {file = "tiktoken-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0968d5beeafbca2a72c595e8385a1a1f8af58feaebb02b227229b69ca5357fd"}, + {file = "tiktoken-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a5fb085a6a3b7350b8fc838baf493317ca0e17bd95e8642f95fc69ecfed1de"}, + {file = "tiktoken-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15a2752dea63d93b0332fb0ddb05dd909371ededa145fe6a3242f46724fa7990"}, + {file = "tiktoken-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:26113fec3bd7a352e4b33dbaf1bd8948de2507e30bd95a44e2b1156647bc01b4"}, + {file = "tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e"}, + {file = "tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348"}, + {file = "tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33"}, + {file = "tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136"}, + {file = "tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336"}, + {file = "tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb"}, + {file = "tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03"}, + {file = "tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210"}, + {file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794"}, + {file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22"}, + {file = "tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2"}, + {file = "tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16"}, + {file = "tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb"}, + {file = "tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63"}, + {file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01"}, + {file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139"}, + {file = "tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a"}, + {file = "tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95"}, + {file = "tiktoken-0.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c6386ca815e7d96ef5b4ac61e0048cd32ca5a92d5781255e13b31381d28667dc"}, + {file = "tiktoken-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75f6d5db5bc2c6274b674ceab1615c1778e6416b14705827d19b40e6355f03e0"}, + {file = "tiktoken-0.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15b16f61e6f4625a57a36496d28dd182a8a60ec20a534c5343ba3cafa156ac7"}, + {file = "tiktoken-0.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebcec91babf21297022882344c3f7d9eed855931466c3311b1ad6b64befb3df"}, + {file = "tiktoken-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e5fd49e7799579240f03913447c0cdfa1129625ebd5ac440787afc4345990427"}, + {file = "tiktoken-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:26242ca9dc8b58e875ff4ca078b9a94d2f0813e6a535dcd2205df5d49d927cc7"}, + {file = "tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d"}, +] + +[package.dependencies] +regex = ">=2022.1.18" +requests = ">=2.26.0" + +[package.extras] +blobfile = ["blobfile (>=2)"] + [[package]] name = "tokenizers" version = "0.21.1" @@ -4243,6 +4741,23 @@ build = ["cmake (>=3.20)", "lit"] tests = ["autopep8", "isort", "llnl-hatchet", "numpy", "pytest", "pytest-forked", "pytest-xdist", "scipy (>=1.7.1)"] tutorials = ["matplotlib", "pandas", "tabulate"] +[[package]] +name = "trustcall" +version = "0.0.39" +description = "Tenacious & trustworthy tool calling built on LangGraph." +optional = false +python-versions = "<4.0,>=3.10" +groups = ["eval"] +files = [ + {file = "trustcall-0.0.39-py3-none-any.whl", hash = "sha256:d7da42e0bba816c0539b2936dfed90ffb3ea8d789e548e73865d416f8ac4ee64"}, + {file = "trustcall-0.0.39.tar.gz", hash = "sha256:ec315818224501b9537ce6b7618dbc21be41210c6e8f2e239169a5a00912cd6e"}, +] + +[package.dependencies] +dydantic = ">=0.0.8,<1.0.0" +jsonpatch = ">=1.33,<2.0" +langgraph = ">=0.2.25" + [[package]] name = "typer" version = "0.16.0" @@ -4734,6 +5249,139 @@ files = [ {file = "xlsxwriter-3.2.3.tar.gz", hash = "sha256:ad6fd41bdcf1b885876b1f6b7087560aecc9ae5a9cc2ba97dcac7ab2e210d3d5"}, ] +[[package]] +name = "xxhash" +version = "3.5.0" +description = "Python binding for xxHash" +optional = false +python-versions = ">=3.7" +groups = ["eval"] +files = [ + {file = "xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212"}, + {file = "xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5d3e570ef46adaf93fc81b44aca6002b5a4d8ca11bd0580c07eac537f36680"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb29a034301e2982df8b1fe6328a84f4b676106a13e9135a0d7e0c3e9f806da"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0d307d27099bb0cbeea7260eb39ed4fdb99c5542e21e94bb6fd29e49c57a23"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0342aafd421795d740e514bc9858ebddfc705a75a8c5046ac56d85fe97bf196"}, + {file = "xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dbbd9892c5ebffeca1ed620cf0ade13eb55a0d8c84e0751a6653adc6ac40d0c"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4cc2d67fdb4d057730c75a64c5923abfa17775ae234a71b0200346bfb0a7f482"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ec28adb204b759306a3d64358a5e5c07d7b1dd0ccbce04aa76cb9377b7b70296"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1328f6d8cca2b86acb14104e381225a3d7b42c92c4b86ceae814e5c400dbb415"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8d47ebd9f5d9607fd039c1fbf4994e3b071ea23eff42f4ecef246ab2b7334198"}, + {file = "xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b96d559e0fcddd3343c510a0fe2b127fbff16bf346dd76280b82292567523442"}, + {file = "xxhash-3.5.0-cp310-cp310-win32.whl", hash = "sha256:61c722ed8d49ac9bc26c7071eeaa1f6ff24053d553146d5df031802deffd03da"}, + {file = "xxhash-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bed5144c6923cc902cd14bb8963f2d5e034def4486ab0bbe1f58f03f042f9a9"}, + {file = "xxhash-3.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:893074d651cf25c1cc14e3bea4fceefd67f2921b1bb8e40fcfeba56820de80c6"}, + {file = "xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1"}, + {file = "xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a"}, + {file = "xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d"}, + {file = "xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839"}, + {file = "xxhash-3.5.0-cp311-cp311-win32.whl", hash = "sha256:109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da"}, + {file = "xxhash-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58"}, + {file = "xxhash-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3"}, + {file = "xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00"}, + {file = "xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6"}, + {file = "xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab"}, + {file = "xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e"}, + {file = "xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8"}, + {file = "xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e"}, + {file = "xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2"}, + {file = "xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6"}, + {file = "xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb"}, + {file = "xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7"}, + {file = "xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c"}, + {file = "xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637"}, + {file = "xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43"}, + {file = "xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b"}, + {file = "xxhash-3.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6e5f70f6dca1d3b09bccb7daf4e087075ff776e3da9ac870f86ca316736bb4aa"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e76e83efc7b443052dd1e585a76201e40b3411fe3da7af4fe434ec51b2f163b"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33eac61d0796ca0591f94548dcfe37bb193671e0c9bcf065789b5792f2eda644"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ec70a89be933ea49222fafc3999987d7899fc676f688dd12252509434636622"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86b8e7f703ec6ff4f351cfdb9f428955859537125904aa8c963604f2e9d3e7"}, + {file = "xxhash-3.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0adfbd36003d9f86c8c97110039f7539b379f28656a04097e7434d3eaf9aa131"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:63107013578c8a730419adc05608756c3fa640bdc6abe806c3123a49fb829f43"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:683b94dbd1ca67557850b86423318a2e323511648f9f3f7b1840408a02b9a48c"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5d2a01dcce81789cf4b12d478b5464632204f4c834dc2d064902ee27d2d1f0ee"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:a9d360a792cbcce2fe7b66b8d51274ec297c53cbc423401480e53b26161a290d"}, + {file = "xxhash-3.5.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f0b48edbebea1b7421a9c687c304f7b44d0677c46498a046079d445454504737"}, + {file = "xxhash-3.5.0-cp37-cp37m-win32.whl", hash = "sha256:7ccb800c9418e438b44b060a32adeb8393764da7441eb52aa2aa195448935306"}, + {file = "xxhash-3.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c3bc7bf8cb8806f8d1c9bf149c18708cb1c406520097d6b0a73977460ea03602"}, + {file = "xxhash-3.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74752ecaa544657d88b1d1c94ae68031e364a4d47005a90288f3bab3da3c970f"}, + {file = "xxhash-3.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dee1316133c9b463aa81aca676bc506d3f80d8f65aeb0bba2b78d0b30c51d7bd"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:602d339548d35a8579c6b013339fb34aee2df9b4e105f985443d2860e4d7ffaa"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:695735deeddfb35da1677dbc16a083445360e37ff46d8ac5c6fcd64917ff9ade"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1030a39ba01b0c519b1a82f80e8802630d16ab95dc3f2b2386a0b5c8ed5cbb10"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5bc08f33c4966f4eb6590d6ff3ceae76151ad744576b5fc6c4ba8edd459fdec"}, + {file = "xxhash-3.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160e0c19ee500482ddfb5d5570a0415f565d8ae2b3fd69c5dcfce8a58107b1c3"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f1abffa122452481a61c3551ab3c89d72238e279e517705b8b03847b1d93d738"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d5e9db7ef3ecbfc0b4733579cea45713a76852b002cf605420b12ef3ef1ec148"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:23241ff6423378a731d84864bf923a41649dc67b144debd1077f02e6249a0d54"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:82b833d5563fefd6fceafb1aed2f3f3ebe19f84760fdd289f8b926731c2e6e91"}, + {file = "xxhash-3.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a80ad0ffd78bef9509eee27b4a29e56f5414b87fb01a888353e3d5bda7038bd"}, + {file = "xxhash-3.5.0-cp38-cp38-win32.whl", hash = "sha256:50ac2184ffb1b999e11e27c7e3e70cc1139047e7ebc1aa95ed12f4269abe98d4"}, + {file = "xxhash-3.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:392f52ebbb932db566973693de48f15ce787cabd15cf6334e855ed22ea0be5b3"}, + {file = "xxhash-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc8cdd7f33d57f0468b0614ae634cc38ab9202c6957a60e31d285a71ebe0301"}, + {file = "xxhash-3.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0c48b6300cd0b0106bf49169c3e0536408dfbeb1ccb53180068a18b03c662ab"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe1a92cfbaa0a1253e339ccec42dbe6db262615e52df591b68726ab10338003f"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33513d6cc3ed3b559134fb307aae9bdd94d7e7c02907b37896a6c45ff9ce51bd"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eefc37f6138f522e771ac6db71a6d4838ec7933939676f3753eafd7d3f4c40bc"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a606c8070ada8aa2a88e181773fa1ef17ba65ce5dd168b9d08038e2a61b33754"}, + {file = "xxhash-3.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42eca420c8fa072cc1dd62597635d140e78e384a79bb4944f825fbef8bfeeef6"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:604253b2143e13218ff1ef0b59ce67f18b8bd1c4205d2ffda22b09b426386898"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6e93a5ad22f434d7876665444a97e713a8f60b5b1a3521e8df11b98309bff833"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7a46e1d6d2817ba8024de44c4fd79913a90e5f7265434cef97026215b7d30df6"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:30eb2efe6503c379b7ab99c81ba4a779748e3830241f032ab46bd182bf5873af"}, + {file = "xxhash-3.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c8aa771ff2c13dd9cda8166d685d7333d389fae30a4d2bb39d63ab5775de8606"}, + {file = "xxhash-3.5.0-cp39-cp39-win32.whl", hash = "sha256:5ed9ebc46f24cf91034544b26b131241b699edbfc99ec5e7f8f3d02d6eb7fba4"}, + {file = "xxhash-3.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:220f3f896c6b8d0316f63f16c077d52c412619e475f9372333474ee15133a558"}, + {file = "xxhash-3.5.0-cp39-cp39-win_arm64.whl", hash = "sha256:a7b1d8315d9b5e9f89eb2933b73afae6ec9597a258d52190944437158b49d38e"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2014c5b3ff15e64feecb6b713af12093f75b7926049e26a580e94dcad3c73d8c"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab81ef75003eda96239a23eda4e4543cedc22e34c373edcaf744e721a163986"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2febf914ace002132aa09169cc572e0d8959d0f305f93d5828c4836f9bc5a6"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d3a10609c51da2a1c0ea0293fc3968ca0a18bd73838455b5bca3069d7f8e32b"}, + {file = "xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b4154c00eb22e4d543f472cfca430e7962a0f1d0f3778334f2e08a7ba59363c"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d30bbc1644f726b825b3278764240f449d75f1a8bdda892e641d4a688b1494ae"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fa0b72f2423e2aa53077e54a61c28e181d23effeaafd73fcb9c494e60930c8e"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13de2b76c1835399b2e419a296d5b38dc4855385d9e96916299170085ef72f57"}, + {file = "xxhash-3.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0691bfcc4f9c656bcb96cc5db94b4d75980b9d5589f2e59de790091028580837"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:297595fe6138d4da2c8ce9e72a04d73e58725bb60f3a19048bc96ab2ff31c692"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc1276d369452040cbb943300dc8abeedab14245ea44056a2943183822513a18"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2061188a1ba352fc699c82bff722f4baacb4b4b8b2f0c745d2001e56d0dfb514"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38c384c434021e4f62b8d9ba0bc9467e14d394893077e2c66d826243025e1f81"}, + {file = "xxhash-3.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e6a4dd644d72ab316b580a1c120b375890e4c52ec392d4aef3c63361ec4d77d1"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:531af8845aaadcadf951b7e0c1345c6b9c68a990eeb74ff9acd8501a0ad6a1c9"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ce379bcaa9fcc00f19affa7773084dd09f5b59947b3fb47a1ceb0179f91aaa1"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd1b2281d01723f076df3c8188f43f2472248a6b63118b036e641243656b1b0f"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c770750cc80e8694492244bca7251385188bc5597b6a39d98a9f30e8da984e0"}, + {file = "xxhash-3.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b150b8467852e1bd844387459aa6fbe11d7f38b56e901f9f3b3e6aba0d660240"}, + {file = "xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f"}, +] + [[package]] name = "zep-cloud" version = "2.15.0" @@ -4751,7 +5399,120 @@ httpx = ">=0.21.2" pydantic = ">=1.9.2" typing_extensions = ">=4.0.0" +[[package]] +name = "zstandard" +version = "0.23.0" +description = "Zstandard bindings for Python" +optional = false +python-versions = ">=3.8" +groups = ["eval"] +files = [ + {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, + {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c"}, + {file = "zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813"}, + {file = "zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473"}, + {file = "zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160"}, + {file = "zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35"}, + {file = "zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d"}, + {file = "zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33"}, + {file = "zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd"}, + {file = "zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ef3775758346d9ac6214123887d25c7061c92afe1f2b354f9388e9e4d48acfc"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4051e406288b8cdbb993798b9a45c59a4896b6ecee2f875424ec10276a895740"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2d1a054f8f0a191004675755448d12be47fa9bebbcffa3cdf01db19f2d30a54"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83fa6cae3fff8e98691248c9320356971b59678a17f20656a9e59cd32cee6d8"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32ba3b5ccde2d581b1e6aa952c836a6291e8435d788f656fe5976445865ae045"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f146f50723defec2975fb7e388ae3a024eb7151542d1599527ec2aa9cacb152"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bfe8de1da6d104f15a60d4a8a768288f66aa953bbe00d027398b93fb9680b26"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:29a2bc7c1b09b0af938b7a8343174b987ae021705acabcbae560166567f5a8db"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61f89436cbfede4bc4e91b4397eaa3e2108ebe96d05e93d6ccc95ab5714be512"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53ea7cdc96c6eb56e76bb06894bcfb5dfa93b7adcf59d61c6b92674e24e2dd5e"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:a4ae99c57668ca1e78597d8b06d5af837f377f340f4cce993b551b2d7731778d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:379b378ae694ba78cef921581ebd420c938936a153ded602c4fea612b7eaa90d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:50a80baba0285386f97ea36239855f6020ce452456605f262b2d33ac35c7770b"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:61062387ad820c654b6a6b5f0b94484fa19515e0c5116faf29f41a6bc91ded6e"}, + {file = "zstandard-0.23.0-cp38-cp38-win32.whl", hash = "sha256:b8c0bd73aeac689beacd4e7667d48c299f61b959475cdbb91e7d3d88d27c56b9"}, + {file = "zstandard-0.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:a05e6d6218461eb1b4771d973728f0133b2a4613a6779995df557f70794fd60f"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa014d55c3af933c1315eb4bb06dd0459661cc0b15cd61077afa6489bec63bb"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7f0804bb3799414af278e9ad51be25edf67f78f916e08afdb983e74161b916"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb2b1ecfef1e67897d336de3a0e3f52478182d6a47eda86cbd42504c5cbd009a"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:837bb6764be6919963ef41235fd56a6486b132ea64afe5fafb4cb279ac44f259"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1516c8c37d3a053b01c1c15b182f3b5f5eef19ced9b930b684a73bad121addf4"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48ef6a43b1846f6025dde6ed9fee0c24e1149c1c25f7fb0a0585572b2f3adc58"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11e3bf3c924853a2d5835b24f03eeba7fc9b07d8ca499e247e06ff5676461a15"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fb4535137de7e244c230e24f9d1ec194f61721c86ebea04e1581d9d06ea1269"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c24f21fa2af4bb9f2c492a86fe0c34e6d2c63812a839590edaf177b7398f700"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8c86881813a78a6f4508ef9daf9d4995b8ac2d147dcb1a450448941398091c9"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe3b385d996ee0822fd46528d9f0443b880d4d05528fd26a9119a54ec3f91c69"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:82d17e94d735c99621bf8ebf9995f870a6b3e6d14543b99e201ae046dfe7de70"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c7c517d74bea1a6afd39aa612fa025e6b8011982a0897768a2f7c8ab4ebb78a2"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fd7e0f1cfb70eb2f95a19b472ee7ad6d9a0a992ec0ae53286870c104ca939e5"}, + {file = "zstandard-0.23.0-cp39-cp39-win32.whl", hash = "sha256:43da0f0092281bf501f9c5f6f3b4c975a8a0ea82de49ba3f7100e64d422a1274"}, + {file = "zstandard-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:f8346bfa098532bc1fb6c7ef06783e969d87a99dd1d2a5a18a892c1d7a643c58"}, + {file = "zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09"}, +] + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "0e0294fc960128caac7069561ac7ec4e4dda42634db988ee96923ddc0af1ca1a" +content-hash = "b56786e7bcdae03bbe0f031e2fe0bee284fef76534a3d3bfb24d6f70a44f4f0d" diff --git a/pyproject.toml b/pyproject.toml index 9d066b987..fe4bc0804 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [tool.poetry] name = "MemoryOS" -version = "0.1.12" +version = "0.1.13" description = "Intelligence Begins with Memory" license = "Apache-2.0" authors = ["MemTensor "] @@ -26,6 +26,7 @@ fastapi = {extras = ["all"], version = "^0.115.12"} sentence-transformers = "^4.1.0" sqlalchemy = "^2.0.41" redis = "^6.2.0" +schedule = "^1.2.2" [tool.poetry.group.dev] optional = false @@ -51,6 +52,10 @@ zep-cloud = "^2.15.0" rouge-score = "^0.1.2" nltk = "^3.9.1" bert-score = "^0.3.13" +scipy = "^1.10.1" +python-dotenv = "^1.1.1" +langgraph = "^0.5.1" +langmem = "^0.0.27" [[tool.poetry.source]] name = "mirrors" diff --git a/src/memos/__init__.py b/src/memos/__init__.py index a390108d5..cc44852c7 100644 --- a/src/memos/__init__.py +++ b/src/memos/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.1.12" +__version__ = "0.1.13" from memos.configs.mem_cube import GeneralMemCubeConfig from memos.configs.mem_os import MOSConfig diff --git a/src/memos/configs/internet_retriever.py b/src/memos/configs/internet_retriever.py new file mode 100644 index 000000000..56f892ac9 --- /dev/null +++ b/src/memos/configs/internet_retriever.py @@ -0,0 +1,81 @@ +"""Configuration classes for internet retrievers.""" + +from typing import Any, ClassVar + +from pydantic import Field, field_validator, model_validator + +from memos.configs.base import BaseConfig +from memos.exceptions import ConfigurationError + + +class BaseInternetRetrieverConfig(BaseConfig): + """Base configuration class for internet retrievers.""" + + api_key: str = Field(..., description="API key for the search service") + search_engine_id: str | None = Field( + None, description="Search engine ID (required for Google Custom Search)" + ) + + +class GoogleCustomSearchConfig(BaseInternetRetrieverConfig): + """Configuration class for Google Custom Search API.""" + + search_engine_id: str = Field(..., description="Google Custom Search Engine ID (cx parameter)") + max_results: int = Field(default=20, description="Maximum number of results to retrieve") + num_per_request: int = Field( + default=10, description="Number of results per API request (max 10 for Google)" + ) + + +class BingSearchConfig(BaseInternetRetrieverConfig): + """Configuration class for Bing Search API.""" + + endpoint: str = Field( + default="https://api.bing.microsoft.com/v7.0/search", description="Bing Search API endpoint" + ) + max_results: int = Field(default=20, description="Maximum number of results to retrieve") + num_per_request: int = Field(default=10, description="Number of results per API request") + + +class XinyuSearchConfig(BaseInternetRetrieverConfig): + """Configuration class for Xinyu Search API.""" + + search_engine_id: str | None = Field( + None, description="Not used for Xinyu Search (kept for compatibility)" + ) + max_results: int = Field(default=20, description="Maximum number of results to retrieve") + num_per_request: int = Field( + default=10, description="Number of results per API request (not used for Xinyu)" + ) + + +class InternetRetrieverConfigFactory(BaseConfig): + """Factory class for creating internet retriever configurations.""" + + backend: str | None = Field( + None, description="Backend for internet retriever (google, bing, etc.)" + ) + config: dict[str, Any] | None = Field( + None, description="Configuration for the internet retriever backend" + ) + + backend_to_class: ClassVar[dict[str, Any]] = { + "google": GoogleCustomSearchConfig, + "bing": BingSearchConfig, + "xinyu": XinyuSearchConfig, + } + + @field_validator("backend") + @classmethod + def validate_backend(cls, backend: str | None) -> str | None: + """Validate the backend field.""" + if backend is not None and backend not in cls.backend_to_class: + raise ConfigurationError(f"Invalid internet retriever backend: {backend}") + return backend + + @model_validator(mode="after") + def create_config(self) -> "InternetRetrieverConfigFactory": + if self.backend is not None: + config_class = self.backend_to_class[self.backend] + self.config = config_class(**self.config) + return self diff --git a/src/memos/configs/llm.py b/src/memos/configs/llm.py index 1f75e9948..b09e98c1f 100644 --- a/src/memos/configs/llm.py +++ b/src/memos/configs/llm.py @@ -43,6 +43,12 @@ class HFLLMConfig(BaseLLMConfig): description="Apply generation template for the conversation", ) +class VLLMLLMConfig(BaseLLMConfig): + api_key: str = Field(default="", description="API key for vLLM (optional for local server)") + api_base: str = Field( + default="http://localhost:8088", + description="Base URL for vLLM API", + ) class LLMConfigFactory(BaseConfig): """Factory class for creating LLM configurations.""" @@ -54,6 +60,7 @@ class LLMConfigFactory(BaseConfig): "openai": OpenAILLMConfig, "ollama": OllamaLLMConfig, "huggingface": HFLLMConfig, + "vllm": VLLMLLMConfig, "huggingface_singleton": HFLLMConfig, # Add singleton support } diff --git a/src/memos/configs/mem_os.py b/src/memos/configs/mem_os.py index d4135a2af..96b4094ed 100644 --- a/src/memos/configs/mem_os.py +++ b/src/memos/configs/mem_os.py @@ -57,6 +57,10 @@ class MOSConfig(BaseConfig): default=False, description="Enable memory scheduler for automated memory management", ) + PRO_MODE: bool = Field( + default=False, + description="Enable PRO mode for complex query decomposition", + ) class MemOSConfigFactory(BaseConfig): diff --git a/src/memos/configs/memory.py b/src/memos/configs/memory.py index a0406d7f9..57ebf356d 100644 --- a/src/memos/configs/memory.py +++ b/src/memos/configs/memory.py @@ -5,6 +5,7 @@ from memos.configs.base import BaseConfig from memos.configs.embedder import EmbedderConfigFactory from memos.configs.graph_db import GraphDBConfigFactory +from memos.configs.internet_retriever import InternetRetrieverConfigFactory from memos.configs.llm import LLMConfigFactory from memos.configs.vec_db import VectorDBConfigFactory from memos.exceptions import ConfigurationError @@ -133,7 +134,7 @@ class GeneralTextMemoryConfig(BaseTextMemoryConfig): class TreeTextMemoryConfig(BaseTextMemoryConfig): - """General memory configuration class.""" + """Tree text memory configuration class.""" extractor_llm: LLMConfigFactory = Field( ..., @@ -155,6 +156,15 @@ class TreeTextMemoryConfig(BaseTextMemoryConfig): default_factory=GraphDBConfigFactory, description="Graph database configuration for the tree-memory storage", ) + internet_retriever: InternetRetrieverConfigFactory | None = Field( + None, + description="Internet retriever configuration (optional)", + ) + + reorganize: bool | None = Field( + False, + description="Optional description for this memory configuration.", + ) # ─── 3. Global Memory Config Factory ────────────────────────────────────────── diff --git a/src/memos/graph_dbs/item.py b/src/memos/graph_dbs/item.py new file mode 100644 index 000000000..c02c13a81 --- /dev/null +++ b/src/memos/graph_dbs/item.py @@ -0,0 +1,46 @@ +import uuid + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from memos.memories.textual.item import TextualMemoryItem + + +class GraphDBNode(TextualMemoryItem): + pass + + +class GraphDBEdge(BaseModel): + """Represents an edge in a graph database (corresponds to Neo4j relationship).""" + + id: str = Field( + default_factory=lambda: str(uuid.uuid4()), description="Unique identifier for the edge" + ) + source: str = Field(..., description="Source node ID") + target: str = Field(..., description="Target node ID") + type: Literal["RELATED", "PARENT"] = Field( + ..., description="Relationship type (must be one of 'RELATED', 'PARENT')" + ) + properties: dict[str, Any] | None = Field( + default=None, description="Additional properties for the edge" + ) + + model_config = ConfigDict(extra="forbid") + + @field_validator("id") + @classmethod + def validate_id(cls, v): + """Validate that ID is a valid UUID.""" + if not isinstance(v, str) or not uuid.UUID(v, version=4): + raise ValueError("ID must be a valid UUID string") + return v + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "GraphDBEdge": + """Create GraphDBEdge from dictionary.""" + return cls(**data) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary format.""" + return self.model_dump(exclude_none=True) diff --git a/src/memos/graph_dbs/neo4j.py b/src/memos/graph_dbs/neo4j.py index a555e36e4..58178c905 100644 --- a/src/memos/graph_dbs/neo4j.py +++ b/src/memos/graph_dbs/neo4j.py @@ -92,6 +92,16 @@ def get_memory_count(self, memory_type: str) -> int: result = session.run(query, memory_type=memory_type) return result.single()["count"] + def count_nodes(self, scope: str) -> int: + query = """ + MATCH (n:Memory) + WHERE n.memory_type = $scope + RETURN count(n) AS count + """ + with self.driver.session(database=self.db_name) as session: + result = session.run(query, {"scope": scope}).single() + return result["count"] + def remove_oldest_memory(self, memory_type: str, keep_latest: int) -> None: """ Remove all WorkingMemory nodes except the latest `keep_latest` entries. @@ -336,6 +346,49 @@ def get_neighbors( """ raise NotImplementedError + def get_neighbors_by_tag( + self, + tags: list[str], + exclude_ids: list[str], + top_k: int = 5, + min_overlap: int = 1, + ) -> list[dict[str, Any]]: + """ + Find top-K neighbor nodes with maximum tag overlap. + + Args: + tags: The list of tags to match. + exclude_ids: Node IDs to exclude (e.g., local cluster). + top_k: Max number of neighbors to return. + min_overlap: Minimum number of overlapping tags required. + + Returns: + List of dicts with node details and overlap count. + """ + query = """ + MATCH (n:Memory) + WHERE NOT n.id IN $exclude_ids + AND n.status = 'activated' + AND n.type <> 'reasoning' + AND n.memory_type <> 'WorkingMemory' + WITH n, [tag IN n.tags WHERE tag IN $tags] AS overlap_tags + WHERE size(overlap_tags) >= $min_overlap + RETURN n, size(overlap_tags) AS overlap_count + ORDER BY overlap_count DESC + LIMIT $top_k + """ + + params = { + "tags": tags, + "exclude_ids": exclude_ids, + "min_overlap": min_overlap, + "top_k": top_k, + } + + with self.driver.session(database=self.db_name) as session: + result = session.run(query, params) + return [_parse_node(dict(record["n"])) for record in result] + def get_children_with_embeddings(self, id: str) -> list[str]: query = """ MATCH (p:Memory)-[:PARENT]->(c:Memory) @@ -386,14 +439,10 @@ def get_subgraph( record = session.run(query, {"center_id": center_id}).single() if not record: - logger.warning( - f"No active node found for center_id={center_id} with status={center_status}" - ) return {"core_node": None, "neighbors": [], "edges": []} centers = record["centers"] if not centers or centers[0] is None: - logger.warning(f"Center node not found or inactive for id={center_id}") return {"core_node": None, "neighbors": [], "edges": []} core_node = _parse_node(dict(centers[0])) @@ -730,6 +779,24 @@ def get_all_memory_items(self, scope: str) -> list[dict]: results = session.run(query, {"scope": scope}) return [_parse_node(dict(record["n"])) for record in results] + def get_structure_optimization_candidates(self, scope: str) -> list[dict]: + """ + Find nodes that are likely candidates for structure optimization: + - Isolated nodes, nodes with empty background, or nodes with exactly one child. + - Plus: the child of any parent node that has exactly one child. + """ + query = """ + MATCH (n:Memory) + WHERE n.memory_type = $scope + AND n.status = 'activated' + AND NOT ( (n)-[:PARENT]->() OR ()-[:PARENT]->(n) ) + RETURN n.id AS id, n AS node + """ + + with self.driver.session(database=self.db_name) as session: + results = session.run(query, {"scope": scope}) + return [_parse_node({"id": record["id"], **dict(record["node"])}) for record in results] + def drop_database(self) -> None: """ Permanently delete the entire database this instance is using. diff --git a/src/memos/llms/vllm.py b/src/memos/llms/vllm.py new file mode 100644 index 000000000..03612b0b8 --- /dev/null +++ b/src/memos/llms/vllm.py @@ -0,0 +1,205 @@ +import asyncio +from typing import Optional, Dict, Any + +import torch +from transformers.cache_utils import DynamicCache + +from memos.configs.llm import VLLMLLMConfig +from memos.llms.base import BaseLLM +from memos.llms.utils import remove_thinking_tags +from memos.log import get_logger +from memos.types import MessageList + + +logger = get_logger(__name__) + + +class VLLMLLM(BaseLLM): + """ + VLLM LLM class for connecting to existing vLLM servers. + """ + + def __init__(self, config: VLLMLLMConfig): + """ + Initialize the VLLM LLM to connect to an existing vLLM server. + """ + self.config = config + + # Initialize OpenAI client for API calls + self.client = None + if hasattr(self.config, "api_key") and self.config.api_key: + import openai + self.client = openai.Client( + api_key=self.config.api_key, + base_url=getattr(self.config, "api_base", "http://localhost:8088") + ) + else: + # Create client without API key for local servers + import openai + self.client = openai.Client( + api_key="dummy", # vLLM local server doesn't require real API key + base_url=getattr(self.config, "api_base", "http://localhost:8088") + ) + + def build_vllm_kv_cache(self, messages) -> str: + """ + Build a KV cache from chat messages via one vLLM request. + Supports the following input types: + - str: Used as a system prompt. + - list[str]: Concatenated and used as a system prompt. + - list[dict]: Used directly as chat messages. + The messages are always converted to a standard chat template. + Raises: + ValueError: If the resulting prompt is empty after template processing. + Returns: + str: The constructed prompt string for vLLM KV cache building. + """ + # Accept multiple input types and convert to standard chat messages + if isinstance(messages, str): + messages = [ + { + "role": "system", + "content": f"Below is some information about the user.\n{messages}", + } + ] + elif isinstance(messages, list) and messages and isinstance(messages[0], str): + # Handle list of strings + str_messages = [str(msg) for msg in messages] + messages = [ + { + "role": "system", + "content": f"Below is some information about the user.\n{' '.join(str_messages)}", + } + ] + + # Convert messages to prompt string using the same logic as HFLLM + # Convert to MessageList format for _messages_to_prompt + if isinstance(messages, str): + message_list = [{"role": "system", "content": messages}] + elif isinstance(messages, list) and messages and isinstance(messages[0], str): + str_messages = [str(msg) for msg in messages] + message_list = [{"role": "system", "content": " ".join(str_messages)}] + else: + message_list = messages # Assume it's already in MessageList format + + # Convert to proper MessageList type + from memos.types import MessageList + typed_message_list: MessageList = [] + for msg in message_list: + if isinstance(msg, dict) and "role" in msg and "content" in msg: + typed_message_list.append({ + "role": str(msg["role"]), + "content": str(msg["content"]) + }) + + prompt = self._messages_to_prompt(typed_message_list) + + if not prompt.strip(): + raise ValueError( + "Prompt after chat template is empty, cannot build KV cache. Check your messages input." + ) + + # Send a request to vLLM server to preload the KV cache + # This is done by sending a completion request with max_tokens=0 + # which will cause vLLM to process the input but not generate any output + if self.client is not None: + # Convert messages to OpenAI format + openai_messages = [] + for msg in messages: + openai_messages.append({ + "role": msg["role"], + "content": msg["content"] + }) + + # Send prefill request to vLLM + try: + prefill_kwargs = { + "model": "default", # vLLM uses "default" as model name + "messages": openai_messages, + "max_tokens": 2, # Don't generate any tokens, just prefill + "temperature": 0.0, # Use deterministic sampling for prefill + "top_p": 1.0, + "top_k": 1, + } + prefill_response = self.client.chat.completions.create(**prefill_kwargs) + logger.info(f"vLLM KV cache prefill completed for prompt length: {len(prompt)}") + except Exception as e: + logger.warning(f"Failed to prefill vLLM KV cache: {e}") + # Continue anyway, as this is not critical for functionality + + return prompt + + def generate(self, messages: MessageList, past_key_values: Optional[DynamicCache] = None) -> str: + """ + Generate a response from the model. + Args: + messages (MessageList): Chat messages for prompt construction. + Returns: + str: Model response. + """ + if self.client is not None: + return self._generate_with_api_client(messages) + else: + raise RuntimeError("API client is not available") + + def _generate_with_api_client(self, messages: MessageList) -> str: + """ + Generate response using vLLM API client. + """ + # Convert messages to OpenAI format + openai_messages = [] + for msg in messages: + openai_messages.append({ + "role": msg["role"], + "content": msg["content"] + }) + + # Generate response + if self.client is not None: + # Create completion request with proper parameter types + completion_kwargs = { + "model": "default", # vLLM uses "default" as model name + "messages": openai_messages, + "temperature": float(getattr(self.config, "temperature", 0.8)), + "max_tokens": int(getattr(self.config, "max_tokens", 1024)), + "top_p": float(getattr(self.config, "top_p", 0.9)), + } + + # Add top_k only if it's greater than 0 + top_k = getattr(self.config, "top_k", 50) + if top_k > 0: + completion_kwargs["top_k"] = int(top_k) + + response = self.client.chat.completions.create(**completion_kwargs) + else: + raise RuntimeError("API client is not available") + + response_text = response.choices[0].message.content or "" + logger.info(f"VLLM API response: {response_text}") + + return ( + remove_thinking_tags(response_text) + if getattr(self.config, "remove_think_prefix", False) + else response_text + ) + + def _messages_to_prompt(self, messages: MessageList) -> str: + """ + Convert messages to prompt string. + """ + # Simple conversion - can be enhanced with proper chat template + prompt_parts = [] + for msg in messages: + role = msg["role"] + content = msg["content"] + + if role == "system": + prompt_parts.append(f"System: {content}") + elif role == "user": + prompt_parts.append(f"User: {content}") + elif role == "assistant": + prompt_parts.append(f"Assistant: {content}") + + return "\n".join(prompt_parts) + + diff --git a/src/memos/mem_os/main.py b/src/memos/mem_os/main.py index bfdde39bf..2c479b452 100644 --- a/src/memos/mem_os/main.py +++ b/src/memos/mem_os/main.py @@ -1,5 +1,21 @@ +import concurrent.futures +import json + +from typing import Any + from memos.configs.mem_os import MOSConfig +from memos.llms.factory import LLMFactory +from memos.log import get_logger from memos.mem_os.core import MOSCore +from memos.memories.textual.base import BaseTextMemory +from memos.templates.mos_prompts import ( + COT_DECOMPOSE_PROMPT, + PRO_MODE_WELCOME_MESSAGE, + SYNTHESIS_PROMPT, +) + + +logger = get_logger(__name__) class MOS(MOSCore): @@ -9,4 +25,479 @@ class MOS(MOSCore): """ def __init__(self, config: MOSConfig): + self.enable_cot = config.PRO_MODE + if config.PRO_MODE: + print(PRO_MODE_WELCOME_MESSAGE) + logger.info(PRO_MODE_WELCOME_MESSAGE) super().__init__(config) + + def chat(self, query: str, user_id: str | None = None) -> str: + """ + Enhanced chat method with optional CoT (Chain of Thought) enhancement. + + Args: + query (str): The user's query. + user_id (str, optional): User ID for context. + + Returns: + str: The response from the MOS. + """ + # Check if CoT enhancement is enabled (either explicitly or via PRO mode) + + if not self.enable_cot: + # Use the original chat method from core + return super().chat(query, user_id) + + # Enhanced chat with CoT decomposition + return self._chat_with_cot_enhancement(query, user_id) + + def _chat_with_cot_enhancement(self, query: str, user_id: str | None = None) -> str: + """ + Chat with CoT enhancement for complex query decomposition. + This method includes all the same validation and processing logic as the core chat method. + + Args: + query (str): The user's query. + user_id (str, optional): User ID for context. + + Returns: + str: The enhanced response. + """ + # Step 1: Perform all the same validation and setup as core chat method + target_user_id = user_id if user_id is not None else self.user_id + accessible_cubes = self.user_manager.get_user_cubes(target_user_id) + user_cube_ids = [cube.cube_id for cube in accessible_cubes] + + # Register chat history if needed + if target_user_id not in self.chat_history_manager: + self._register_chat_history(target_user_id) + + chat_history = self.chat_history_manager[target_user_id] + + try: + # Step 2: Decompose the query using CoT + logger.info(f"🔍 [CoT] Decomposing query: {query}") + decomposition_result = self.cot_decompose( + query, self.config.chat_model, target_user_id, self.chat_llm + ) + + # Check if the query is complex and needs decomposition + if not decomposition_result.get("is_complex", False): + logger.info("🔍 [CoT] Query is not complex, using standard chat") + return super().chat(query, user_id) + + sub_questions = decomposition_result.get("sub_questions", []) + logger.info(f"🔍 [CoT] Decomposed into {len(sub_questions)} sub-questions") + + # Step 3: Get search engine for sub-questions (with proper validation) + search_engine = self._get_search_engine_for_cot_with_validation(user_cube_ids) + if not search_engine: + logger.warning("🔍 [CoT] No search engine available, using standard chat") + return super().chat(query, user_id) + + # Step 4: Get answers for sub-questions + logger.info("🔍 [CoT] Getting answers for sub-questions...") + sub_questions, sub_answers = self.get_sub_answers( + sub_questions=sub_questions, + search_engine=search_engine, + llm_config=self.config.chat_model, + user_id=target_user_id, + top_k=getattr(self.config, "cot_top_k", 3), + llm=self.chat_llm, + ) + + # Step 5: Generate enhanced response using sub-answers + logger.info("🔍 [CoT] Generating enhanced response...") + enhanced_response = self._generate_enhanced_response_with_context( + original_query=query, + sub_questions=sub_questions, + sub_answers=sub_answers, + chat_history=chat_history, + user_id=target_user_id, + search_engine=search_engine, + ) + + # Step 6: Update chat history (same as core method) + chat_history.chat_history.append({"role": "user", "content": query}) + chat_history.chat_history.append({"role": "assistant", "content": enhanced_response}) + self.chat_history_manager[target_user_id] = chat_history + + # Step 7: Submit message to scheduler (same as core method) + if len(accessible_cubes) == 1: + mem_cube_id = accessible_cubes[0].cube_id + mem_cube = self.mem_cubes[mem_cube_id] + if self.enable_mem_scheduler and self.mem_scheduler is not None: + from datetime import datetime + + from memos.mem_scheduler.modules.schemas import ( + ANSWER_LABEL, + ScheduleMessageItem, + ) + + message_item = ScheduleMessageItem( + user_id=target_user_id, + mem_cube_id=mem_cube_id, + mem_cube=mem_cube, + label=ANSWER_LABEL, + content=enhanced_response, + timestamp=datetime.now(), + ) + self.mem_scheduler.submit_messages(messages=[message_item]) + + return enhanced_response + + except Exception as e: + logger.error(f"🔍 [CoT] Error in CoT enhancement: {e}") + logger.info("🔍 [CoT] Falling back to standard chat") + return super().chat(query, user_id) + + def _get_search_engine_for_cot_with_validation( + self, user_cube_ids: list[str] + ) -> BaseTextMemory | None: + """ + Get the best available search engine for CoT operations with proper validation. + + Args: + user_cube_ids (list[str]): List of cube IDs the user has access to. + + Returns: + BaseTextMemory or None: The search engine to use for CoT. + """ + if not self.mem_cubes: + return None + + # Get the first available text memory from user's accessible cubes + for mem_cube_id, mem_cube in self.mem_cubes.items(): + if mem_cube_id not in user_cube_ids: + continue + if mem_cube.text_mem: + return mem_cube.text_mem + + return None + + def _generate_enhanced_response_with_context( + self, + original_query: str, + sub_questions: list[str], + sub_answers: list[str], + chat_history: Any, + user_id: str | None = None, + search_engine: BaseTextMemory | None = None, + ) -> str: + """ + Generate an enhanced response using sub-questions and their answers, with chat context. + + Args: + original_query (str): The original user query. + sub_questions (list[str]): List of sub-questions. + sub_answers (list[str]): List of answers to sub-questions. + chat_history: The user's chat history. + user_id (str, optional): User ID for context. + + Returns: + str: The enhanced response. + """ + # Build the synthesis prompt + qa_text = "" + for i, (question, answer) in enumerate(zip(sub_questions, sub_answers, strict=False), 1): + qa_text += f"Q{i}: {question}\nA{i}: {answer}\n\n" + + # Build messages with chat history context (similar to core method) + if (search_engine is not None) and self.config.enable_textual_memory: + if self.enable_cot: + search_memories = search_engine.search( + original_query, top_k=self.config.top_k, mode="fine" + ) + else: + search_memories = search_engine.search( + original_query, top_k=self.config.top_k, mode="fast" + ) + system_prompt = self._build_system_prompt( + search_memories + ) # Use the same system prompt builder + else: + system_prompt = self._build_system_prompt() + current_messages = [ + {"role": "system", "content": system_prompt + SYNTHESIS_PROMPT.format(qa_text=qa_text)}, + *chat_history.chat_history, + { + "role": "user", + "content": original_query, + }, + ] + + # Handle activation memory if enabled (same as core method) + past_key_values = None + if self.config.enable_activation_memory: + assert self.config.chat_model.backend == "huggingface", ( + "Activation memory only used for huggingface backend." + ) + # Get accessible cubes for the user + target_user_id = user_id if user_id is not None else self.user_id + accessible_cubes = self.user_manager.get_user_cubes(target_user_id) + user_cube_ids = [cube.cube_id for cube in accessible_cubes] + + for mem_cube_id, mem_cube in self.mem_cubes.items(): + if mem_cube_id not in user_cube_ids: + continue + if mem_cube.act_mem: + kv_cache = next(iter(mem_cube.act_mem.get_all()), None) + past_key_values = ( + kv_cache.memory if (kv_cache and hasattr(kv_cache, "memory")) else None + ) + break + + try: + # Generate the enhanced response using the chat LLM with same parameters as core + if past_key_values is not None: + enhanced_response = self.chat_llm.generate( + current_messages, past_key_values=past_key_values + ) + else: + enhanced_response = self.chat_llm.generate(current_messages) + + logger.info("🔍 [CoT] Generated enhanced response") + return enhanced_response + except Exception as e: + logger.error(f"🔍 [CoT] Error generating enhanced response: {e}") + # Fallback to standard chat + return super().chat(original_query, user_id) + + @classmethod + def cot_decompose( + cls, query: str, llm_config: Any, user_id: str | None = None, llm: LLMFactory | None = None + ) -> list[str] | dict[str, Any]: + """ + Decompose a complex query into sub-questions using Chain of Thought reasoning. + + Args: + query (str): The complex query to decompose + llm_config: LLM configuration for decomposition + user_id (str, optional): User ID for context + + Returns: + Union[List[str], Dict[str, Any]]: List of decomposed sub-questions or dict with complexity analysis + """ + # Create a temporary LLM instance for decomposition + if llm is None: + llm = LLMFactory.from_config(llm_config) + + # System prompt for CoT decomposition with complexity analysis + system_prompt = COT_DECOMPOSE_PROMPT.format(query=query) + + messages = [{"role": "system", "content": system_prompt}] + + try: + response = llm.generate(messages) + # Try to parse JSON response + result = json.loads(response) + return result + except json.JSONDecodeError as e: + logger.warning(f"Failed to parse JSON response from LLM: {e}") + logger.warning(f"Raw response: {response}") + + # Try to extract JSON-like content from the response + try: + # Look for JSON-like content between curly braces + import re + + json_match = re.search(r"\{.*\}", response, re.DOTALL) + if json_match: + json_str = json_match.group(0) + result = json.loads(json_str) + return result + except Exception: + pass + + # If all parsing attempts fail, return default + return {"is_complex": False, "sub_questions": []} + except Exception as e: + logger.error(f"Unexpected error in cot_decompose: {e}") + return {"is_complex": False, "sub_questions": []} + + @classmethod + def get_sub_answers( + cls, + sub_questions: list[str] | dict[str, Any], + search_results: dict[str, Any] | None = None, + search_engine: BaseTextMemory | None = None, + llm_config: LLMFactory | None = None, + user_id: str | None = None, + top_k: int = 5, + llm: LLMFactory | None = None, + ) -> tuple[list[str], list[str]]: + """ + Get answers for sub-questions using either search results or a search engine. + + Args: + sub_questions (Union[List[str], Dict[str, Any]]): List of sub-questions from cot_decompose or dict with analysis + search_results (Dict[str, Any], optional): Search results containing relevant information + search_engine (BaseTextMemory, optional): Text memory engine for searching + llm_config (Any, optional): LLM configuration for processing (required if search_engine is provided) + user_id (str, optional): User ID for context + top_k (int): Number of top results to retrieve from search engine + + Returns: + Tuple[List[str], List[str]]: (sub_questions, sub_answers) + """ + # Extract sub-questions from decomposition result if needed + if isinstance(sub_questions, dict): + if not sub_questions.get("is_complex", False): + return [], [] + sub_questions = sub_questions.get("sub_questions", []) + + if not sub_questions: + return [], [] + + # Validate inputs + if search_results is None and search_engine is None: + raise ValueError("Either search_results or search_engine must be provided") + if llm is None: + llm = LLMFactory.from_config(llm_config) + + # Step 1: Get search results if search_engine is provided + if search_engine is not None: + search_results = cls._search_with_engine(sub_questions, search_engine, top_k) + + # Step 2: Generate answers for each sub-question using LLM in parallel + def generate_answer_for_question(question_index: int, sub_question: str) -> tuple[int, str]: + """Generate answer for a single sub-question.""" + # Extract relevant information from search results + relevant_info = [] + if search_results and search_results.get("text_mem"): + for cube_result in search_results["text_mem"]: + for memory in cube_result.get("memories", []): + relevant_info.append(memory.memory) + + # Build system prompt with memories (similar to MOSCore._build_system_prompt) + base_prompt = ( + "You are a knowledgeable and helpful AI assistant. " + "You have access to relevant information that helps you provide accurate answers. " + "Use the provided information to answer the question comprehensively. " + "If the information is not sufficient, acknowledge the limitations." + ) + + # Add memory context if available + if relevant_info: + memory_context = "\n\n## Relevant Information:\n" + for j, info in enumerate(relevant_info[:top_k], 1): # Take top 3 most relevant + memory_context += f"{j}. {info}\n" + system_prompt = base_prompt + memory_context + else: + system_prompt = ( + base_prompt + + "\n\n## Relevant Information:\nNo specific information found in memory." + ) + + # Create messages for LLM + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": sub_question}, + ] + + try: + # Generate answer using LLM + response = llm.generate(messages) + return question_index, response + except Exception as e: + logger.error(f"Failed to generate answer for sub-question '{sub_question}': {e}") + return question_index, f"Unable to generate answer for: {sub_question}" + + # Generate answers in parallel while maintaining order + sub_answers = [None] * len(sub_questions) + with concurrent.futures.ThreadPoolExecutor( + max_workers=min(len(sub_questions), 10) + ) as executor: + # Submit all answer generation tasks + future_to_index = { + executor.submit(generate_answer_for_question, i, question): i + for i, question in enumerate(sub_questions) + } + + # Collect results as they complete, but store them in the correct position + for future in concurrent.futures.as_completed(future_to_index): + try: + question_index, answer = future.result() + sub_answers[question_index] = answer + except Exception as e: + question_index = future_to_index[future] + logger.error( + f"Exception occurred while generating answer for question at index {question_index}: {e}" + ) + sub_answers[question_index] = ( + f"Error generating answer for question {question_index + 1}" + ) + + return sub_questions, sub_answers + + @classmethod + def _search_with_engine( + cls, sub_questions: list[str], search_engine: BaseTextMemory, top_k: int + ) -> dict[str, Any]: + """ + Search for sub-questions using the provided search engine in parallel. + + Args: + sub_questions (List[str]): List of sub-questions to search for + search_engine (BaseTextMemory): Text memory engine for searching + top_k (int): Number of top results to retrieve + + Returns: + Dict[str, Any]: Search results in the expected format + """ + + def search_single_question(question: str) -> list[Any]: + """Search for a single question using the search engine.""" + try: + # Handle different search method signatures + if hasattr(search_engine, "search"): + # Try different parameter combinations based on the engine type + try: + # For tree_text memory + return search_engine.search(question, top_k, mode="fast") + except TypeError: + try: + # For general_text memory + return search_engine.search(question, top_k) + except TypeError: + # For naive_text memory + return search_engine.search(question, top_k) + else: + return [] + except Exception as e: + logger.error(f"Search failed for question '{question}': {e}") + return [] + + # Search in parallel while maintaining order + all_memories = [] + with concurrent.futures.ThreadPoolExecutor( + max_workers=min(len(sub_questions), 10) + ) as executor: + # Submit all search tasks and keep track of their order + future_to_index = { + executor.submit(search_single_question, question): i + for i, question in enumerate(sub_questions) + } + + # Initialize results list with None values to maintain order + results = [None] * len(sub_questions) + + # Collect results as they complete, but store them in the correct position + for future in concurrent.futures.as_completed(future_to_index): + index = future_to_index[future] + try: + memories = future.result() + results[index] = memories + except Exception as e: + logger.error( + f"Exception occurred while searching for question at index {index}: {e}" + ) + results[index] = [] + + # Combine all results in the correct order + for result in results: + if result is not None: + all_memories.extend(result) + + # Format results in the expected structure + return {"text_mem": [{"cube_id": "search_engine", "memories": all_memories}]} diff --git a/src/memos/mem_user/user_manager.py b/src/memos/mem_user/user_manager.py index 31d4dfc87..e5ca73b58 100644 --- a/src/memos/mem_user/user_manager.py +++ b/src/memos/mem_user/user_manager.py @@ -476,3 +476,13 @@ def delete_cube(self, cube_id: str) -> bool: return False finally: session.close() + + def close(self) -> None: + """Close the database engine and dispose of all connections. + + This method should be called when the UserManager is no longer needed + to ensure proper cleanup of database connections. + """ + if hasattr(self, "engine"): + self.engine.dispose() + logger.info("UserManager database connections closed") diff --git a/src/memos/memories/textual/item.py b/src/memos/memories/textual/item.py index 8da39fea4..795b8de8d 100644 --- a/src/memos/memories/textual/item.py +++ b/src/memos/memories/textual/item.py @@ -27,7 +27,9 @@ class TextualMemoryMetadata(BaseModel): default="activated", description="The status of the memory, e.g., 'activated', 'archived', 'deleted'.", ) - type: Literal["procedure", "fact", "event", "opinion", "topic"] | None = Field(default=None) + type: Literal["procedure", "fact", "event", "opinion", "topic", "reasoning"] | None = Field( + default=None + ) memory_time: str | None = Field( default=None, description='The time the memory occurred or refers to. Must be in standard `YYYY-MM-DD` format. Relative expressions such as "yesterday" or "tomorrow" are not allowed.', diff --git a/src/memos/memories/textual/tree.py b/src/memos/memories/textual/tree.py index de830380a..112d7c161 100644 --- a/src/memos/memories/textual/tree.py +++ b/src/memos/memories/textual/tree.py @@ -15,6 +15,9 @@ from memos.memories.textual.base import BaseTextMemory from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata from memos.memories.textual.tree_text_memory.organize.manager import MemoryManager +from memos.memories.textual.tree_text_memory.retrieve.internet_retriever_factory import ( + InternetRetrieverFactory, +) from memos.memories.textual.tree_text_memory.retrieve.searcher import Searcher from memos.types import MessageList @@ -32,7 +35,23 @@ def __init__(self, config: TreeTextMemoryConfig): self.dispatcher_llm: OpenAILLM | OllamaLLM = LLMFactory.from_config(config.dispatcher_llm) self.embedder: OllamaEmbedder = EmbedderFactory.from_config(config.embedder) self.graph_store: Neo4jGraphDB = GraphStoreFactory.from_config(config.graph_db) - self.memory_manager: MemoryManager = MemoryManager(self.graph_store, self.embedder) + self.is_reorganize = config.reorganize + + self.memory_manager: MemoryManager = MemoryManager( + self.graph_store, self.embedder, self.extractor_llm, is_reorganize=self.is_reorganize + ) + + # Create internet retriever if configured + self.internet_retriever = None + if config.internet_retriever is not None: + self.internet_retriever = InternetRetrieverFactory.from_config( + config.internet_retriever, self.embedder + ) + logger.info( + f"Internet retriever initialized with backend: {config.internet_retriever.backend}" + ) + else: + logger.info("No internet retriever configured") def add(self, memories: list[TextualMemoryItem | dict[str, Any]]) -> None: """Add memories. @@ -66,7 +85,13 @@ def get_current_memory_size(self) -> dict[str, int]: return self.memory_manager.get_current_memory_size() def search( - self, query: str, top_k: int, info=None, mode: str = "fast", memory_type: str = "All" + self, + query: str, + top_k: int, + info=None, + mode: str = "fast", + memory_type: str = "All", + manual_close_internet: bool = False, ) -> list[TextualMemoryItem]: """Search for memories based on a query. User query -> TaskGoalParser -> MemoryPathResolver -> @@ -80,10 +105,21 @@ def search( - 'fine': Uses a more detailed search process, invoking large models for higher precision, but slower performance. memory_type (str): Type restriction for search. ['All', 'WorkingMemory', 'LongTermMemory', 'UserMemory'] + manual_close_internet (bool): If True, the internet retriever will be closed by this search, it high priority than config. Returns: list[TextualMemoryItem]: List of matching memories. """ - searcher = Searcher(self.dispatcher_llm, self.graph_store, self.embedder) + if (self.internet_retriever is not None) and manual_close_internet: + logger.warning( + "Internet retriever is init by config , but this search set manual_close_internet is True and will close it" + ) + self.internet_retriever = None + searcher = Searcher( + self.dispatcher_llm, + self.graph_store, + self.embedder, + internet_retriever=self.internet_retriever, + ) return searcher.search(query, top_k, info, mode, memory_type) def get_relevant_subgraph( diff --git a/src/memos/memories/textual/tree_text_memory/organize/conflict.py b/src/memos/memories/textual/tree_text_memory/organize/conflict.py new file mode 100644 index 000000000..0ecf1847e --- /dev/null +++ b/src/memos/memories/textual/tree_text_memory/organize/conflict.py @@ -0,0 +1,196 @@ +import json +import re + +from datetime import datetime + +from memos.embedders.base import BaseEmbedder +from memos.graph_dbs.neo4j import Neo4jGraphDB +from memos.llms.base import BaseLLM +from memos.log import get_logger +from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata +from memos.templates.tree_reorganize_prompts import ( + CONFLICT_DETECTOR_PROMPT, + CONFLICT_RESOLVER_PROMPT, +) + + +logger = get_logger(__name__) + + +class ConflictHandler: + EMBEDDING_THRESHOLD: float = 0.8 # Threshold for embedding similarity to consider conflict + + def __init__(self, graph_store: Neo4jGraphDB, llm: BaseLLM, embedder: BaseEmbedder): + self.graph_store = graph_store + self.llm = llm + self.embedder = embedder + + def detect( + self, memory: TextualMemoryItem, top_k: int = 5, scope: str | None = None + ) -> list[tuple[TextualMemoryItem, TextualMemoryItem]]: + """ + Detect conflicts by finding the most similar items in the graph database based on embedding, then use LLM to judge conflict. + Args: + memory: The memory item (should have an embedding attribute or field). + top_k: Number of top similar nodes to retrieve. + scope: Optional memory type filter. + Returns: + List of conflict pairs (each pair is a tuple: (memory, candidate)). + """ + # 1. Search for similar memories based on embedding + embedding = memory.metadata.embedding + embedding_candidates_info = self.graph_store.search_by_embedding( + embedding, top_k=top_k, scope=scope + ) + # 2. Filter based on similarity threshold + embedding_candidates_ids = [ + info["id"] + for info in embedding_candidates_info + if info["score"] >= self.EMBEDDING_THRESHOLD and info["id"] != memory.id + ] + # 3. Judge conflicts using LLM + embedding_candidates = self.graph_store.get_nodes(embedding_candidates_ids) + conflict_pairs = [] + for embedding_candidate in embedding_candidates: + embedding_candidate = TextualMemoryItem.from_dict(embedding_candidate) + prompt = [ + { + "role": "system", + "content": "You are a conflict detector for memory items.", + }, + { + "role": "user", + "content": CONFLICT_DETECTOR_PROMPT.format( + statement_1=memory.memory, + statement_2=embedding_candidate.memory, + ), + }, + ] + result = self.llm.generate(prompt).strip() + if "yes" in result.lower(): + conflict_pairs.append([memory, embedding_candidate]) + if len(conflict_pairs): + conflict_text = "\n".join( + f'"{pair[0].memory!s}" <==CONFLICT==> "{pair[1].memory!s}"' + for pair in conflict_pairs + ) + logger.warning( + f"Detected {len(conflict_pairs)} conflicts for memory {memory.id}\n {conflict_text}" + ) + return conflict_pairs + + def resolve(self, memory_a: TextualMemoryItem, memory_b: TextualMemoryItem) -> None: + """ + Resolve detected conflicts between two memory items using LLM fusion. + Args: + memory_a: The first conflicting memory item. + memory_b: The second conflicting memory item. + Returns: + A fused TextualMemoryItem representing the resolved memory. + """ + + # ———————————— 1. LLM generate fused memory ———————————— + metadata_for_resolve = ["key", "background", "confidence", "updated_at"] + metadata_1 = memory_a.metadata.model_dump_json(include=metadata_for_resolve) + metadata_2 = memory_b.metadata.model_dump_json(include=metadata_for_resolve) + prompt = [ + { + "role": "system", + "content": "", + }, + { + "role": "user", + "content": CONFLICT_RESOLVER_PROMPT.format( + statement_1=memory_a.memory, + metadata_1=metadata_1, + statement_2=memory_b.memory, + metadata_2=metadata_2, + ), + }, + ] + response = self.llm.generate(prompt).strip() + + # ———————————— 2. Parse the response ———————————— + try: + answer = re.search(r"(.*?)", response, re.DOTALL) + answer = answer.group(1).strip() + # —————— 2.1 Can't resolve conflict, hard update by comparing timestamp ———— + if len(answer) <= 10 and "no" in answer.lower(): + logger.warning( + f"Conflict between {memory_a.id} and {memory_b.id} could not be resolved. " + ) + self._hard_update(memory_a, memory_b) + # —————— 2.2 Conflict resolved, update metadata and memory ———— + else: + fixed_metadata = self._merge_metadata(answer, memory_a.metadata, memory_b.metadata) + merged_memory = TextualMemoryItem(memory=answer, metadata=fixed_metadata) + logger.info(f"Resolved result: {merged_memory}") + self._resolve_in_graph(memory_a, memory_b, merged_memory) + except json.decoder.JSONDecodeError: + logger.error(f"Failed to parse LLM response: {response}") + + def _hard_update(self, memory_a: TextualMemoryItem, memory_b: TextualMemoryItem): + """ + Hard update: compare updated_at, keep the newer one, overwrite the older one's metadata. + """ + time_a = datetime.fromisoformat(memory_a.metadata.updated_at) + time_b = datetime.fromisoformat(memory_b.metadata.updated_at) + + newer_mem = memory_a if time_a >= time_b else memory_b + older_mem = memory_b if time_a >= time_b else memory_a + + self.graph_store.delete_node(older_mem.id) + logger.warning( + f"Delete older memory {older_mem.id}: <{older_mem.memory}> due to conflict with {newer_mem.id}: <{newer_mem.memory}>" + ) + + def _resolve_in_graph( + self, + conflict_a: TextualMemoryItem, + conflict_b: TextualMemoryItem, + merged: TextualMemoryItem, + ): + edges_a = self.graph_store.get_edges(conflict_a.id, type="ANY", direction="ANY") + edges_b = self.graph_store.get_edges(conflict_b.id, type="ANY", direction="ANY") + all_edges = edges_a + edges_b + + self.graph_store.add_node( + merged.id, merged.memory, merged.metadata.model_dump(exclude_none=True) + ) + + for edge in all_edges: + new_from = merged.id if edge["from"] in (conflict_a.id, conflict_b.id) else edge["from"] + new_to = merged.id if edge["to"] in (conflict_a.id, conflict_b.id) else edge["to"] + if new_from == new_to: + continue + # Check if the edge already exists before adding + if not self.graph_store.edge_exists(new_from, new_to, edge["type"], direction="ANY"): + self.graph_store.add_edge(new_from, new_to, edge["type"]) + + self.graph_store.delete_node(conflict_a.id) + self.graph_store.delete_node(conflict_b.id) + logger.debug( + f"Remove {conflict_a.id} and {conflict_b.id}, and inherit their edges to {merged.id}." + ) + + def _merge_metadata( + self, + memory: str, + metadata_a: TreeNodeTextualMemoryMetadata, + metadata_b: TreeNodeTextualMemoryMetadata, + ) -> TreeNodeTextualMemoryMetadata: + metadata_1 = metadata_a.model_dump() + metadata_2 = metadata_b.model_dump() + merged_metadata = { + "sources": (metadata_1["sources"] or []) + (metadata_2["sources"] or []), + "embedding": self.embedder.embed([memory])[0], + "update_at": datetime.now().isoformat(), + "created_at": datetime.now().isoformat(), + } + for key in metadata_1: + if key in merged_metadata: + continue + merged_metadata[key] = ( + metadata_1[key] if metadata_1[key] is not None else metadata_2[key] + ) + return TreeNodeTextualMemoryMetadata.model_validate(merged_metadata) diff --git a/src/memos/memories/textual/tree_text_memory/organize/manager.py b/src/memos/memories/textual/tree_text_memory/organize/manager.py index f7cab032c..471bd3659 100644 --- a/src/memos/memories/textual/tree_text_memory/organize/manager.py +++ b/src/memos/memories/textual/tree_text_memory/organize/manager.py @@ -5,8 +5,13 @@ from memos.embedders.factory import OllamaEmbedder from memos.graph_dbs.neo4j import Neo4jGraphDB +from memos.llms.factory import OllamaLLM, OpenAILLM from memos.log import get_logger from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata +from memos.memories.textual.tree_text_memory.organize.reorganizer import ( + GraphStructureReorganizer, + QueueMessage, +) logger = get_logger(__name__) @@ -17,9 +22,11 @@ def __init__( self, graph_store: Neo4jGraphDB, embedder: OllamaEmbedder, + llm: OpenAILLM | OllamaLLM, memory_size: dict | None = None, threshold: float | None = 0.80, merged_threshold: float | None = 0.92, + is_reorganize: bool = False, ): self.graph_store = graph_store self.embedder = embedder @@ -36,6 +43,10 @@ def __init__( "UserMemory": 10000, } self._threshold = threshold + self.is_reorganize = is_reorganize + self.reorganizer = GraphStructureReorganizer( + graph_store, llm, embedder, is_reorganize=is_reorganize + ) self._merged_threshold = merged_threshold def add(self, memories: list[TextualMemoryItem]) -> None: @@ -155,14 +166,12 @@ def _add_to_graph_memory(self, memory: TextualMemoryItem, memory_type: str): self.graph_store.add_node( node_id, memory.memory, memory.metadata.model_dump(exclude_none=True) ) - - # Step 3: Optionally link to a summary node based on topic - if memory.metadata.tags: - parent_id = self._ensure_structure_path( - memory_type=memory_type, metadata=memory.metadata + self.reorganizer.add_message( + QueueMessage( + op="add", + after_node=[node_id], ) - if parent_id: - self.graph_store.add_edge(parent_id, node_id, "PARENT") + ) def _merge(self, source_node: TextualMemoryItem, similar_nodes: list[dict]) -> None: """ @@ -230,6 +239,18 @@ def _merge(self, source_node: TextualMemoryItem, similar_nodes: list[dict]) -> N ): self.graph_store.add_edge(merged_id, related_node["id"], type="RELATE") + # log to reorganizer before updating the graph + self.reorganizer.add_message( + QueueMessage( + op="merge", + before_node=[ + original_id, + source_node.id, + ], + after_node=[merged_id], + ) + ) + def _inherit_edges(self, from_id: str, to_id: str) -> None: """ Migrate all non-lineage edges from `from_id` to `to_id`, @@ -293,13 +314,33 @@ def _ensure_structure_path( background="", ), ) - self.graph_store.add_node( id=new_node.id, memory=new_node.memory, metadata=new_node.metadata.model_dump(exclude_none=True), ) + self.reorganizer.add_message( + QueueMessage( + op="add", + after_node=[new_node.id], + ) + ) + node_id = new_node.id # Step 3: Return this structure node ID as the parent_id return node_id + + def wait_reorganizer(self): + """ + Wait for the reorganizer to finish processing all messages. + """ + logger.debug("Waiting for reorganizer to finish processing messages...") + self.reorganizer.wait_until_current_task_done() + + def close(self): + self.wait_reorganizer() + self.reorganizer.stop() + + def __del__(self): + self.close() diff --git a/src/memos/memories/textual/tree_text_memory/organize/redundancy.py b/src/memos/memories/textual/tree_text_memory/organize/redundancy.py new file mode 100644 index 000000000..7b56a8107 --- /dev/null +++ b/src/memos/memories/textual/tree_text_memory/organize/redundancy.py @@ -0,0 +1,212 @@ +import json +import re + +from datetime import datetime + +from memos.embedders.base import BaseEmbedder +from memos.graph_dbs.neo4j import Neo4jGraphDB +from memos.llms.base import BaseLLM +from memos.log import get_logger +from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata +from memos.templates.tree_reorganize_prompts import ( + REDUNDANCY_DETECTOR_PROMPT, + REDUNDANCY_MERGE_PROMPT, + REDUNDANCY_RESOLVER_PROMPT, +) + + +logger = get_logger(__name__) + + +class RedundancyHandler: + EMBEDDING_THRESHOLD: float = 0.8 # Threshold for embedding similarity to consider redundancy + + def __init__(self, graph_store: Neo4jGraphDB, llm: BaseLLM, embedder: BaseEmbedder): + self.graph_store = graph_store + self.llm = llm + self.embedder = embedder + + def detect( + self, memory: TextualMemoryItem, top_k: int = 5, scope: str | None = None + ) -> list[tuple[TextualMemoryItem, TextualMemoryItem]]: + """ + Detect redundancy by finding the most similar items in the graph database based on embedding, then use LLM to judge conflict. + Args: + memory: The memory item (should have an embedding attribute or field). + top_k: Number of top similar nodes to retrieve. + scope: Optional memory type filter. + Returns: + List of redundancy pairs (each pair is a tuple: (memory, candidate)). + """ + # 1. Search for similar memories based on embedding + embedding = memory.metadata.embedding + embedding_candidates_info = self.graph_store.search_by_embedding( + embedding, top_k=top_k, scope=scope + ) + # 2. Filter based on similarity threshold + embedding_candidates_ids = [ + info["id"] + for info in embedding_candidates_info + if info["score"] >= self.EMBEDDING_THRESHOLD and info["id"] != memory.id + ] + # 3. Judge conflicts using LLM + embedding_candidates = self.graph_store.get_nodes(embedding_candidates_ids) + redundant_pairs = [] + for embedding_candidate in embedding_candidates: + embedding_candidate = TextualMemoryItem.from_dict(embedding_candidate) + prompt = [ + { + "role": "system", + "content": "You are a conflict detector for memory items.", + }, + { + "role": "user", + "content": REDUNDANCY_DETECTOR_PROMPT.format( + statement_1=memory.memory, + statement_2=embedding_candidate.memory, + ), + }, + ] + result = self.llm.generate(prompt).strip() + if "yes" in result.lower(): + redundant_pairs.append([memory, embedding_candidate]) + if len(redundant_pairs): + conflict_text = "\n".join( + f'"{pair[0].memory!s}" <==REDUNDANCY==> "{pair[1].memory!s}"' + for pair in redundant_pairs + ) + logger.warning( + f"Detected {len(redundant_pairs)} redundancies for memory {memory.id}\n {conflict_text}" + ) + return redundant_pairs + + def resolve_two_nodes(self, memory_a: TextualMemoryItem, memory_b: TextualMemoryItem) -> None: + """ + Resolve detected redundancies between two memory items using LLM fusion. + Args: + memory_a: The first conflicting memory item. + memory_b: The second conflicting memory item. + Returns: + A fused TextualMemoryItem representing the resolved memory. + """ + + # ———————————— 1. LLM generate fused memory ———————————— + metadata_for_resolve = ["key", "background", "confidence", "updated_at"] + metadata_1 = memory_a.metadata.model_dump_json(include=metadata_for_resolve) + metadata_2 = memory_b.metadata.model_dump_json(include=metadata_for_resolve) + prompt = [ + { + "role": "system", + "content": "", + }, + { + "role": "user", + "content": REDUNDANCY_RESOLVER_PROMPT.format( + statement_1=memory_a.memory, + metadata_1=metadata_1, + statement_2=memory_b.memory, + metadata_2=metadata_2, + ), + }, + ] + response = self.llm.generate(prompt).strip() + + # ———————————— 2. Parse the response ———————————— + try: + answer = re.search(r"(.*?)", response, re.DOTALL) + answer = answer.group(1).strip() + # —————— 2.1 Can't resolve conflict, hard update by comparing timestamp ———— + if len(answer) <= 10 and "no" in answer.lower(): + logger.warning( + f"Conflict between {memory_a.id} and {memory_b.id} could not be resolved. " + ) + self._hard_update(memory_a, memory_b) + # —————— 2.2 Conflict resolved, update metadata and memory ———— + else: + fixed_metadata = self._merge_metadata(answer, memory_a.metadata, memory_b.metadata) + merged_memory = TextualMemoryItem(memory=answer, metadata=fixed_metadata) + logger.info(f"Resolved result: {merged_memory}") + self._resolve_in_graph(memory_a, memory_b, merged_memory) + except json.decoder.JSONDecodeError: + logger.error(f"Failed to parse LLM response: {response}") + + def resolve_one_node(self, memory: TextualMemoryItem) -> None: + prompt = [ + { + "role": "user", + "content": REDUNDANCY_MERGE_PROMPT.format(merged_text=memory.memory), + }, + ] + response = self.llm.generate(prompt) + memory.memory = response.strip() + self.graph_store.update_node( + memory.id, + {"memory": memory.memory, **memory.metadata.model_dump(exclude_none=True)}, + ) + logger.debug(f"Merged memory: {memory.memory}") + + def _hard_update(self, memory_a: TextualMemoryItem, memory_b: TextualMemoryItem): + """ + Hard update: compare updated_at, keep the newer one, overwrite the older one's metadata. + """ + time_a = datetime.fromisoformat(memory_a.metadata.updated_at) + time_b = datetime.fromisoformat(memory_b.metadata.updated_at) + + newer_mem = memory_a if time_a >= time_b else memory_b + older_mem = memory_b if time_a >= time_b else memory_a + + self.graph_store.delete_node(older_mem.id) + logger.warning( + f"Delete older memory {older_mem.id}: <{older_mem.memory}> due to conflict with {newer_mem.id}: <{newer_mem.memory}>" + ) + + def _resolve_in_graph( + self, + conflict_a: TextualMemoryItem, + conflict_b: TextualMemoryItem, + merged: TextualMemoryItem, + ): + edges_a = self.graph_store.get_edges(conflict_a.id, type="ANY", direction="ANY") + edges_b = self.graph_store.get_edges(conflict_b.id, type="ANY", direction="ANY") + all_edges = edges_a + edges_b + + self.graph_store.add_node( + merged.id, merged.memory, merged.metadata.model_dump(exclude_none=True) + ) + + for edge in all_edges: + new_from = merged.id if edge["from"] in (conflict_a.id, conflict_b.id) else edge["from"] + new_to = merged.id if edge["to"] in (conflict_a.id, conflict_b.id) else edge["to"] + if new_from == new_to: + continue + # Check if the edge already exists before adding + if not self.graph_store.edge_exists(new_from, new_to, edge["type"], direction="ANY"): + self.graph_store.add_edge(new_from, new_to, edge["type"]) + + self.graph_store.delete_node(conflict_a.id) + self.graph_store.delete_node(conflict_b.id) + logger.debug( + f"Remove {conflict_a.id} and {conflict_b.id}, and inherit their edges to {merged.id}." + ) + + def _merge_metadata( + self, + memory: str, + metadata_a: TreeNodeTextualMemoryMetadata, + metadata_b: TreeNodeTextualMemoryMetadata, + ) -> TreeNodeTextualMemoryMetadata: + metadata_1 = metadata_a.model_dump() + metadata_2 = metadata_b.model_dump() + merged_metadata = { + "sources": (metadata_1["sources"] or []) + (metadata_2["sources"] or []), + "embedding": self.embedder.embed([memory])[0], + "update_at": datetime.now().isoformat(), + "created_at": datetime.now().isoformat(), + } + for key in metadata_1: + if key in merged_metadata: + continue + merged_metadata[key] = ( + metadata_1[key] if metadata_1[key] is not None else metadata_2[key] + ) + return TreeNodeTextualMemoryMetadata.model_validate(merged_metadata) diff --git a/src/memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py b/src/memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py new file mode 100644 index 000000000..803a73bcb --- /dev/null +++ b/src/memos/memories/textual/tree_text_memory/organize/relation_reason_detector.py @@ -0,0 +1,235 @@ +import json + +from memos.embedders.factory import OllamaEmbedder +from memos.graph_dbs.item import GraphDBNode +from memos.graph_dbs.neo4j import Neo4jGraphDB +from memos.llms.base import BaseLLM +from memos.log import get_logger +from memos.memories.textual.item import TreeNodeTextualMemoryMetadata +from memos.templates.tree_reorganize_prompts import ( + AGGREGATE_PROMPT, + INFER_FACT_PROMPT, + PAIRWISE_RELATION_PROMPT, +) + + +logger = get_logger(__name__) + + +class RelationAndReasoningDetector: + def __init__(self, graph_store: Neo4jGraphDB, llm: BaseLLM, embedder: OllamaEmbedder): + self.graph_store = graph_store + self.llm = llm + self.embedder = embedder + + def process_node(self, node: GraphDBNode, exclude_ids: list[str], top_k: int = 5): + """ + Unified pipeline for: + 1) Pairwise relations (cause, condition, conflict, relate) + 2) Inferred nodes + 3) Sequence links + 4) Aggregate concepts + """ + if node.metadata.type == "reasoning": + logger.info(f"Skip reasoning for inferred node {node.id}") + return { + "relations": [], + "inferred_nodes": [], + "sequence_links": [], + "aggregate_nodes": [], + } + + results = { + "relations": [], + "inferred_nodes": [], + "sequence_links": [], + "aggregate_nodes": [], + } + + nearest = self.graph_store.get_neighbors_by_tag( + tags=node.metadata.tags, + exclude_ids=exclude_ids, + top_k=top_k, + min_overlap=2, + ) + nearest = [GraphDBNode(**cand_data) for cand_data in nearest] + + # 1) Pairwise relations (including CAUSE/CONDITION/CONFLICT) + pairwise = self._detect_pairwise_causal_condition_relations(node, nearest) + results["relations"].extend(pairwise["relations"]) + + # 2) Inferred nodes (from causal/condition) + inferred = self._infer_fact_nodes_from_relations(pairwise) + results["inferred_nodes"].extend(inferred) + + # 3) Sequence (optional, if you have timestamps) + seq = self._detect_sequence_links(node, nearest) + results["sequence_links"].extend(seq) + + # 4) Aggregate + agg = self._detect_aggregate_node_for_group(node, nearest, min_group_size=3) + if agg: + results["aggregate_nodes"].append(agg) + + return results + + def _detect_pairwise_causal_condition_relations( + self, node: GraphDBNode, nearest_nodes: list[GraphDBNode] + ): + """ + Vector/tag search ➜ For each candidate, use LLM to decide: + - CAUSE + - CONDITION + - RELATE_TO + - CONFLICT + """ + results = {"relations": []} + + for candidate in nearest_nodes: + prompt = PAIRWISE_RELATION_PROMPT.format( + node1=node.memory, + node2=candidate.memory, + ) + response_text = self._call_llm(prompt) + relation_type = self._parse_relation_result(response_text) + if relation_type != "NONE": + results["relations"].append( + { + "source_id": node.id, + "target_id": candidate.id, + "relation_type": relation_type, + } + ) + + return results + + def _infer_fact_nodes_from_relations(self, pairwise_results: dict): + inferred_nodes = [] + for rel in pairwise_results["relations"]: + if rel["relation_type"] in ("CAUSE", "CONDITION"): + src = self.graph_store.get_node(rel["source_id"]) + tgt = self.graph_store.get_node(rel["target_id"]) + if not src or not tgt: + continue + + prompt = INFER_FACT_PROMPT.format( + source=src["memory"], target=tgt["memory"], relation_type=rel["relation_type"] + ) + response_text = self._call_llm(prompt).strip() + if not response_text: + continue + embedding = self.embedder.embed([response_text])[0] + + inferred_nodes.append( + GraphDBNode( + memory=response_text, + metadata=src["metadata"].__class__( + user_id="", + session_id="", + memory_type="LongTermMemory", + status="activated", + key=f"InferredFact:{rel['relation_type']}", + tags=["inferred"], + embedding=embedding, + usage=[], + sources=[src["id"], tgt["id"]], + background=f"Inferred from {rel['relation_type']}", + confidence=0.9, + type="reasoning", + ), + ) + ) + return inferred_nodes + + def _detect_sequence_links(self, node: GraphDBNode, nearest_nodes: list[GraphDBNode]): + """ + If node has timestamp, find other nodes to link FOLLOWS edges. + """ + results = [] + # Pseudo: find older/newer events with same tags + # TODO: add time sequence recall + neighbors = nearest_nodes + for cand in neighbors: + # Compare timestamps + if cand.metadata.updated_at < node.metadata.updated_at: + results.append({"from_id": cand.id, "to_id": node.id}) + elif cand.metadata.updated_at > node.metadata.updated_at: + results.append({"from_id": node.id, "to_id": cand.id}) + return results + + def _detect_aggregate_node_for_group( + self, node: GraphDBNode, nearest_nodes: list[GraphDBNode], min_group_size: int = 3 + ): + """ + If nodes share overlapping tags, LLM checks if they should be summarized into a new concept. + """ + if len(nearest_nodes) < min_group_size: + return None + combined_nodes = [node, *nearest_nodes] + + joined = "\n".join(f"- {n.memory}" for n in combined_nodes) + prompt = AGGREGATE_PROMPT.format(joined=joined) + response_text = self._call_llm(prompt) + response_json = self._parse_json_result(response_text) + if not response_json: + return None + summary = json.loads(response_text) + embedding = self.embedder.embed([summary["value"]])[0] + + parent_node = GraphDBNode( + memory=summary["value"], + metadata=TreeNodeTextualMemoryMetadata( + user_id="", # TODO: summarized node: no user_id + session_id="", # TODO: summarized node: no session_id + memory_type=node.metadata.memory_type, + status="activated", + key=summary["key"], + tags=summary.get("tags", []), + embedding=embedding, + usage=[], + sources=[n.id for n in nearest_nodes], + background=summary.get("background", ""), + confidence=0.99, + type="reasoning", + ), + ) + return parent_node + + def _call_llm(self, prompt: str) -> str: + messages = [{"role": "user", "content": prompt}] + try: + response = self.llm.generate(messages).strip() + logger.debug(f"[LLM Raw] {response}") + return response + except Exception as e: + logger.warning(f"[LLM Error] {e}") + return "" + + def _parse_relation_result(self, response_text: str) -> str: + relation = response_text.strip().upper() + valid = {"CAUSE", "CONDITION", "RELATE_TO", "CONFLICT", "NONE"} + if relation not in valid: + logger.warning(f"[RelationDetector] Unexpected relation: {relation}. Fallback NONE.") + return "NONE" + return relation + + def _parse_json_result(self, response_text): + try: + response_text = response_text.replace("```", "").replace("json", "") + response_json = json.loads(response_text) + return response_json + except json.JSONDecodeError: + return {} + + def _parse_relation_result(self, response_text: str) -> str: + """ + Normalize and validate the LLM relation type output. + """ + relation = response_text.strip().upper() + valid = {"CAUSE", "CONDITION", "RELATE_TO", "CONFLICT", "NONE"} + if relation not in valid: + logger.warning( + f"[RelationDetector] Unexpected relation type: {relation}. Fallback to NONE." + ) + return "NONE" + return relation diff --git a/src/memos/memories/textual/tree_text_memory/organize/reorganizer.py b/src/memos/memories/textual/tree_text_memory/organize/reorganizer.py new file mode 100644 index 000000000..3bd81da49 --- /dev/null +++ b/src/memos/memories/textual/tree_text_memory/organize/reorganizer.py @@ -0,0 +1,584 @@ +import json +import threading +import time +import traceback + +from concurrent.futures import ThreadPoolExecutor, as_completed +from queue import PriorityQueue +from typing import Literal + +import numpy as np +import schedule + +from sklearn.cluster import MiniBatchKMeans + +from memos.embedders.factory import OllamaEmbedder +from memos.graph_dbs.item import GraphDBEdge, GraphDBNode +from memos.graph_dbs.neo4j import Neo4jGraphDB +from memos.llms.base import BaseLLM +from memos.log import get_logger +from memos.memories.textual.item import TreeNodeTextualMemoryMetadata +from memos.memories.textual.tree_text_memory.organize.conflict import ConflictHandler +from memos.memories.textual.tree_text_memory.organize.redundancy import RedundancyHandler +from memos.memories.textual.tree_text_memory.organize.relation_reason_detector import ( + RelationAndReasoningDetector, +) +from memos.templates.tree_reorganize_prompts import LOCAL_SUBCLUSTER_PROMPT, REORGANIZE_PROMPT + + +logger = get_logger(__name__) + + +class QueueMessage: + def __init__( + self, + op: Literal["add", "remove", "merge", "update"], + # `str` for node and edge IDs, `GraphDBNode` and `GraphDBEdge` for actual objects + before_node: list[str] | list[GraphDBNode] | None = None, + before_edge: list[str] | list[GraphDBEdge] | None = None, + after_node: list[str] | list[GraphDBNode] | None = None, + after_edge: list[str] | list[GraphDBEdge] | None = None, + ): + self.op = op + self.before_node = before_node + self.before_edge = before_edge + self.after_node = after_node + self.after_edge = after_edge + + def __str__(self) -> str: + return f"QueueMessage(op={self.op}, before_node={self.before_node if self.before_node is None else len(self.before_node)}, after_node={self.after_node if self.after_node is None else len(self.after_node)})" + + def __lt__(self, other: "QueueMessage") -> bool: + op_priority = {"add": 2, "remove": 2, "merge": 1} + return op_priority[self.op] < op_priority[other.op] + + +class GraphStructureReorganizer: + def __init__( + self, graph_store: Neo4jGraphDB, llm: BaseLLM, embedder: OllamaEmbedder, is_reorganize: bool + ): + self.queue = PriorityQueue() # Min-heap + self.graph_store = graph_store + self.llm = llm + self.embedder = embedder + self.relation_detector = RelationAndReasoningDetector( + self.graph_store, self.llm, self.embedder + ) + self.conflict = ConflictHandler(graph_store=graph_store, llm=llm, embedder=embedder) + self.redundancy = RedundancyHandler(graph_store=graph_store, llm=llm, embedder=embedder) + + self.is_reorganize = is_reorganize + if self.is_reorganize: + # ____ 1. For queue message driven thread ___________ + self.thread = threading.Thread(target=self._run_message_consumer_loop) + self.thread.start() + # ____ 2. For periodic structure optimization _______ + self._stop_scheduler = False + self._is_optimizing = {"LongTermMemory": False, "UserMemory": False} + self.structure_optimizer_thread = threading.Thread( + target=self._run_structure_organizer_loop + ) + self.structure_optimizer_thread.start() + + def add_message(self, message: QueueMessage): + self.queue.put_nowait(message) + + def wait_until_current_task_done(self): + """ + Wait until: + 1) queue is empty + 2) any running structure optimization is done + """ + if not self.is_reorganize: + return + + if not self.queue.empty(): + self.queue.join() + logger.debug("Queue is now empty.") + + while any(self._is_optimizing.values()): + logger.debug(f"Waiting for structure optimizer to finish... {self._is_optimizing}") + time.sleep(1) + logger.debug("Structure optimizer is now idle.") + + def _run_message_consumer_loop(self): + while True: + message = self.queue.get() + if message is None: + break + + try: + if self._preprocess_message(message): + self.handle_message(message) + except Exception: + logger.error(traceback.format_exc()) + self.queue.task_done() + + def _run_structure_organizer_loop(self): + """ + Use schedule library to periodically trigger structure optimization. + This runs until the stop flag is set. + """ + schedule.every(20).seconds.do(self.optimize_structure, scope="LongTermMemory") + schedule.every(20).seconds.do(self.optimize_structure, scope="UserMemory") + + logger.info("Structure optimizer schedule started.") + while not getattr(self, "_stop_scheduler", False): + schedule.run_pending() + time.sleep(1) + + def stop(self): + """ + Stop the reorganizer thread. + """ + if not self.is_reorganize: + return + + self.add_message(None) + self.thread.join() + logger.info("Reorganize thread stopped.") + self._stop_scheduler = True + self.structure_optimizer_thread.join() + logger.info("Structure optimizer stopped.") + + def handle_message(self, message: QueueMessage): + handle_map = { + "add": self.handle_add, + "remove": self.handle_remove, + "merge": self.handle_merge, + } + handle_map[message.op](message) + logger.debug(f"message queue size: {self.queue.qsize()}") + + def handle_add(self, message: QueueMessage): + logger.debug(f"Handling add operation: {str(message)[:500]}") + assert message.before_node is None and message.before_edge is None, ( + "Before node and edge should be None for `add` operation." + ) + # ———————— 1. check for conflicts ———————— + added_node = message.after_node[0] + conflicts = self.conflict.detect(added_node, scope=added_node.metadata.memory_type) + if conflicts: + for added_node, existing_node in conflicts: + self.conflict.resolve(added_node, existing_node) + logger.info(f"Resolved conflict between {added_node.id} and {existing_node.id}.") + + # ———————— 2. check for redundancy ———————— + redundancy = self.redundancy.detect(added_node, scope=added_node.metadata.memory_type) + if redundancy: + for added_node, existing_node in redundancy: + self.redundancy.resolve_two_nodes(added_node, existing_node) + logger.info(f"Resolved redundancy between {added_node.id} and {existing_node.id}.") + + def handle_remove(self, message: QueueMessage): + logger.debug(f"Handling remove operation: {str(message)[:50]}") + + def handle_merge(self, message: QueueMessage): + after_node = message.after_node[0] + logger.debug(f"Handling merge operation: <{after_node.memory}>") + self.redundancy_resolver.resolve_one_node(after_node) + + def optimize_structure( + self, + scope: str = "LongTermMemory", + local_tree_threshold: int = 10, + min_cluster_size: int = 3, + min_group_size: int = 10, + ): + """ + Periodically reorganize the graph: + 1. Weakly partition nodes into clusters. + 2. Summarize each cluster. + 3. Create parent nodes and build local PARENT trees. + """ + if self._is_optimizing[scope]: + logger.info(f"Already optimizing for {scope}. Skipping.") + return + + if self.graph_store.count_nodes(scope) == 0: + logger.debug(f"[GraphStructureReorganize] No nodes for scope={scope}. Skip.") + return + + self._is_optimizing[scope] = True + try: + logger.debug( + f"[GraphStructureReorganize] 🔍 Starting structure optimization for scope: {scope}" + ) + + logger.debug( + f"Num of scope in self.graph_store is {self.graph_store.get_memory_count(scope)}" + ) + # Load candidate nodes + raw_nodes = self.graph_store.get_structure_optimization_candidates(scope) + nodes = [GraphDBNode(**n) for n in raw_nodes] + + if not nodes: + logger.info("[GraphStructureReorganize] No nodes to optimize. Skipping.") + return + + if len(nodes) < min_group_size: + logger.info( + f"[GraphStructureReorganize] Only {len(nodes)} candidate nodes found. Not enough to reorganize. Skipping." + ) + return + + logger.info(f"[GraphStructureReorganize] Loaded {len(nodes)} nodes.") + + # Step 2: Partition nodes + partitioned_groups = self._partition(nodes) + + logger.info( + f"[GraphStructureReorganize] Partitioned into {len(partitioned_groups)} clusters." + ) + + with ThreadPoolExecutor(max_workers=4) as executor: + futures = [] + for cluster_nodes in partitioned_groups: + futures.append( + executor.submit( + self._process_cluster_and_write, + cluster_nodes, + scope, + local_tree_threshold, + min_cluster_size, + ) + ) + + for f in as_completed(futures): + try: + f.result() + except Exception as e: + logger.warning(f"[Reorganize] Cluster processing failed: {e}") + logger.info("[GraphStructure Reorganize] Structure optimization finished.") + + finally: + self._is_optimizing[scope] = False + logger.info("[GraphStructureReorganize] Structure optimization finished.") + + def _process_cluster_and_write( + self, + cluster_nodes: list[GraphDBNode], + scope: str, + local_tree_threshold: int, + min_cluster_size: int, + ): + if len(cluster_nodes) <= min_cluster_size: + return + + if len(cluster_nodes) <= local_tree_threshold: + # Small cluster ➜ single parent + parent_node = self._summarize_cluster(cluster_nodes, scope) + self._create_parent_node(parent_node) + self._link_cluster_nodes(parent_node, cluster_nodes) + else: + # Large cluster ➜ local sub-clustering + sub_clusters = self._local_subcluster(cluster_nodes) + sub_parents = [] + + for sub_nodes in sub_clusters: + if len(sub_nodes) < min_cluster_size: + continue # Skip tiny noise + sub_parent_node = self._summarize_cluster(sub_nodes, scope) + self._create_parent_node(sub_parent_node) + self._link_cluster_nodes(sub_parent_node, sub_nodes) + sub_parents.append(sub_parent_node) + + if sub_parents: + cluster_parent_node = self._summarize_cluster(cluster_nodes, scope) + self._create_parent_node(cluster_parent_node) + for sub_parent in sub_parents: + self.graph_store.add_edge(cluster_parent_node.id, sub_parent.id, "PARENT") + + logger.info("Adding relations/reasons") + nodes_to_check = cluster_nodes + exclude_ids = [n.id for n in nodes_to_check] + + with ThreadPoolExecutor(max_workers=4) as executor: + futures = [] + for node in nodes_to_check: + futures.append( + executor.submit( + self.relation_detector.process_node, + node, + exclude_ids, + 10, # top_k + ) + ) + + for f in as_completed(futures): + results = f.result() + + # 1) Add pairwise relations + for rel in results["relations"]: + if not self.graph_store.edge_exists( + rel["source_id"], rel["target_id"], rel["relation_type"] + ): + self.graph_store.add_edge( + rel["source_id"], rel["target_id"], rel["relation_type"] + ) + + # 2) Add inferred nodes and link to sources + for inf_node in results["inferred_nodes"]: + self.graph_store.add_node( + inf_node.id, + inf_node.memory, + inf_node.metadata.model_dump(exclude_none=True), + ) + for src_id in inf_node.metadata.sources: + self.graph_store.add_edge(src_id, inf_node.id, "INFERS") + + # 3) Add sequence links + for seq in results["sequence_links"]: + if not self.graph_store.edge_exists(seq["from_id"], seq["to_id"], "FOLLOWS"): + self.graph_store.add_edge(seq["from_id"], seq["to_id"], "FOLLOWS") + + # 4) Add aggregate concept nodes + for agg_node in results["aggregate_nodes"]: + self.graph_store.add_node( + agg_node.id, + agg_node.memory, + agg_node.metadata.model_dump(exclude_none=True), + ) + for child_id in agg_node.metadata.sources: + self.graph_store.add_edge(agg_node.id, child_id, "AGGREGATES") + + logger.info("[Reorganizer] Cluster relation/reasoning done.") + + def _local_subcluster(self, cluster_nodes: list[GraphDBNode]) -> list[list[GraphDBNode]]: + """ + Use LLM to split a large cluster into semantically coherent sub-clusters. + """ + if not cluster_nodes: + return [] + + # Prepare conversation-like input: ID + key + value + scene_lines = [] + for node in cluster_nodes: + line = f"- ID: {node.id} | Key: {node.metadata.key} | Value: {node.memory}" + scene_lines.append(line) + + joined_scene = "\n".join(scene_lines) + prompt = LOCAL_SUBCLUSTER_PROMPT.format(joined_scene=joined_scene) + + messages = [{"role": "user", "content": prompt}] + response_text = self.llm.generate(messages) + response_json = self._parse_json_result(response_text) + assigned_ids = set() + result_subclusters = [] + + for cluster in response_json.get("clusters", []): + ids = [] + for nid in cluster.get("ids", []): + if nid not in assigned_ids: + ids.append(nid) + assigned_ids.add(nid) + sub_nodes = [node for node in cluster_nodes if node.id in ids] + if len(sub_nodes) >= 2: + result_subclusters.append(sub_nodes) + + return result_subclusters + + def _partition( + self, nodes: list[GraphDBNode], min_cluster_size: int = 3 + ) -> list[list[GraphDBNode]]: + """ + Partition nodes by: + 1) Frequent tags (top N & above threshold) + 2) Remaining nodes by embedding clustering (MiniBatchKMeans) + 3) Small clusters merged or assigned to 'Other' + + Args: + nodes: List of GraphDBNode + min_cluster_size: Min size to keep a cluster as-is + + Returns: + List of clusters, each as a list of GraphDBNode + """ + from collections import Counter, defaultdict + + # 1) Count all tags + tag_counter = Counter() + for node in nodes: + for tag in node.metadata.tags: + tag_counter[tag] += 1 + + # Select frequent tags + top_n_tags = {tag for tag, count in tag_counter.most_common(50)} + threshold_tags = {tag for tag, count in tag_counter.items() if count >= 50} + frequent_tags = top_n_tags | threshold_tags + + # Group nodes by tags, ensure each group is unique internally + tag_groups = defaultdict(list) + + for node in nodes: + for tag in node.metadata.tags: + if tag in frequent_tags: + tag_groups[tag].append(node) + break + + filtered_tag_clusters = [] + assigned_ids = set() + for tag, group in tag_groups.items(): + if len(group) >= min_cluster_size: + filtered_tag_clusters.append(group) + assigned_ids.update(n.id for n in group) + else: + logger.info(f"... dropped {tag} ...") + + logger.info( + f"[MixedPartition] Created {len(filtered_tag_clusters)} clusters from tags. " + f"Nodes grouped by tags: {len(assigned_ids)} / {len(nodes)}" + ) + + # 5) Remaining nodes -> embedding clustering + remaining_nodes = [n for n in nodes if n.id not in assigned_ids] + logger.info( + f"[MixedPartition] Remaining nodes for embedding clustering: {len(remaining_nodes)}" + ) + + embedding_clusters = [] + if remaining_nodes: + x = np.array([n.metadata.embedding for n in remaining_nodes if n.metadata.embedding]) + k = max(1, min(len(remaining_nodes) // min_cluster_size, 20)) + if len(x) < k: + k = len(x) + + if 1 < k <= len(x): + kmeans = MiniBatchKMeans(n_clusters=k, batch_size=256, random_state=42) + labels = kmeans.fit_predict(x) + + label_groups = defaultdict(list) + for node, label in zip(remaining_nodes, labels, strict=False): + label_groups[label].append(node) + + embedding_clusters = list(label_groups.values()) + logger.info( + f"[MixedPartition] Created {len(embedding_clusters)} clusters from embedding." + ) + else: + embedding_clusters = [remaining_nodes] + + # Merge all & handle small clusters + all_clusters = filtered_tag_clusters + embedding_clusters + + # Optional: merge tiny clusters + final_clusters = [] + small_nodes = [] + for group in all_clusters: + if len(group) < min_cluster_size: + small_nodes.extend(group) + else: + final_clusters.append(group) + + if small_nodes: + final_clusters.append(small_nodes) + logger.info(f"[MixedPartition] {len(small_nodes)} nodes assigned to 'Other' cluster.") + + logger.info(f"[MixedPartition] Total final clusters: {len(final_clusters)}") + return final_clusters + + def _summarize_cluster(self, cluster_nodes: list[GraphDBNode], scope: str) -> GraphDBNode: + """ + Generate a cluster label using LLM, based on top keys in the cluster. + """ + if not cluster_nodes: + raise ValueError("Cluster nodes cannot be empty.") + + joined_keys = "\n".join(f"- {n.metadata.key}" for n in cluster_nodes if n.metadata.key) + joined_values = "\n".join(f"- {n.memory}" for n in cluster_nodes) + joined_backgrounds = "\n".join( + f"- {n.metadata.background}" for n in cluster_nodes if n.metadata.background + ) + + # Build prompt + prompt = REORGANIZE_PROMPT.format( + joined_keys=joined_keys, + joined_values=joined_values, + joined_backgrounds=joined_backgrounds, + ) + + messages = [{"role": "user", "content": prompt}] + response_text = self.llm.generate(messages) + response_json = self._parse_json_result(response_text) + + # Extract fields + parent_key = response_json.get("key", "").strip() + parent_value = response_json.get("value", "").strip() + parent_tags = response_json.get("tags", []) + parent_background = response_json.get("background", "").strip() + + embedding = self.embedder.embed([parent_value])[0] + + parent_node = GraphDBNode( + memory=parent_value, + metadata=TreeNodeTextualMemoryMetadata( + user_id="", # TODO: summarized node: no user_id + session_id="", # TODO: summarized node: no session_id + memory_type=scope, + status="activated", + key=parent_key, + tags=parent_tags, + embedding=embedding, + usage=[], + sources=[n.id for n in cluster_nodes], + background=parent_background, + confidence=0.99, + type="topic", + ), + ) + return parent_node + + def _parse_json_result(self, response_text): + try: + response_text = response_text.replace("```", "").replace("json", "") + response_json = json.loads(response_text) + return response_json + except json.JSONDecodeError as e: + logger.warning( + f"Failed to parse LLM response as JSON: {e}\nRaw response:\n{response_text}" + ) + return {} + + def _create_parent_node(self, parent_node: GraphDBNode) -> None: + """ + Create a new parent node for the cluster. + """ + self.graph_store.add_node( + parent_node.id, + parent_node.memory, + parent_node.metadata.model_dump(exclude_none=True), + ) + + def _link_cluster_nodes(self, parent_node: GraphDBNode, child_nodes: list[GraphDBNode]): + """ + Add PARENT edges from the parent node to all nodes in the cluster. + """ + for child in child_nodes: + if not self.graph_store.edge_exists( + parent_node.id, child.id, "PARENT", direction="OUTGOING" + ): + self.graph_store.add_edge(parent_node.id, child.id, "PARENT") + + def _preprocess_message(self, message: QueueMessage) -> bool: + message = self._convert_id_to_node(message) + if None in message.after_node: + logger.debug( + f"Found non-existent node in after_node in message: {message}, skip this message." + ) + return False + return True + + def _convert_id_to_node(self, message: QueueMessage) -> QueueMessage: + """ + Convert IDs in the message.after_node to GraphDBNode objects. + """ + for i, node in enumerate(message.after_node or []): + if not isinstance(node, str): + continue + raw_node = self.graph_store.get_node(node) + if raw_node is None: + logger.debug(f"Node with ID {node} not found in the graph store.") + message.after_node[i] = None + else: + message.after_node[i] = GraphDBNode(**raw_node) + return message diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py b/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py new file mode 100644 index 000000000..de4f3646b --- /dev/null +++ b/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever.py @@ -0,0 +1,263 @@ +"""Internet retrieval module for tree text memory.""" + +import uuid + +from datetime import datetime + +import requests + +from memos.embedders.factory import OllamaEmbedder +from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata + + +class GoogleCustomSearchAPI: + """Google Custom Search API Client""" + + def __init__( + self, api_key: str, search_engine_id: str, max_results: int = 20, num_per_request: int = 10 + ): + """ + Initialize Google Custom Search API client + + Args: + api_key: Google API key + search_engine_id: Search engine ID (cx parameter) + max_results: Maximum number of results to retrieve + num_per_request: Number of results per API request + """ + self.api_key = api_key + self.search_engine_id = search_engine_id + self.max_results = max_results + self.num_per_request = min(num_per_request, 10) # Google API limits to 10 + self.base_url = "https://www.googleapis.com/customsearch/v1" + + def search(self, query: str, num_results: int | None = None, start_index: int = 1) -> dict: + """ + Execute search request + + Args: + query: Search query + num_results: Number of results to return (uses config default if None) + start_index: Starting index (default 1) + + Returns: + Dictionary containing search results + """ + if num_results is None: + num_results = self.num_per_request + + params = { + "key": self.api_key, + "cx": self.search_engine_id, + "q": query, + "num": min(num_results, self.num_per_request), + "start": start_index, + } + + try: + response = requests.get(self.base_url, params=params) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Google search request failed: {e}") + return {} + + def get_all_results(self, query: str, max_results: int | None = None) -> list[dict]: + """ + Get all search results (with pagination) + + Args: + query: Search query + max_results: Maximum number of results (uses config default if None) + + Returns: + List of all search results + """ + if max_results is None: + max_results = self.max_results + + all_results = [] + start_index = 1 + + while len(all_results) < max_results: + search_data = self.search(query, start_index=start_index) + + if not search_data or "items" not in search_data: + break + + all_results.extend(search_data["items"]) + + # Check if there are more results + if len(search_data["items"]) < self.num_per_request: + break + + start_index += self.num_per_request + + # Avoid infinite loop + if start_index > 100: + break + + return all_results[:max_results] + + +class InternetGoogleRetriever: + """Internet retriever that converts search results to TextualMemoryItem format""" + + def __init__( + self, + api_key: str, + search_engine_id: str, + embedder: OllamaEmbedder, + max_results: int = 20, + num_per_request: int = 10, + ): + """ + Initialize internet retriever + + Args: + api_key: Google API key + search_engine_id: Search engine ID + embedder: Embedder instance for generating embeddings + max_results: Maximum number of results to retrieve + num_per_request: Number of results per API request + """ + self.google_api = GoogleCustomSearchAPI( + api_key, search_engine_id, max_results=max_results, num_per_request=num_per_request + ) + self.embedder = embedder + + def retrieve_from_internet( + self, query: str, top_k: int = 10, parsed_goal=None + ) -> list[TextualMemoryItem]: + """ + Retrieve information from the internet and convert to TextualMemoryItem format + + Args: + query: Search query + top_k: Number of results to return + parsed_goal: Parsed task goal (optional) + + Returns: + List of TextualMemoryItem + """ + # Get search results + search_results = self.google_api.get_all_results(query, max_results=top_k) + + # Convert to TextualMemoryItem format + memory_items = [] + + for _, result in enumerate(search_results): + # Extract basic information + title = result.get("title", "") + snippet = result.get("snippet", "") + link = result.get("link", "") + display_link = result.get("displayLink", "") + + # Combine memory content + memory_content = f"Title: {title}\nSummary: {snippet}\nSource: {link}" + # Create metadata + metadata = TreeNodeTextualMemoryMetadata( + user_id=None, + session_id=None, + status="activated", + type="fact", # Internet search results are usually factual information + memory_time=datetime.now().strftime("%Y-%m-%d"), + source="web", + confidence=85.0, # Confidence level for internet information + entities=self._extract_entities(title, snippet), + tags=self._extract_tags(title, snippet, parsed_goal), + visibility="public", + memory_type="LongTermMemory", # Internet search results as working memory + key=title, + sources=[link] if link else [], + embedding=self.embedder.embed([memory_content])[0], # Can add embedding later + created_at=datetime.now().isoformat(), + usage=[], + background=f"Internet search result from {display_link}", + ) + + # Create TextualMemoryItem + memory_item = TextualMemoryItem( + id=str(uuid.uuid4()), memory=memory_content, metadata=metadata + ) + + memory_items.append(memory_item) + + return memory_items + + def _extract_entities(self, title: str, snippet: str) -> list[str]: + """ + Extract entities from title and snippet + + Args: + title: Title + snippet: Snippet + + Returns: + List of entities + """ + # Simple entity extraction logic, can be improved as needed + text = f"{title} {snippet}" + entities = [] + + # Extract possible organization names (with common suffixes) + org_suffixes = ["Inc", "Corp", "LLC", "Ltd", "Company", "University", "Institute"] + words = text.split() + for i, word in enumerate(words): + if word in org_suffixes and i > 0: + entities.append(f"{words[i - 1]} {word}") + + # Extract possible dates + import re + + date_pattern = r"\d{4}-\d{2}-\d{2}|\d{1,2}/\d{1,2}/\d{4}|\w+ \d{1,2}, \d{4}" + dates = re.findall(date_pattern, text) + entities.extend(dates) + + return entities[:5] # Limit number of entities + + def _extract_tags(self, title: str, snippet: str, parsed_goal=None) -> list[str]: + """ + Extract tags from title and snippet + + Args: + title: Title + snippet: Snippet + parsed_goal: Parsed task goal + + Returns: + List of tags + """ + tags = [] + + # Extract tags from parsed goal + if parsed_goal: + if hasattr(parsed_goal, "topic") and parsed_goal.topic: + tags.append(parsed_goal.topic) + if hasattr(parsed_goal, "concept") and parsed_goal.concept: + tags.append(parsed_goal.concept) + + # Extract keywords from text + text = f"{title} {snippet}".lower() + + # Simple keyword extraction + keywords = [ + "news", + "report", + "article", + "study", + "research", + "analysis", + "update", + "announcement", + "policy", + "memo", + "document", + ] + + for keyword in keywords: + if keyword in text: + tags.append(keyword) + + # Remove duplicates and limit count + return list(set(tags))[:10] diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py b/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py new file mode 100644 index 000000000..d6af5944c --- /dev/null +++ b/src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py @@ -0,0 +1,89 @@ +"""Factory for creating internet retrievers.""" + +from typing import Any, ClassVar + +from memos.configs.internet_retriever import InternetRetrieverConfigFactory +from memos.embedders.base import BaseEmbedder +from memos.memories.textual.tree_text_memory.retrieve.internet_retriever import ( + InternetGoogleRetriever, +) +from memos.memories.textual.tree_text_memory.retrieve.xinyusearch import XinyuSearchRetriever + + +class InternetRetrieverFactory: + """Factory class for creating internet retriever instances.""" + + backend_to_class: ClassVar[dict[str, Any]] = { + "google": InternetGoogleRetriever, + "bing": InternetGoogleRetriever, # TODO: Implement BingRetriever + "xinyu": XinyuSearchRetriever, + } + + @classmethod + def from_config( + cls, config_factory: InternetRetrieverConfigFactory, embedder: BaseEmbedder + ) -> InternetGoogleRetriever | None: + """ + Create internet retriever from configuration. + + Args: + config_factory: Internet retriever configuration + embedder: Embedder instance for generating embeddings + + Returns: + InternetRetriever instance or None if no configuration provided + """ + if config_factory.backend is None: + return None + + backend = config_factory.backend + if backend not in cls.backend_to_class: + raise ValueError(f"Invalid internet retriever backend: {backend}") + + retriever_class = cls.backend_to_class[backend] + config = config_factory.config + + # Create retriever with appropriate parameters + if backend == "google": + return retriever_class( + api_key=config.api_key, + search_engine_id=config.search_engine_id, + embedder=embedder, + max_results=config.max_results, + num_per_request=config.num_per_request, + ) + elif backend == "bing": + # TODO: Implement Bing retriever + return retriever_class( + api_key=config.api_key, + search_engine_id=None, # Bing doesn't use search_engine_id + embedder=embedder, + max_results=config.max_results, + num_per_request=config.num_per_request, + ) + elif backend == "xinyu": + return retriever_class( + access_key=config.api_key, # Use api_key as access_key for xinyu + search_engine_id=config.search_engine_id, + embedder=embedder, + max_results=config.max_results, + ) + else: + raise ValueError(f"Unsupported backend: {backend}") + + @classmethod + def create_google_retriever( + cls, api_key: str, search_engine_id: str, embedder: BaseEmbedder + ) -> InternetGoogleRetriever: + """ + Create Google Custom Search retriever. + + Args: + api_key: Google API key + search_engine_id: Google Custom Search Engine ID + embedder: Embedder instance + + Returns: + InternetRetriever instance + """ + return InternetGoogleRetriever(api_key, search_engine_id, embedder) diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/reasoner.py b/src/memos/memories/textual/tree_text_memory/retrieve/reasoner.py index a5790c361..35582c873 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/reasoner.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/reasoner.py @@ -34,10 +34,7 @@ def reason( """ prompt_template = Template(REASON_PROMPT) memory_detailed_str = "\n".join( - [ - f"[{m.id}] ({m.metadata.hierarchy_level}) {m.metadata.key}: {m.memory}" - for m in ranked_memories - ] + [f"[{m.id}] {m.metadata.key}: {m.memory}" for m in ranked_memories] ) prompt = prompt_template.substitute(task=query, detailed_memory_list=memory_detailed_str) diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py index 0c1509b68..9cb434b5c 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/searcher.py @@ -8,6 +8,7 @@ from memos.llms.factory import OllamaLLM, OpenAILLM from memos.memories.textual.item import SearchedTreeNodeTextualMemoryMetadata, TextualMemoryItem +from .internet_retriever_factory import InternetRetrieverFactory from .reasoner import MemoryReasoner from .recall import GraphMemoryRetriever from .reranker import MemoryReranker @@ -20,6 +21,7 @@ def __init__( dispatcher_llm: OpenAILLM | OllamaLLM, graph_store: Neo4jGraphDB, embedder: OllamaEmbedder, + internet_retriever: InternetRetrieverFactory | None = None, ): self.graph_store = graph_store self.embedder = embedder @@ -29,6 +31,9 @@ def __init__( self.reranker = MemoryReranker(dispatcher_llm, self.embedder) self.reasoner = MemoryReasoner(dispatcher_llm) + # Create internet retriever from config if provided + self.internet_retriever = internet_retriever + def search( self, query: str, top_k: int, info=None, mode: str = "fast", memory_type: str = "All" ) -> list[TextualMemoryItem]: @@ -50,7 +55,19 @@ def search( """ # Step 1: Parse task structure into topic, concept, and fact levels - parsed_goal = self.task_goal_parser.parse(query) + context = [] + if mode == "fine": + query_embedding = self.embedder.embed([query])[0] + related_node_ids = self.graph_store.search_by_embedding(query_embedding, top_k=top_k) + related_nodes = [ + self.graph_store.get_node(related_node["id"]) for related_node in related_node_ids + ] + + context = [related_node["memory"] for related_node in related_nodes] + context = list(set(context)) + + # Step 1a: Parse task structure into topic, concept, and fact levels + parsed_goal = self.task_goal_parser.parse(query, "\n".join(context)) if parsed_goal.memories: query_embedding = self.embedder.embed(list({query, *parsed_goal.memories})) @@ -114,14 +131,39 @@ def retrieve_ranked_long_term_and_user(): ) return ranked_memories - # Step 3: Parallel execution of both paths - with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + # Step 2c: Internet retrieval (Path C) + def retrieve_from_internet(): + """ + Retrieve information from the internet using Google Custom Search API. + """ + if not self.internet_retriever: + return [] + if memory_type not in ["All"]: + return [] + internet_items = self.internet_retriever.retrieve_from_internet( + query=query, top_k=top_k, parsed_goal=parsed_goal + ) + + # Convert to the format expected by reranker + ranked_memories = self.reranker.rerank( + query=query, + query_embedding=query_embedding[0], + graph_results=internet_items, + top_k=top_k * 2, + parsed_goal=parsed_goal, + ) + return ranked_memories + + # Step 3: Parallel execution of all paths + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: future_working = executor.submit(retrieve_from_working_memory) future_hybrid = executor.submit(retrieve_ranked_long_term_and_user) + future_internet = executor.submit(retrieve_from_internet) working_results = future_working.result() hybrid_results = future_hybrid.result() - searched_res = working_results + hybrid_results + internet_results = future_internet.result() + searched_res = working_results + hybrid_results + internet_results # Deduplicate by item.memory, keep higher score deduped_result = {} diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py b/src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py index 5af204f13..e14c94ade 100644 --- a/src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py +++ b/src/memos/memories/textual/tree_text_memory/retrieve/task_goal_parser.py @@ -31,7 +31,7 @@ def parse(self, task_description: str, context: str = "") -> ParsedTaskGoal: elif self.mode == "fine": if not self.llm: raise ValueError("LLM not provided for slow mode.") - return self._parse_fine(task_description) + return self._parse_fine(task_description, context) else: raise ValueError(f"Unknown mode: {self.mode}") @@ -43,11 +43,11 @@ def _parse_fast(self, task_description: str, limit_num: int = 5) -> ParsedTaskGo memories=[task_description], keys=[task_description], tags=[], goal_type="default" ) - def _parse_fine(self, query: str) -> ParsedTaskGoal: + def _parse_fine(self, query: str, context: str = "") -> ParsedTaskGoal: """ Slow mode: LLM structured parse. """ - prompt = Template(TASK_PARSE_PROMPT).substitute(task=query.strip(), context="") + prompt = Template(TASK_PARSE_PROMPT).substitute(task=query.strip(), context=context) response = self.llm.generate(messages=[{"role": "user", "content": prompt}]) return self._parse_response(response) diff --git a/src/memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py b/src/memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py new file mode 100644 index 000000000..b803dfa4c --- /dev/null +++ b/src/memos/memories/textual/tree_text_memory/retrieve/xinyusearch.py @@ -0,0 +1,335 @@ +"""Xinyu Search API retriever for tree text memory.""" + +import json +import uuid + +from datetime import datetime + +import requests + +from memos.embedders.factory import OllamaEmbedder +from memos.log import get_logger +from memos.memories.textual.item import TextualMemoryItem, TreeNodeTextualMemoryMetadata + + +logger = get_logger(__name__) + + +class XinyuSearchAPI: + """Xinyu Search API Client""" + + def __init__(self, access_key: str, search_engine_id: str, max_results: int = 20): + """ + Initialize Xinyu Search API client + + Args: + access_key: Xinyu API access key + max_results: Maximum number of results to retrieve + """ + self.access_key = access_key + self.max_results = max_results + + # API configuration + self.config = {"url": search_engine_id} + + self.headers = { + "User-Agent": "PostmanRuntime/7.39.0", + "Content-Type": "application/json", + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + "token": access_key, + } + + def query_detail(self, body: dict | None = None, detail: bool = True) -> list[dict]: + """ + Query Xinyu search API for detailed results + + Args: + body: Search parameters + detail: Whether to get detailed results + + Returns: + List of search results + """ + res = [] + try: + url = self.config["url"] + + params = json.dumps(body) + resp = requests.request("POST", url, headers=self.headers, data=params) + res = json.loads(resp.text)["results"] + + # If detail interface, return online part + if "search_type" in body: + res = res["online"] + + if not detail: + for res_i in res: + res_i["summary"] = "「SUMMARY」" + res_i.get("summary", "") + + except Exception: + import traceback + + logger.error(f"xinyu search error: {traceback.format_exc()}") + return res + + def search(self, query: str, max_results: int | None = None) -> list[dict]: + """ + Execute search request + + Args: + query: Search query + max_results: Maximum number of results to return + + Returns: + List of search results + """ + if max_results is None: + max_results = self.max_results + + body = { + "search_type": ["online"], + "online_search": { + "max_entries": max_results, + "cache_switch": False, + "baidu_field": {"switch": True, "mode": "relevance", "type": "page"}, + "bing_field": {"switch": False, "mode": "relevance", "type": "page_web"}, + "sogou_field": {"switch": False, "mode": "relevance", "type": "page"}, + }, + "request_id": "memos" + str(uuid.uuid4()), + "queries": query, + } + + return self.query_detail(body) + + +class XinyuSearchRetriever: + """Xinyu Search retriever that converts search results to TextualMemoryItem format""" + + def __init__( + self, + access_key: str, + search_engine_id: str, + embedder: OllamaEmbedder, + max_results: int = 20, + ): + """ + Initialize Xinyu search retriever + + Args: + access_key: Xinyu API access key + embedder: Embedder instance for generating embeddings + max_results: Maximum number of results to retrieve + """ + self.xinyu_api = XinyuSearchAPI(access_key, search_engine_id, max_results=max_results) + self.embedder = embedder + + def retrieve_from_internet( + self, query: str, top_k: int = 10, parsed_goal=None + ) -> list[TextualMemoryItem]: + """ + Retrieve information from Xinyu search and convert to TextualMemoryItem format + + Args: + query: Search query + top_k: Number of results to return + parsed_goal: Parsed task goal (optional) + + Returns: + List of TextualMemoryItem + """ + # Get search results + search_results = self.xinyu_api.search(query, max_results=top_k) + + # Convert to TextualMemoryItem format + memory_items = [] + + for _, result in enumerate(search_results): + # Extract basic information from Xinyu response format + title = result.get("title", "") + content = result.get("content", "") + summary = result.get("summary", "") + url = result.get("url", "") + publish_time = result.get("publish_time", "") + if publish_time: + try: + publish_time = datetime.strptime(publish_time, "%Y-%m-%d %H:%M:%S").strftime( + "%Y-%m-%d" + ) + except Exception as e: + logger.error(f"xinyu search error: {e}") + publish_time = datetime.now().strftime("%Y-%m-%d") + else: + publish_time = datetime.now().strftime("%Y-%m-%d") + source = result.get("source", "") + site = result.get("site", "") + if site: + site = site.split("|")[0] + + # Combine memory content + memory_content = ( + f"Title: {title}\nSummary: {summary}\nContent: {content[:200]}...\nSource: {url}" + ) + + # Create metadata + metadata = TreeNodeTextualMemoryMetadata( + user_id=None, + session_id=None, + status="activated", + type="fact", # Search results are usually factual information + memory_time=publish_time, + source="web", + confidence=85.0, # Confidence level for search information + entities=self._extract_entities(title, content, summary), + tags=self._extract_tags(title, content, summary, parsed_goal), + visibility="public", + memory_type="LongTermMemory", # Search results as working memory + key=title, + sources=[url] if url else [], + embedding=self.embedder.embed([memory_content])[0], + created_at=datetime.now().isoformat(), + usage=[], + background=f"Xinyu search result from {site or source}", + ) + # Create TextualMemoryItem + memory_item = TextualMemoryItem( + id=str(uuid.uuid4()), memory=memory_content, metadata=metadata + ) + + memory_items.append(memory_item) + + return memory_items + + def _extract_entities(self, title: str, content: str, summary: str) -> list[str]: + """ + Extract entities from title, content and summary + + Args: + title: Article title + content: Article content + summary: Article summary + + Returns: + List of extracted entities + """ + # Simple entity extraction - can be enhanced with NER + text = f"{title} {content} {summary}" + entities = [] + + # Extract potential entities (simple approach) + # This can be enhanced with proper NER models + words = text.split() + for word in words: + if len(word) > 2 and word[0].isupper(): + entities.append(word) + + return list(set(entities))[:10] # Limit to 10 entities + + def _extract_tags(self, title: str, content: str, summary: str, parsed_goal=None) -> list[str]: + """ + Extract tags from title, content and summary + + Args: + title: Article title + content: Article content + summary: Article summary + parsed_goal: Parsed task goal (optional) + + Returns: + List of extracted tags + """ + tags = [] + + # Add source-based tags + tags.append("xinyu_search") + tags.append("news") + + # Add content-based tags + text = f"{title} {content} {summary}".lower() + + # Simple keyword-based tagging + keywords = { + "economy": [ + "economy", + "GDP", + "growth", + "production", + "industry", + "investment", + "consumption", + "market", + "trade", + "finance", + ], + "politics": [ + "politics", + "government", + "policy", + "meeting", + "leader", + "election", + "parliament", + "ministry", + ], + "technology": [ + "technology", + "tech", + "innovation", + "digital", + "internet", + "AI", + "artificial intelligence", + "software", + "hardware", + ], + "sports": [ + "sports", + "game", + "athlete", + "olympic", + "championship", + "tournament", + "team", + "player", + ], + "culture": [ + "culture", + "education", + "art", + "history", + "literature", + "music", + "film", + "museum", + ], + "health": [ + "health", + "medical", + "pandemic", + "hospital", + "doctor", + "medicine", + "disease", + "treatment", + ], + "environment": [ + "environment", + "ecology", + "pollution", + "green", + "climate", + "sustainability", + "renewable", + ], + } + + for category, words in keywords.items(): + if any(word in text for word in words): + tags.append(category) + + # Add goal-based tags if available + if parsed_goal and hasattr(parsed_goal, "tags"): + tags.extend(parsed_goal.tags) + + return list(set(tags))[:15] # Limit to 15 tags diff --git a/src/memos/templates/mos_prompts.py b/src/memos/templates/mos_prompts.py new file mode 100644 index 000000000..7f41f7dd5 --- /dev/null +++ b/src/memos/templates/mos_prompts.py @@ -0,0 +1,63 @@ +COT_DECOMPOSE_PROMPT = """ +I am an 8-year-old student who needs help analyzing and breaking down complex questions. Your task is to help me understand whether a question is complex enough to be broken down into smaller parts. + +Requirements: +1. First, determine if the question is a decomposable problem. If it is a decomposable problem, set 'is_complex' to True. +2. If the question needs to be decomposed, break it down into 1-3 sub-questions. The number should be controlled by the model based on the complexity of the question. +3. For decomposable questions, break them down into sub-questions and put them in the 'sub_questions' list. Each sub-question should contain only one question content without any additional notes. +4. If the question is not a decomposable problem, set 'is_complex' to False and set 'sub_questions' to an empty list. +5. You must return ONLY a valid JSON object. Do not include any other text, explanations, or formatting. + +Here are some examples: + +Question: Who is the current head coach of the gymnastics team in the capital of the country that Lang Ping represents? +Answer: {{"is_complex": true, "sub_questions": ["Which country does Lang Ping represent in volleyball?", "What is the capital of this country?", "Who is the current head coach of the gymnastics team in this capital?"]}} + +Question: Which country's cultural heritage is the Great Wall? +Answer: {{"is_complex": false, "sub_questions": []}} + +Question: How did the trade relationship between Madagascar and China develop, and how does this relationship affect the market expansion of the essential oil industry on Nosy Be Island? +Answer: {{"is_complex": true, "sub_questions": ["How did the trade relationship between Madagascar and China develop?", "How does this trade relationship affect the market expansion of the essential oil industry on Nosy Be Island?"]}} + +Please analyze the following question and respond with ONLY a valid JSON object: +Question: {query} +Answer:""" + +PRO_MODE_WELCOME_MESSAGE = """ +============================================================ +🚀 MemOS PRO Mode Activated! +============================================================ +✅ Chain of Thought (CoT) enhancement is now enabled by default +✅ Complex queries will be automatically decomposed and enhanced + +🌐 To enable Internet search capabilities: + 1. Go to your cube's textual memory configuration + 2. Set the backend to 'google' in the internet_retriever section + 3. Configure the following parameters: + - api_key: Your Google Search API key + - cse_id: Your Custom Search Engine ID + - num_results: Number of search results (default: 5) + +📝 Example configuration at cube config for tree_text_memory : + internet_retriever: + backend: 'google' + config: + api_key: 'your_google_api_key_here' + cse_id: 'your_custom_search_engine_id' + num_results: 5 +details: https://github.com/memos-ai/memos/blob/main/examples/core_memories/tree_textual_w_internet_memoy.py +============================================================ +""" + +SYNTHESIS_PROMPT = """ +exclude memory information, synthesizing information from multiple sources to provide comprehensive answers. +I will give you chain of thought for sub-questions and their answers. +Sub-questions and their answers: +{qa_text} + +Please synthesize these answers into a comprehensive response that: +1. Addresses the original question completely +2. Integrates information from all sub-questions +3. Provides clear reasoning and connections +4. Is well-structured and easy to understand +5. Maintains a natural conversational tone""" diff --git a/src/memos/templates/tree_reorganize_prompts.py b/src/memos/templates/tree_reorganize_prompts.py new file mode 100644 index 000000000..8b62a2547 --- /dev/null +++ b/src/memos/templates/tree_reorganize_prompts.py @@ -0,0 +1,168 @@ +REORGANIZE_PROMPT = """You are a memory clustering and summarization expert. + +Given the following child memory items: + +Keys: +{joined_keys} + +Values: +{joined_values} + +Backgrounds: +{joined_backgrounds} + +Your task: +- Generate a single clear English `key` (5–10 words max). +- Write a detailed `value` that merges the key points into a single, complete, well-structured text. This must stand alone and convey what the user should remember. +- Provide a list of 5–10 relevant English `tags`. +- Write a short `background` note (50–100 words) covering any extra context, sources, or traceability info. + +Return valid JSON: +{{ + "key": "", + "value": "", + "tags": ["tag1", "tag2", ...], + "background": "" +}} +""" + +LOCAL_SUBCLUSTER_PROMPT = """ +You are a memory organization expert. + +You are given a cluster of memory items, each with an ID and content. +Your task is to divide these into smaller, semantically meaningful sub-clusters. + +Instructions: +- Identify natural topics by analyzing common time, place, people, and event elements. +- Each sub-cluster must reflect a coherent theme that helps retrieval. +- Each sub-cluster should have 2–10 items. Discard singletons. +- Each item ID must appear in exactly one sub-cluster. +- Return strictly valid JSON only. + +Example: If you have items about a project across multiple phases, group them by milestone, team, or event. + +Return valid JSON: +{{ + "clusters": [ + {{ + "ids": ["id1", "id2", ...], + "theme": "" + }}, + ... + ] +}} + +Memory items: +{joined_scene} +""" + +PAIRWISE_RELATION_PROMPT = """ +You are a reasoning assistant. + +Given two memory units: +- Node 1: "{node1}" +- Node 2: "{node2}" + +Your task: +- Determine their relationship ONLY if it reveals NEW usable reasoning or retrieval knowledge that is NOT already explicit in either unit. +- Focus on whether combining them adds new temporal, causal, conditional, or conflict information. + +Valid options: +- CAUSE: One clearly leads to the other. +- CONDITION: One happens only if the other condition holds. +- RELATE_TO: They are semantically related by shared people, time, place, or event, but neither causes the other. +- CONFLICT: They logically contradict each other. +- NONE: No clear useful connection. + +Example: +- Node 1: "The marketing campaign ended in June." +- Node 2: "Product sales dropped in July." +Answer: CAUSE + +Another Example: +- Node 1: "The conference was postponed to August due to the venue being unavailable." +- Node 2: "The venue was booked for a wedding in August." +Answer: CONFLICT + +Always respond with ONE word: [CAUSE | CONDITION | RELATE_TO | CONFLICT | NONE] +""" + +INFER_FACT_PROMPT = """ +You are an inference expert. + +Source Memory: "{source}" +Target Memory: "{target}" + +They are connected by a {relation_type} relation. +Derive ONE new factual statement that clearly combines them in a way that is NOT a trivial restatement. + +Requirements: +- Include relevant time, place, people, and event details if available. +- If the inference is a logical guess, explicitly use phrases like "It can be inferred that...". + +Example: +Source: "John missed the team meeting on Monday." +Target: "Important project deadlines were discussed in that meeting." +Relation: CAUSE +Inference: "It can be inferred that John may not know the new project deadlines." + +If there is NO new useful fact that combines them, reply exactly: "None" +""" + +AGGREGATE_PROMPT = """ +You are a concept summarization assistant. + +Below is a list of memory items: +{joined} + +Your task: +- Identify if they can be meaningfully grouped under a new, higher-level concept that clarifies their shared time, place, people, or event context. +- Do NOT aggregate if the overlap is trivial or obvious from each unit alone. +- If the summary involves any plausible interpretation, explicitly note it (e.g., "This suggests..."). + +Example: +Input Memories: +- "Mary organized the 2023 sustainability summit in Berlin." +- "Mary presented a keynote on renewable energy at the same summit." + +Good Aggregate: +{{ + "key": "Mary's Sustainability Summit Role", + "value": "Mary organized and spoke at the 2023 sustainability summit in Berlin, highlighting renewable energy initiatives.", + "tags": ["Mary", "summit", "Berlin", "2023"], + "background": "Combined from multiple memories about Mary's activities at the summit." +}} + +If you find NO useful higher-level concept, reply exactly: "None". +""" + +CONFLICT_DETECTOR_PROMPT = """You are given two plaintext statements. Determine if these two statements are factually contradictory. Respond with only "yes" if they contradict each other, or "no" if they do not contradict each other. Do not provide any explanation or additional text. +Statement 1: {statement_1} +Statement 2: {statement_2} +""" + +CONFLICT_RESOLVER_PROMPT = """You are given two facts that conflict with each other. You are also given some contextual metadata of them. Your task is to analyze the two facts in light of the contextual metadata and try to reconcile them into a single, consistent, non-conflicting fact. +- Don't output any explanation or additional text, just the final reconciled fact, try to be objective and remain independent of the context, don't use pronouns. +- Try to judge facts by using its time, confidence etc. +- Try to retain as much information as possible from the perspective of time. +If the conflict cannot be resolved, output No. Otherwise, output the fused, consistent fact in enclosed with tags. + +Output Example 1: +No + +Output Example 2: + ... + +Now reconcile the following two facts: +Statement 1: {statement_1} +Metadata 1: {metadata_1} +Statement 2: {statement_2} +Metadata 2: {metadata_2} +""" + +REDUNDANCY_MERGE_PROMPT = """You are given two pieces of text joined by the marker `⟵MERGED⟶`. Please carefully read both sides of the merged text. Your task is to summarize and consolidate all the factual details from both sides into a single, coherent text, without omitting any information. You must include every distinct detail mentioned in either text. Do not provide any explanation or analysis — only return the merged summary. Don't use pronouns or subjective language, just the facts as they are presented.\n{merged_text}""" + + +REDUNDANCY_DETECTOR_PROMPT = """""" + +REDUNDANCY_RESOLVER_PROMPT = """""" diff --git a/tests/mem_os/test_memos_core.py b/tests/mem_os/test_memos_core.py index fa4f92f05..3623bd900 100644 --- a/tests/mem_os/test_memos_core.py +++ b/tests/mem_os/test_memos_core.py @@ -22,7 +22,7 @@ def mock_config(): "chat_model": { "backend": "huggingface", "config": { - "model_name_or_path": "Qwen/Qwen3-1.7B", + "model_name_or_path": "hf-internal-testing/tiny-random-gpt2", "temperature": 0.1, "remove_think_prefix": True, "max_tokens": 4096, @@ -188,8 +188,10 @@ def test_mos_init_success( mock_user_manager.validate_user.assert_called_once_with("test_user") @patch("memos.mem_os.core.UserManager") - def test_mos_init_invalid_user(self, mock_user_manager_class, mock_config): + @patch("memos.mem_os.core.LLMFactory") + def test_mos_init_invalid_user(self, mock_llm_factory, mock_user_manager_class, mock_config): """Test MOS initialization with invalid user.""" + mock_llm_factory.from_config.return_value = MagicMock() mock_user_manager = MagicMock() mock_user_manager.validate_user.return_value = False mock_user_manager_class.return_value = mock_user_manager diff --git a/tests/mem_user/test_mem_user.py b/tests/mem_user/test_mem_user.py index 570d92fbe..1298e2fbf 100644 --- a/tests/mem_user/test_mem_user.py +++ b/tests/mem_user/test_mem_user.py @@ -27,15 +27,22 @@ def temp_db(self): temp_dir = tempfile.mkdtemp() db_path = os.path.join(temp_dir, "test_memos.db") yield db_path - # Cleanup - if os.path.exists(db_path): - os.remove(db_path) - os.rmdir(temp_dir) + # Cleanup - note: file cleanup is handled by user_manager fixture + try: + if os.path.exists(db_path): + os.remove(db_path) + os.rmdir(temp_dir) + except (OSError, PermissionError): + # On Windows, files might still be locked, ignore cleanup errors + pass @pytest.fixture def user_manager(self, temp_db): """Create UserManager instance with temporary database.""" - return UserManager(db_path=temp_db) + manager = UserManager(db_path=temp_db) + yield manager + # Ensure database connections are closed + manager.close() def test_initialization(self, temp_db): """Test UserManager initialization.""" @@ -63,18 +70,27 @@ class MockSettings: # Replace the settings import monkeypatch.setattr("memos.mem_user.user_manager.settings", MockSettings()) + manager = None try: manager = UserManager() expected_path = mock_memos_dir / "memos_users.db" assert manager.db_path == str(expected_path) assert os.path.exists(expected_path) finally: + # Close database connections first + if manager: + manager.close() + # Cleanup - expected_path = mock_memos_dir / "memos_users.db" - if os.path.exists(expected_path): - os.remove(expected_path) - if os.path.exists(temp_dir): - os.rmdir(temp_dir) + try: + expected_path = mock_memos_dir / "memos_users.db" + if os.path.exists(expected_path): + os.remove(expected_path) + if os.path.exists(temp_dir): + os.rmdir(temp_dir) + except (OSError, PermissionError): + # On Windows, files might still be locked, ignore cleanup errors + pass class TestUserOperations: @@ -93,7 +109,9 @@ def temp_db(self): @pytest.fixture def user_manager(self, temp_db): """Create UserManager instance with temporary database.""" - return UserManager(db_path=temp_db) + manager = UserManager(db_path=temp_db) + yield manager + manager.close() def test_create_user(self, user_manager): """Test user creation.""" @@ -239,7 +257,9 @@ def temp_db(self): @pytest.fixture def user_manager(self, temp_db): """Create UserManager instance with temporary database.""" - return UserManager(db_path=temp_db) + manager = UserManager(db_path=temp_db) + yield manager + manager.close() def test_create_cube(self, user_manager): """Test cube creation.""" @@ -264,7 +284,7 @@ def test_create_cube_with_path_and_custom_id(self, user_manager): owner_id = user_manager.create_user("cube_owner", UserRole.USER) custom_cube_id = "custom_cube_123" - cube_path = "/path/to/cube" + cube_path = str(Path("/path/to/cube")) # Use pathlib for cross-platform path handling cube_id = user_manager.create_cube( "custom_cube", owner_id, cube_path=cube_path, cube_id=custom_cube_id @@ -433,7 +453,9 @@ def temp_db(self): @pytest.fixture def user_manager(self, temp_db): """Create UserManager instance with temporary database.""" - return UserManager(db_path=temp_db) + manager = UserManager(db_path=temp_db) + yield manager + manager.close() def test_user_roles(self, user_manager): """Test different user roles.""" @@ -483,7 +505,9 @@ def temp_db(self): @pytest.fixture def user_manager(self, temp_db): """Create UserManager instance with temporary database.""" - return UserManager(db_path=temp_db) + manager = UserManager(db_path=temp_db) + yield manager + manager.close() def test_cascade_delete_user_cubes(self, user_manager): """Test that user's owned cubes are handled when user is deleted.""" diff --git a/tests/memories/textual/test_general.py b/tests/memories/textual/test_general.py index fefbe72df..7019b1216 100644 --- a/tests/memories/textual/test_general.py +++ b/tests/memories/textual/test_general.py @@ -1,5 +1,6 @@ # TODO: Overcomplex. Use pytest fixtures instead of setUp/tearDown. import json +import os import unittest import uuid @@ -455,9 +456,9 @@ def test_load(self): def test_dump(self): """Test dump functionality for GeneralTextMemory.""" - test_dir = "/test/directory" + test_dir = "test/directory" memory_filename = "textual_memory.json" - memory_file_path = test_dir + "/" + memory_filename + memory_file_path = os.path.join(test_dir, memory_filename) # Set the config's memory_filename self.config.memory_filename = memory_filename diff --git a/tests/memories/textual/test_tree_manager.py b/tests/memories/textual/test_tree_manager.py index 6232ad1a0..e885d0231 100644 --- a/tests/memories/textual/test_tree_manager.py +++ b/tests/memories/textual/test_tree_manager.py @@ -14,7 +14,13 @@ def mock_graph_store(): store.get_node.return_value = { "id": str(uuid.uuid4()), "memory": "old text", - "metadata": {"confidence": 90, "background": "", "tags": [], "sources": [], "usage": []}, + "metadata": { + "confidence": 90, + "background": "", + "tags": [], + "sources": [], + "usage": [], + }, } store.search_by_embedding.return_value = [{"id": str(uuid.uuid4()), "score": 0.95}] store.get_edges.return_value = [{"from": "from_id", "to": "to_id", "type": "RELATE"}] @@ -30,8 +36,19 @@ def mock_embedder(): @pytest.fixture -def memory_manager(mock_graph_store, mock_embedder): - return MemoryManager(graph_store=mock_graph_store, embedder=mock_embedder) +def mock_llm(): + llm = MagicMock() + llm.run.side_effect = lambda *args, **kwargs: "mock_output" + return llm + + +@pytest.fixture +def memory_manager(mock_graph_store, mock_embedder, mock_llm): + return MemoryManager( + graph_store=mock_graph_store, + embedder=mock_embedder, + llm=mock_llm, + ) def test_add_and_replace_working_memory(memory_manager): diff --git a/tests/test_docs.py b/tests/test_docs.py deleted file mode 100644 index d16ce5e7b..000000000 --- a/tests/test_docs.py +++ /dev/null @@ -1,333 +0,0 @@ -""" -Tests for docs/settings.yml configuration file. - -This module tests the validity and completeness of the documentation -configuration in settings.yml. -""" - -import os -import re -import warnings - -from pathlib import Path -from typing import Literal - -import pytest -import requests -import yaml - - -@pytest.fixture -def settings_file_path() -> Path: - """Return the path to settings.yml file.""" - return Path(__file__).parent.parent / "docs" / "settings.yml" - - -@pytest.fixture -def docs_root_path() -> Path: - """Return the path to docs directory.""" - return Path(__file__).parent.parent / "docs" - - -@pytest.fixture -def settings_data(settings_file_path: Path) -> dict: - """Load and return the settings.yml data.""" - with open(settings_file_path, encoding="utf-8") as f: - return yaml.safe_load(f) - - -def test_yml_file_format_is_valid(settings_file_path: Path): - """Test that settings.yml is a valid YAML file.""" - try: - with open(settings_file_path, encoding="utf-8") as f: - yaml.safe_load(f) - except yaml.YAMLError as e: - pytest.fail(f"settings.yml is not a valid YAML file: {e}") - - -def test_settings_yml_has_required_structure(settings_data: dict): - """Test that settings.yml has the required structure.""" - assert "nav" in settings_data, "Missing 'nav' key" - assert isinstance(settings_data["nav"], list), "'nav' should be a list" - - -def test_all_nav_files_exist_in_docs_folder(settings_data: dict, docs_root_path: Path): - """Test that all files referenced in nav exist in the docs folder.""" - nav_items = settings_data.get("nav", []) - file_paths = _extract_file_paths_from_nav(nav_items) - - missing_files = [] - for file_path in file_paths: - full_path = docs_root_path / file_path - if not full_path.exists(): - missing_files.append(file_path) - - assert not missing_files, ( - f"Files referenced in nav but not found in docs folder: {missing_files}" - ) - - -def test_all_markdown_files_are_in_nav(settings_data: dict, docs_root_path: Path): - """Test that all markdown files in docs folder are referenced in nav.""" - # Get all markdown files in docs folder and subdirectories - markdown_files = set() - for md_file in docs_root_path.rglob("*.md"): - # Get relative path from docs root - relative_path = md_file.relative_to(docs_root_path) - # Convert to forward slashes for consistency - relative_path_str = str(relative_path).replace(os.sep, "/") - markdown_files.add(relative_path_str) - - # Get all files referenced in nav - nav_items = settings_data.get("nav", []) - nav_files = _extract_file_paths_from_nav(nav_items) - - # Find markdown files not in nav - missing_in_nav = markdown_files - nav_files - - assert not missing_in_nav, ( - f"Markdown files in docs folder but not referenced in settings.yml: {missing_in_nav}" - ) - - -def test_nav_files_are_markdown_files_and_properly_named(settings_data: dict): - """Test that all files in nav are markdown files and properly named.""" - - nav_items = settings_data.get("nav", []) - file_paths = _extract_file_paths_from_nav(nav_items) - - non_markdown_files = [] - improperly_named_files = [] - pattern = re.compile(r"^[a-z0-9_\/]+\.md$") - - for file_path in file_paths: - if not file_path.endswith(".md"): - non_markdown_files.append(file_path) - elif not pattern.match(file_path): - improperly_named_files.append(file_path) - - assert not non_markdown_files, f"Non-markdown files found in nav: {non_markdown_files}" - assert not improperly_named_files, ( - "Files in nav must be lowercase, use only a-z, 0-9, underscores, and slashes, and end with .md: " - f"{improperly_named_files}" - ) - - -def test_nav_structure_has_proper_nesting(settings_data: dict): - """Test that nav structure follows proper nesting conventions.""" - nav_items = settings_data.get("nav", []) - - def validate_nav_item(item, depth=0): - """Validate individual nav item structure.""" - if isinstance(item, dict): - assert len(item) == 1, f"Nav item should have exactly one key-value pair: {item}" - key, value = next(iter(item.items())) - assert isinstance(key, str), f"Nav item key should be a string: {key}" - - if isinstance(value, str): - # File reference - assert value.endswith(".md"), f"File reference should be a markdown file: {value}" - elif isinstance(value, list): - # Nested structure - assert depth < 3, f"Nav nesting too deep (max 2 levels): {item}" - for nested_item in value: - validate_nav_item(nested_item, depth + 1) - else: - pytest.fail(f"Invalid nav item value type: {type(value)} for {value}") - else: - pytest.fail(f"Nav item should be a dictionary: {item}") - - for item in nav_items: - validate_nav_item(item) - - -def test_no_duplicate_files_in_nav(settings_data: dict): - """Test that no file is referenced multiple times in nav.""" - nav_items = settings_data.get("nav", []) - file_paths = _extract_file_paths_from_nav(nav_items) - file_paths_list = list(file_paths) - - # Check for duplicates - unique_files = set(file_paths_list) - assert len(file_paths_list) == len(unique_files), "Duplicate file references found in nav" - - -def test_nav_keys_are_descriptive(settings_data: dict): - """Test that navigation keys are descriptive and properly formatted.""" - nav_items = settings_data.get("nav", []) - - def check_nav_keys(items): - """Recursively check navigation keys.""" - problematic_keys = [] - - for item in items: - if isinstance(item, dict): - for key, value in item.items(): - # Check key formatting - if not key.strip(): - problematic_keys.append(f"Empty key: '{key}'") - elif len(key) < 2: - problematic_keys.append(f"Too short key: '{key}'") - elif key != key.strip(): - problematic_keys.append(f"Key has leading/trailing whitespace: '{key}'") - - # Recursively check nested items - if isinstance(value, list): - nested_problems = check_nav_keys(value) - problematic_keys.extend(nested_problems) - - return problematic_keys - - problematic_keys = check_nav_keys(nav_items) - assert not problematic_keys, f"Problematic navigation keys found: {problematic_keys}" - - -def test_yaml_encoding_is_utf8(settings_file_path: Path): - """Test that settings.yml uses UTF-8 encoding.""" - try: - with open(settings_file_path, encoding="utf-8") as f: - f.read() - except UnicodeDecodeError: - pytest.fail("settings.yml is not encoded in UTF-8") - - -def test_yaml_indentation_is_consistent(settings_file_path: Path): - """Test that YAML indentation is consistent (2 spaces).""" - with open(settings_file_path, encoding="utf-8") as f: - lines = f.readlines() - - indentation_errors = [] - for i, line in enumerate(lines, 1): - if line.strip() and line.startswith(" "): - # Count leading spaces - leading_spaces = len(line) - len(line.lstrip(" ")) - if leading_spaces % 2 != 0: - indentation_errors.append( - f"Line {i}: inconsistent indentation ({leading_spaces} spaces)" - ) - - assert not indentation_errors, f"YAML indentation errors: {indentation_errors}" - - -def _all_md_file_paths() -> list[str]: - project_dir = Path(__file__).parent.parent - docs_dir = project_dir / "docs" - return sorted([str(p.relative_to(project_dir)) for p in docs_dir.rglob("*.md")]) - - -def _get_links(file_path: str, mode: Literal["remote", "local"]) -> list[str]: - """Extract remote or local links from a markdown file. - - Args: - file_path (str): Path to the markdown file. - mode (Literal['remote', 'local']): Mode to extract 'remote' or 'local' links. - - Returns: - set[str]: Set of extracted links. - """ - with open(file_path, encoding="utf-8", errors="ignore") as f: - content = f.read() - - matches = re.findall( - r"(?:\!)?\[.*?\]\((?!#)(.*?)\)|" # Markdown links/images (not anchors) - r'href=["\'](?!#)(.*?)["\']|' # HTML href attributes - r"<(https?://[^>]+)>", # Direct URLs in angle brackets - content, - ) - found_links = {url for match in matches for url in match if url} - - remote_links, local_links = [], [] - for link in found_links: - if link.startswith(("http://", "https://")): - remote_links.append(link) - elif not link.startswith("mailto:"): - local_links.append(link) - return remote_links if mode == "remote" else local_links - - -@pytest.mark.parametrize("file_path", _all_md_file_paths(), ids=_all_md_file_paths()) -def test_remote_links_accessibility(file_path: str): - """Test that all remote links in markdown file are accessible""" - remote_links = _get_links(file_path, mode="remote") - print(remote_links) - for link in remote_links: - with requests.Session() as session: - session.headers.update({"User-Agent": "Mozilla/5.0 (compatible; LinkChecker/1.0)"}) - try: - # Try HEAD first (faster) - response = session.head(link, timeout=2, allow_redirects=True) - - if response.status_code >= 400: - warnings.warn( - f"❌ Link {link} in {file_path} may be broken (HEAD request failed). " - "Consider checking the link manually.", - stacklevel=2, - ) - return - except requests.RequestException: - try: - # Fallback to GET with shorter timeout - response = session.get(link, timeout=2, allow_redirects=True, stream=True) - if response.status_code >= 400: - warnings.warn( - f"❌ Link {link} in {file_path} may be broken (GET request failed). " - "Consider checking the link manually.", - stacklevel=2, - ) - return - except requests.RequestException as e: - error_msg = str(e) - if "timeout" in error_msg.lower(): - warnings.warn( - f"❌ Link {link} in {file_path} timed out. " - "Consider checking the link manually.", - stacklevel=2, - ) - return - elif "connection" in error_msg.lower(): - warnings.warn( - f"❌ Link {link} in {file_path} failed to connect. " - "Consider checking the link manually.", - stacklevel=2, - ) - return - else: - warnings.warn( - f"❌ Link {link} in {file_path} failed to connect. " - "Consider checking the link manually.", - stacklevel=2, - ) - return - return - - -def _extract_file_paths_from_nav(nav_items: list, base_path: str = "") -> set[str]: - """ - Recursively extract all file paths from navigation structure. - - Args: - nav_items: List of navigation items - base_path: Base path for relative file paths - - Returns: - Set of file paths found in navigation - """ - file_paths = set() - - for item in nav_items: - if isinstance(item, dict): - for _, value in item.items(): - if isinstance(value, str): - # Direct file reference - file_path = os.path.join(base_path, value) if base_path else value - file_paths.add(file_path) - elif isinstance(value, list): - # Nested navigation structure - nested_paths = _extract_file_paths_from_nav(value, base_path) - file_paths.update(nested_paths) - elif isinstance(item, str): - # Direct string file reference (less common) - file_path = os.path.join(base_path, item) if base_path else item - file_paths.add(file_path) - - return file_paths