Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6684b96f7 | ||
|
|
1fa0f0a157 | ||
|
|
2754f45387 | ||
|
|
ecd4d91046 | ||
|
|
a5a247b161 | ||
|
|
d47cb8d284 | ||
|
|
fa15db089d | ||
|
|
d35065c887 | ||
|
|
cdee6a4ff0 | ||
|
|
9eb4e77c75 | ||
|
|
c700d790db | ||
|
|
a90b572389 | ||
|
|
62c330e5b3 | ||
|
|
c70dc7614b | ||
|
|
e0003247c3 | ||
|
|
888ee766c5 | ||
|
|
c7e91171a0 | ||
|
|
18c870ec79 | ||
|
|
3e5f68ee90 | ||
|
|
a0cd4065d9 | ||
|
|
7c0c4a03c4 | ||
|
|
a8ace18607 | ||
|
|
df43f904d1 |
@@ -52,8 +52,12 @@ jobs:
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
|
||||
- name: Install GEOS Libraries
|
||||
run: sudo apt-get update && sudo apt-get install -y libgeos-dev
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install --only-binary=shapely shapely
|
||||
make install_all
|
||||
pip install -e ".[test]"
|
||||
pip install pinecone pinecone-text
|
||||
@@ -102,4 +106,4 @@ jobs:
|
||||
with:
|
||||
file: coverage.xml
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
+21
-15
@@ -16,18 +16,19 @@ To make a contribution, follow these steps:
|
||||
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
|
||||
|
||||
|
||||
### 📦 Package manager
|
||||
### 📦 Development Environment
|
||||
|
||||
We use `poetry` as our package manager. You can install poetry by following the instructions [here](https://python-poetry.org/docs/#installation).
|
||||
|
||||
Please DO NOT use pip or conda to install the dependencies. Instead, use poetry:
|
||||
We use `hatch` for managing development environments. To set up:
|
||||
|
||||
```bash
|
||||
make install_all
|
||||
# Activate environment for specific Python version:
|
||||
hatch shell dev_py_3_9 # Python 3.9
|
||||
hatch shell dev_py_3_10 # Python 3.10
|
||||
hatch shell dev_py_3_11 # Python 3.11
|
||||
|
||||
#activate
|
||||
|
||||
poetry shell
|
||||
# The environment will automatically install all dev dependencies
|
||||
# Run tests within the activated shell:
|
||||
make test
|
||||
```
|
||||
|
||||
### 📌 Pre-commit
|
||||
@@ -40,16 +41,21 @@ pre-commit install
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
We use `pytest` to test our code. You can run the tests by running the following command:
|
||||
We use `pytest` to test our code across multiple Python versions. You can run tests using:
|
||||
|
||||
```bash
|
||||
poetry run pytest tests
|
||||
|
||||
# or
|
||||
|
||||
# Run tests with default Python version
|
||||
make test
|
||||
|
||||
# Test specific Python versions:
|
||||
make test-py-3.9 # Python 3.9 environment
|
||||
make test-py-3.10 # Python 3.10 environment
|
||||
make test-py-3.11 # Python 3.11 environment
|
||||
|
||||
# When using hatch shells, run tests with:
|
||||
make test # After activating a shell with hatch shell test_XX
|
||||
```
|
||||
|
||||
Several packages have been removed from Poetry to make the package lighter. Therefore, it is recommended to run `make install_all` to install the remaining packages and ensure all tests pass. Make sure that all tests pass before submitting a pull request.
|
||||
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
|
||||
@@ -12,8 +12,8 @@ install:
|
||||
|
||||
install_all:
|
||||
pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
||||
google-generativeai elasticsearch opensearch-py vecs pinecone pinecone-text faiss-cpu langchain-community \
|
||||
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j rank-bm25
|
||||
google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \
|
||||
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j rank-bm25 pymochow
|
||||
|
||||
# Format code with ruff
|
||||
format:
|
||||
@@ -41,3 +41,12 @@ clean:
|
||||
|
||||
test:
|
||||
hatch run test
|
||||
|
||||
test-py-3.9:
|
||||
hatch run dev_py_3_9:test
|
||||
|
||||
test-py-3.10:
|
||||
hatch run dev_py_3_10:test
|
||||
|
||||
test-py-3.11:
|
||||
hatch run dev_py_3_11:test
|
||||
|
||||
+234
-244
@@ -8,6 +8,127 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2025-06-19" description="v0.1.109">
|
||||
|
||||
**New Features:**
|
||||
- **AgentOps:** Added AgentOps integration
|
||||
- **LM Studio:** Added response_format parameter for LM Studio configuration
|
||||
- **Examples:** Added Memory agent powered by voice (Cartesia + Agno)
|
||||
|
||||
**Improvements:**
|
||||
- **AI SDK:** Added output_format parameter
|
||||
- **Client:** Enhanced update method to support metadata
|
||||
- **Google:** Added Google Genai library support
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Build:** Fixed Build CI failure
|
||||
- **Pinecone:** Fixed pinecone for async memory
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-14" description="v0.1.108">
|
||||
|
||||
**New Features:**
|
||||
- **MongoDB:** Added MongoDB Vector Store support
|
||||
- **Client:** Added client support for summary functionality
|
||||
|
||||
**Improvements:**
|
||||
- **Pinecone:** Fixed pinecone version issues
|
||||
- **OpenSearch:** Added logger support
|
||||
- **Testing:** Added python version test environments
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-11" description="v0.1.107">
|
||||
|
||||
**Improvements:**
|
||||
- **Documentation:**
|
||||
- Updated Livekit documentation migration
|
||||
- Updated OpenMemory hosted version documentation
|
||||
- **Core:** Updated categorization flow
|
||||
- **Storage:** Fixed migration issues
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-09" description="v0.1.106">
|
||||
|
||||
**New Features:**
|
||||
- **Cloudflare:** Added Cloudflare vector store support
|
||||
- **Search:** Added threshold parameter to search functionality
|
||||
- **API:** Added wildcard character support for v2 Memory APIs
|
||||
|
||||
**Improvements:**
|
||||
- **Documentation:** Updated README docs for OpenMemory environment setup
|
||||
- **Core:** Added support for unique user IDs
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Core:** Fixed error handling exceptions
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-03" description="v0.1.104">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** Fixed GET_ALL functionality for FAISS and OpenSearch
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-02" description="v0.1.103">
|
||||
|
||||
**New Features:**
|
||||
- **LLM:** Added support for OpenAI compatible LLM providers with baseUrl configuration
|
||||
|
||||
**Improvements:**
|
||||
- **Documentation:**
|
||||
- Fixed broken links
|
||||
- Improved Graph Memory features documentation clarity
|
||||
- Updated enable_graph documentation
|
||||
- **TypeScript SDK:** Updated Google SDK peer dependency version
|
||||
- **Client:** Added async mode parameter
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-26" description="v0.1.102">
|
||||
|
||||
**New Features:**
|
||||
- **Examples:** Added Neo4j example
|
||||
- **AI SDK:** Added Google provider support
|
||||
- **OpenMemory:** Added LLM and Embedding Providers support
|
||||
|
||||
**Improvements:**
|
||||
- **Documentation:**
|
||||
- Updated memory export documentation
|
||||
- Enhanced role-based memory attribution rules documentation
|
||||
- Updated API reference and messages documentation
|
||||
- Added Mastra and Raycast documentation
|
||||
- Added NOT filter documentation for Search and GetAll V2
|
||||
- Announced Claude 4 support
|
||||
- **Core:**
|
||||
- Removed support for passing string as input in client.add()
|
||||
- Added support for sarvam-m model
|
||||
- **TypeScript SDK:** Fixed types from message interface
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Memory:** Prevented saving prompt artifacts as memory when no new facts are present
|
||||
- **OpenMemory:** Fixed typos in MCP tool description
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-15" description="v0.1.101">
|
||||
|
||||
**New Features:**
|
||||
- **Neo4j:** Added base label configuration support
|
||||
|
||||
**Improvements:**
|
||||
- **Documentation:**
|
||||
- Updated Healthcare example index
|
||||
- Enhanced collaborative task agent documentation clarity
|
||||
- Added criteria-based filtering documentation
|
||||
- **OpenMemory:** Added cURL command for easy installation
|
||||
- **Build:** Migrated to Hatch build system
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-10" description="v0.1.100">
|
||||
|
||||
**New Features:**
|
||||
@@ -288,6 +409,16 @@ mode: "wide"
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2025-06-20" description="v2.1.32">
|
||||
**Improvement :**
|
||||
- **Client:** Made `api_version` V2 as default.
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-17" description="v2.1.31">
|
||||
**Improvement :**
|
||||
- **Client:** Added param `filter_memories`.
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-06" description="v2.1.30">
|
||||
**New Features:**
|
||||
- **OSS:** Added Cloudflare support
|
||||
@@ -437,6 +568,104 @@ mode: "wide"
|
||||
|
||||
<Tab title="Platform">
|
||||
|
||||
<Update label="2025-06-19" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Rate Limiting:** Implemented comprehensive rate limiting system
|
||||
|
||||
**Improvements:**
|
||||
- **Performance:** Added performance indexes for memory stats query
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Search:** Fixed search events not respecting top-k parameter
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-18" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
|
||||
- **Playground:** Added Claude 4 support on Playground
|
||||
|
||||
**Improvements:**
|
||||
- **Memory:** Added ability to update memory metadata
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-17" description="">
|
||||
|
||||
**New Features:**
|
||||
- **UI:** New Memories Page UI design
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-16" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-13" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-11" description="">
|
||||
|
||||
**New Features:**
|
||||
- **OMM:** Added OMM Script and UI functionality
|
||||
|
||||
**Improvements:**
|
||||
- **API:** Added filters validation to semantic_search_v2 endpoint
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-09" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Intercom:** Set Intercom events for ADD and SEARCH operations
|
||||
- **OpenMemory:** Added Posthog integration and feedback functionality
|
||||
- **MCP:** New JavaScript MCP Server with feedback support
|
||||
|
||||
**Improvements:**
|
||||
- **Structured Data:** Enhanced structured data handling in memory management
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-06" description="">
|
||||
|
||||
**New Features:**
|
||||
- **OAuth:** Added Mem0 OAuth integration
|
||||
- **OMM:** Added OMM-Mem0 sync for deleted memories
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-05" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-06-02" description="">
|
||||
|
||||
**New Features:**
|
||||
- **OpenMemory Cloud:** Added OpenMemory Cloud support
|
||||
- **Structured Data:** Added 'structured_attributes' field to Memory model
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-30" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Projects:** Added version and enable_graph to project views
|
||||
- **OpenMemory:** Added Postgres support for OpenMemory
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-19" description="">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -444,254 +673,15 @@ mode: "wide"
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-17" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Graph:** Added Neo4J Graph Migration
|
||||
- **API:** Added API to set custom instructions
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-16" description="">
|
||||
|
||||
**New Features:**
|
||||
- **API:** Added Org-wide API Limit and Usage
|
||||
|
||||
**Improvements:**
|
||||
- **Database:** Added migration for "is_deleted" column
|
||||
- **Graph:** Improved graph queries
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-15" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Lambda:** Added actions to lambda
|
||||
- **Core:** Added background runs support
|
||||
- **Models:** Added o4-mini for pro users
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-10" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Integrations:** Added Intercom Events integration
|
||||
- **Billing:** Added prefilled email for payments
|
||||
- **Organizations:** Added Pro organization marking
|
||||
|
||||
**Improvements:**
|
||||
- **UI:** Fixed loading jitter for organization selection
|
||||
- **Infrastructure:** Improved production scaling
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-09" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Memory:** Fixed filters in Memory Page
|
||||
- **Deployment:** Added custom categories for on-premise
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-08" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Backend:** Updated Django settings for metrics
|
||||
- **Memory:** Added retries to memory filtering
|
||||
- **Search:** Added scoring mechanism
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-07" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Deployment:** Updated deployment scripts
|
||||
- **Testing:** Added code coverage tracking
|
||||
- **Memory:** Added background cron job for memory quality
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-06" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Models:** Added support for 4.1-mini model
|
||||
|
||||
**Improvements:**
|
||||
- **Infrastructure:** Increased instance count
|
||||
- **API:** Added V2 for Manage Entities
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-04" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Testing:** Added code coverage tracking
|
||||
- **AI:** Added Keywords AI integration
|
||||
|
||||
**Improvements:**
|
||||
- **UI:** Updated UI with tabs
|
||||
- **Database:** Added migrations for custom instructions
|
||||
- **Search:** Added criteria filtering
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-26" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Performance:** Parallelized embedding calls
|
||||
- **Monitoring:** Added timing for LLM calls
|
||||
- **Search:** Added category checking in Search V2
|
||||
- **Bug Fixes:** Fixed issues with ADD filters
|
||||
- **Graph:** Implemented new graph updates
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-25" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Memory:** Fixed memory export functionality
|
||||
- **Analytics:** Added logging for project
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-24" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Output:** Added memory_type display for ADD output
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-23" description="">
|
||||
|
||||
**New Features:**
|
||||
- **UI:** Added new Pricing Component
|
||||
- **Memory:** Implemented Long/Short term memory categorization
|
||||
- **Output:** Modified serializer to hide memory_type
|
||||
|
||||
**Documentation:**
|
||||
- Updated README for deployment
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-22" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Added timestamp to ADD call
|
||||
|
||||
**Bug Fixes:**
|
||||
- Fixed issues with coreV2
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-21" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Implemented backdating with migrations and backfilling script
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-17" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Billing:** Integrated Stripe Billing Dashboard
|
||||
- **Admin:** Added webhook creation functionality
|
||||
|
||||
**Bug Fixes:**
|
||||
- Fixed Users Page issues
|
||||
- Fixed Custom Categories
|
||||
- Fixed Table components
|
||||
- Updated Stripe configuration
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-16" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Performance:** Made Admin panel and Memory Page faster
|
||||
- **Security:** Implemented active session cancellation
|
||||
- **Analytics:** Added Stripe customer ID capture
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-12" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Memory Management:**
|
||||
- Added ability to delete memories from Project level with filters
|
||||
- Added delete memories capability on Memories Page
|
||||
- **Memory Visualization:** Released V1 Graph Memory Visualization
|
||||
- **Graph Playground:** Enabled for @mem0.ai users
|
||||
- **Notifications:** Added email alerts to organization owners when new members join
|
||||
- **Memory Export:** Added date support for filtering memory exports
|
||||
|
||||
**Improvements:**
|
||||
- **Performance:**
|
||||
- Optimized graph for better performance
|
||||
- Optimized database calls in ADD method
|
||||
- **Analytics:** Added flagging of paid users in Posthog
|
||||
- **CI/CD:** Improved CI pipeline and fixed lint issues
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-10" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Notifications:** Implemented email notifications for organization owners when new members join
|
||||
|
||||
**Improvements:**
|
||||
- **CI/CD:** Fixed Dockerfile for CI tests
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-09" description="">
|
||||
|
||||
**Improvements:**
|
||||
- **Integrations:** Updated chat model for Together Qwen
|
||||
- **Platform:** Removed older platforms
|
||||
- **Bug Fixes:** Fixed FILTER_MAPPING
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-03" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Added implicit memory capabilities
|
||||
- **API:** Improved implicit lambda and get_all v2 functionality
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-04-02" description="">
|
||||
|
||||
**New Features:**
|
||||
- **Integrations:** Added Clay integration
|
||||
|
||||
**Improvements:**
|
||||
- **Integrations:** Removed deepseek coder from Together
|
||||
- **API:** Added custom instructions for add v2
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-03-31" description="">
|
||||
|
||||
**Security:**
|
||||
- **Validation:** Added key validation in messages
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025-03-28" description="">
|
||||
- **Updated Playground Prompt**
|
||||
- **Send Email on User Addition to Org/Proj**
|
||||
- **Fix Search Entity**
|
||||
</Update>
|
||||
|
||||
<Update label="2025-03-19" description="">
|
||||
- **General Stability & Performance Improvements**
|
||||
</Update>
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Vercel AI SDK">
|
||||
|
||||
<Update label="2025-06-15" description="v1.0.6">
|
||||
**New Features:**
|
||||
- **Vercel AI SDK:** Added param `filter_memories`.
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-23" description="v1.0.5">
|
||||
**New Features:**
|
||||
- **Vercel AI SDK:** Added support for Google provider.
|
||||
|
||||
@@ -4,7 +4,11 @@ title: Gemini
|
||||
|
||||
<Snippet file="paper-release.mdx" />
|
||||
|
||||
To use Gemini model, you have to set the `GEMINI_API_KEY` environment variable. You can obtain the Gemini API key from the [Google AI Studio](https://aistudio.google.com/app/apikey)
|
||||
To use the Gemini model, set the `GEMINI_API_KEY` environment variable. You can obtain the Gemini API key from [Google AI Studio](https://aistudio.google.com/app/apikey).
|
||||
|
||||
> **Note:** As of the latest release, Mem0 uses the new `google.genai` SDK instead of the deprecated `google.generativeai`. All message formatting and model interaction now use the updated `types` module from `google.genai`.
|
||||
|
||||
> **Note:** Some Gemini models are being deprecated and will retire soon. It is recommended to migrate to the latest stable models like `"gemini-2.0-flash-001"` or `"gemini-2.0-flash-lite-001"` to ensure ongoing support and improvements.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -12,28 +16,32 @@ To use Gemini model, you have to set the `GEMINI_API_KEY` environment variable.
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "your-api-key" # used for embedding model
|
||||
os.environ["GEMINI_API_KEY"] = "your-api-key"
|
||||
os.environ["OPENAI_API_KEY"] = "your-openai-api-key" # Used for embedding model
|
||||
os.environ["GEMINI_API_KEY"] = "your-gemini-api-key"
|
||||
|
||||
config = {
|
||||
"llm": {
|
||||
"provider": "gemini",
|
||||
"config": {
|
||||
"model": "gemini-1.5-flash-latest",
|
||||
"model": "gemini-2.0-flash-001",
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 2000,
|
||||
"top_p": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about a thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I’m not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I’m not a big fan of thrillers, but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thrillers and suggest sci-fi movies instead."}
|
||||
]
|
||||
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
@@ -23,6 +23,7 @@ config = {
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 2000,
|
||||
"lmstudio_base_url": "http://localhost:1234/v1", # default LM Studio API URL
|
||||
"lmstudio_response_format": {"type": "json_schema", "json_schema": {"type": "object", "schema": {}}},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Baidu VectorDB (Mochow)
|
||||
---
|
||||
|
||||
[Baidu VectorDB](https://cloud.baidu.com/doc/VDB/index.html) is an enterprise-level distributed vector database service developed by Baidu Intelligent Cloud. It is powered by Baidu's proprietary "Mochow" vector database kernel, providing high performance, availability, and security for vector search.
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "baidu",
|
||||
"config": {
|
||||
"endpoint": "http://your-mochow-endpoint:8287",
|
||||
"account": "root",
|
||||
"api_key": "your-api-key",
|
||||
"database_name": "mem0",
|
||||
"table_name": "mem0_table",
|
||||
"embedding_model_dims": 1536,
|
||||
"metric_type": "COSINE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about a thriller movie? They can be quite engaging."},
|
||||
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Here are the available parameters for the `mochow` config:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `endpoint` | Endpoint URL for your Baidu VectorDB instance | Required |
|
||||
| `account` | Baidu VectorDB account name | `root` |
|
||||
| `api_key` | API key for accessing Baidu VectorDB | Required |
|
||||
| `database_name` | Name of the database | `mem0` |
|
||||
| `table_name` | Name of the table | `mem0_table` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `metric_type` | Distance metric for similarity search | `L2` |
|
||||
|
||||
### Distance Metrics
|
||||
|
||||
The following distance metrics are supported:
|
||||
|
||||
- `L2`: Euclidean distance (default)
|
||||
- `IP`: Inner product
|
||||
- `COSINE`: Cosine similarity
|
||||
|
||||
### Index Configuration
|
||||
|
||||
The vector index is automatically configured with the following HNSW parameters:
|
||||
|
||||
- `m`: 16 (number of connections per element)
|
||||
- `efconstruction`: 200 (size of the dynamic candidate list)
|
||||
- `auto_build`: true (automatically build index)
|
||||
- `auto_build_index_policy`: Incremental build with 10000 rows increment
|
||||
@@ -0,0 +1,49 @@
|
||||
# MongoDB
|
||||
|
||||
[MongoDB](https://www.mongodb.com/) is a versatile document database that supports vector search capabilities, allowing for efficient high-dimensional similarity searches over large datasets with robust scalability and performance.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "mongodb",
|
||||
"config": {
|
||||
"db_name": "mem0-db",
|
||||
"collection_name": "mem0-collection",
|
||||
"user": "my-user",
|
||||
"password": "my-password",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about a thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I’m not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
Here are the parameters available for configuring MongoDB:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| db_name | Name of the MongoDB database | `"mem0_db"` |
|
||||
| collection_name | Name of the MongoDB collection | `"mem0_collection"` |
|
||||
| embedding_model_dims | Dimensions of the embedding vectors | `1536` |
|
||||
| user | MongoDB user for authentication | `None` |
|
||||
| password | Password for the MongoDB user | `None` |
|
||||
| host | MongoDB host | `"localhost"` |
|
||||
| port | MongoDB port | `27017` |
|
||||
|
||||
> **Note**: `user` and `password` must either be provided together or omitted together.
|
||||
@@ -23,6 +23,7 @@ See the list of supported vector databases below.
|
||||
<Card title="Upstash Vector" href="/components/vectordbs/dbs/upstash-vector"></Card>
|
||||
<Card title="Milvus" href="/components/vectordbs/dbs/milvus"></Card>
|
||||
<Card title="Pinecone" href="/components/vectordbs/dbs/pinecone"></Card>
|
||||
<Card title="MongoDB" href="/components/vectordbs/dbs/mongodb"></Card>
|
||||
<Card title="Azure" href="/components/vectordbs/dbs/azure"></Card>
|
||||
<Card title="Redis" href="/components/vectordbs/dbs/redis"></Card>
|
||||
<Card title="Elasticsearch" href="/components/vectordbs/dbs/elasticsearch"></Card>
|
||||
|
||||
+4
-1
@@ -137,6 +137,7 @@
|
||||
"components/vectordbs/dbs/pgvector",
|
||||
"components/vectordbs/dbs/milvus",
|
||||
"components/vectordbs/dbs/pinecone",
|
||||
"components/vectordbs/dbs/mongodb",
|
||||
"components/vectordbs/dbs/azure",
|
||||
"components/vectordbs/dbs/redis",
|
||||
"components/vectordbs/dbs/elasticsearch",
|
||||
@@ -145,7 +146,8 @@
|
||||
"components/vectordbs/dbs/vertex_ai",
|
||||
"components/vectordbs/dbs/weaviate",
|
||||
"components/vectordbs/dbs/faiss",
|
||||
"components/vectordbs/dbs/langchain"
|
||||
"components/vectordbs/dbs/langchain",
|
||||
"components/vectordbs/dbs/baidu"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -235,6 +237,7 @@
|
||||
"icon": "plug",
|
||||
"pages": [
|
||||
"integrations",
|
||||
"integrations/agentops",
|
||||
"integrations/vercel-ai-sdk",
|
||||
"integrations/flowise",
|
||||
"integrations/crewai",
|
||||
|
||||
@@ -18,6 +18,23 @@ Here are the available integrations for Mem0:
|
||||
## Integrations
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="AgentOps"
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="26"
|
||||
viewBox="0 0 30 36"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M10.4659 6.47277C10.45 6.37428 10.4381 6.27986 10.4303 6.18101L10.4285 6.16388C10.4212 6.09482 10.414 6.02566 10.4106 5.95626L1.18538 21.8752C0.505422 23.0493 0.323356 24.4208 0.675227 25.7289C0.849119 26.3869 1.14971 26.9859 1.55323 27.5098C1.95675 28.0338 2.46282 28.4751 3.05175 28.8143C3.83464 29.2675 4.70856 29.5 5.59028 29.5C6.03318 29.5 6.4798 29.4408 6.91899 29.3226C8.23581 28.972 9.3349 28.1326 10.0152 26.9545L15.9268 16.749V16.7449L16.5001 15.7637L17.6431 13.7936L16.5001 11.8234L15.9309 10.8381L15.9268 10.8341L13.7836 7.13406C13.6651 6.933 13.5741 6.72418 13.5109 6.51165C13.2817 5.80223 13.3292 5.04172 13.6097 4.37599L13.8115 4.02535C14.3532 3.09155 15.31 2.53987 16.3184 2.47692C16.3738 2.46915 16.4251 2.46915 16.4804 2.46915C16.5421 2.46915 16.6038 2.47257 16.6654 2.47599L16.6822 2.47692C17.6906 2.53987 18.6474 3.09155 19.1892 4.02535L21.2216 7.52838L21.8146 8.55289L21.8421 8.60399L30.1024 22.8601C30.5174 23.5814 30.6281 24.4167 30.4148 25.2205C30.1975 26.0244 29.6832 26.6942 28.9598 27.1081C28.2364 27.5258 27.3977 27.6361 26.5911 27.4195C25.7844 27.2066 25.1123 26.6905 24.6968 25.9696L18.2119 14.7788L17.069 16.7449L22.9847 26.9545C23.6646 28.1326 24.7641 28.972 26.0809 29.3226C26.5197 29.4408 26.9626 29.5 27.4096 29.5C28.2914 29.5 29.1612 29.2675 29.9482 28.8143C31.1264 28.1367 31.9728 27.0411 32.3247 25.7289C32.6766 24.4208 32.4949 23.0493 31.8145 21.8752L21.1261 3.43034C20.7029 2.51617 20.0033 1.72011 19.0621 1.18027C18.5281 0.877027 17.9708 0.675975 17.3975 0.581189C17.3027 0.565268 17.2076 0.549717 17.1129 0.537868C17.0099 0.52602 16.9074 0.518244 16.8045 0.510469C16.6027 0.498621 16.3972 0.494548 16.1914 0.510469C16.0885 0.518244 15.9859 0.52639 15.883 0.537868C15.795 0.54887 15.7067 0.563384 15.6187 0.577852L15.5984 0.581189C15.0291 0.675605 14.4673 0.876657 13.9375 1.18027C12.9885 1.72789 12.2766 2.53579 11.8537 3.46181C11.7742 3.63473 11.707 3.81282 11.6471 3.99314C11.6361 4.02668 11.6269 4.06051 11.6177 4.09435C11.612 4.11503 11.6064 4.13579 11.6003 4.15642C11.5624 4.28601 11.5275 4.41634 11.4996 4.54853C11.4885 4.60231 11.4794 4.65668 11.4703 4.71111L11.4666 4.73329C11.4443 4.86399 11.4264 4.99543 11.4145 5.12762C11.4093 5.18686 11.4045 5.24573 11.4012 5.30534C11.3934 5.44567 11.3923 5.58637 11.3963 5.72744C11.3969 5.74403 11.3962 5.76062 11.3956 5.7772C11.3949 5.79616 11.3942 5.81512 11.3952 5.83407C11.3952 5.86184 11.3952 5.88924 11.3993 5.92071C11.3998 5.9291 11.4006 5.93736 11.4014 5.94564C11.402 5.95125 11.4026 5.95687 11.403 5.96255C11.4045 5.98181 11.4064 6.00106 11.4082 6.02031C11.4097 6.03577 11.4109 6.05122 11.4122 6.06674C11.4142 6.09134 11.4163 6.11621 11.419 6.14139L11.4428 6.32282C11.4506 6.38983 11.4625 6.46092 11.4744 6.52757C11.5063 6.68863 11.5468 6.84896 11.5936 7.0078C11.5944 7.0102 11.5949 7.0127 11.5955 7.0152C11.5958 7.01662 11.5961 7.01804 11.5965 7.01944C11.5967 7.02051 11.597 7.02157 11.5974 7.02261C11.6483 7.19293 11.7081 7.36177 11.7787 7.52838C11.8619 7.72943 11.9607 7.92641 12.0715 8.11932L12.3245 8.5566V8.56067L12.4984 8.85614L12.7199 9.24232H12.7239L12.728 9.25417L14.7802 12.7927V12.7968L14.7883 12.805V12.809L15.3576 13.7943L14.7883 14.7796L8.30344 25.9703C7.88431 26.6912 7.21216 27.2077 6.40921 27.4202C6.14019 27.4913 5.86338 27.5306 5.59474 27.5306C5.053 27.5306 4.51906 27.3888 4.04085 27.1089C3.31705 26.6953 2.79909 26.0251 2.58581 25.2213C2.36845 24.4174 2.47917 23.5821 2.89829 22.8609L11.1585 8.60473L11.186 8.56141V8.55734C11.1266 8.45478 11.0753 8.35629 11.024 8.25409C11.0105 8.22496 10.9969 8.19611 10.9834 8.16739C10.9458 8.08735 10.9086 8.00836 10.8739 7.92715C10.8718 7.92504 10.8708 7.92194 10.8698 7.91887C10.8688 7.91602 10.8679 7.91319 10.8661 7.91123V7.90346C10.8423 7.8483 10.8186 7.79311 10.7989 7.73795C10.7476 7.60799 10.7041 7.47803 10.6644 7.3477C10.6012 7.15479 10.5536 6.96152 10.518 6.76861C10.4942 6.67012 10.4786 6.5757 10.4667 6.47684C10.4667 6.47684 10.47 6.47684 10.4659 6.47277Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
}
|
||||
href="/integrations/agentops"
|
||||
>
|
||||
Monitor and analyze Mem0 operations with comprehensive AI agent analytics and LLM observability.
|
||||
</Card>
|
||||
<Card
|
||||
title="LangChain"
|
||||
icon={
|
||||
@@ -305,6 +322,7 @@ Here are the available integrations for Mem0:
|
||||
>
|
||||
Build autonomous agents with memory using Agno framework.
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Keywords AI"
|
||||
icon={
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: AgentOps
|
||||
---
|
||||
<Snippet file="paper-release.mdx" />
|
||||
|
||||
Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [AgentOps](https://agentops.ai), a comprehensive monitoring and analytics platform for AI agents. This integration enables automatic tracking and analysis of memory operations, providing insights into agent performance and memory usage patterns.
|
||||
|
||||
## Overview
|
||||
|
||||
1. Automatic monitoring of Mem0 operations and performance metrics
|
||||
2. Real-time tracking of memory add, search, and retrieval operations
|
||||
3. Analytics dashboard with memory usage patterns and insights
|
||||
4. Error tracking and debugging capabilities for memory operations
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Mem0 with AgentOps, ensure you have:
|
||||
|
||||
1. Installed the required packages:
|
||||
```bash
|
||||
pip install mem0ai agentops
|
||||
```
|
||||
|
||||
2. Valid API keys:
|
||||
- [AgentOps API Key](https://app.agentops.ai/dashboard/api-keys)
|
||||
- OpenAI API Key (for LLM operations)
|
||||
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys) (optional, for cloud operations)
|
||||
|
||||
## Basic Integration Example
|
||||
|
||||
The following example demonstrates how to integrate Mem0 with AgentOps monitoring for comprehensive memory operation tracking:
|
||||
|
||||
```python
|
||||
#Import the required libraries for local memory management with Mem0
|
||||
from mem0 import Memory, AsyncMemory
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
import agentops
|
||||
|
||||
#Set up environment variables for API keys
|
||||
os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY")
|
||||
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
#Set up the configuration for local memory storage and define sample user data.
|
||||
local_config = {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4o-mini",
|
||||
"temperature": 0.1,
|
||||
"max_tokens": 2000,
|
||||
},
|
||||
}
|
||||
}
|
||||
user_id = "alice_demo"
|
||||
agent_id = "assistant_demo"
|
||||
run_id = "session_001"
|
||||
|
||||
sample_messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about a thriller? They can be quite engaging."},
|
||||
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.",
|
||||
},
|
||||
]
|
||||
|
||||
sample_preferences = [
|
||||
"I prefer dark roast coffee over light roast",
|
||||
"I exercise every morning at 6 AM",
|
||||
"I'm vegetarian and avoid all meat products",
|
||||
"I love reading science fiction novels",
|
||||
"I work in software engineering",
|
||||
]
|
||||
|
||||
#This function demonstrates sequential memory operations using the synchronous Memory class
|
||||
def demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id):
|
||||
"""
|
||||
Demonstrate synchronous Memory class operations.
|
||||
"""
|
||||
|
||||
agentops.start_trace("mem0_memory_example", tags=["mem0_memory_example"])
|
||||
try:
|
||||
|
||||
memory = Memory.from_config(local_config)
|
||||
|
||||
result = memory.add(
|
||||
sample_messages, user_id=user_id, metadata={"category": "movie_preferences", "session": "demo"}
|
||||
)
|
||||
|
||||
for i, preference in enumerate(sample_preferences):
|
||||
result = memory.add(preference, user_id=user_id, metadata={"type": "preference", "index": i})
|
||||
|
||||
search_queries = [
|
||||
"What movies does the user like?",
|
||||
"What are the user's food preferences?",
|
||||
"When does the user exercise?",
|
||||
]
|
||||
|
||||
for query in search_queries:
|
||||
results = memory.search(query, user_id=user_id)
|
||||
|
||||
if results and "results" in results:
|
||||
for j, result in enumerate(results):
|
||||
print(f"Result {j+1}: {result.get('memory', 'N/A')}")
|
||||
else:
|
||||
print("No results found")
|
||||
|
||||
all_memories = memory.get_all(user_id=user_id)
|
||||
if all_memories and "results" in all_memories:
|
||||
print(f"Total memories: {len(all_memories['results'])}")
|
||||
|
||||
delete_all_result = memory.delete_all(user_id=user_id)
|
||||
print(f"Delete all result: {delete_all_result}")
|
||||
|
||||
agentops.end_trace(end_state="success")
|
||||
except Exception as e:
|
||||
agentops.end_trace(end_state="error")
|
||||
|
||||
# Execute sync demonstrations
|
||||
demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id)
|
||||
|
||||
```
|
||||
|
||||
For detailed information on this integration, refer to the official [Agentops Mem0 integration documentation](https://docs.agentops.ai/v2/integrations/mem0).
|
||||
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Automatic Operation Tracking
|
||||
|
||||
AgentOps automatically monitors all Mem0 operations:
|
||||
|
||||
- **Memory Operations**: Track add, search, get_all, delete operations and much more
|
||||
- **Performance Metrics**: Monitor response times and success rates
|
||||
- **Error Tracking**: Capture and analyze operation failures
|
||||
|
||||
### 2. Real-time Analytics Dashboard
|
||||
|
||||
Access comprehensive analytics through the AgentOps dashboard:
|
||||
|
||||
- **Usage Patterns**: Visualize memory usage trends over time
|
||||
- **User Behavior**: Analyze how different users interact with memory
|
||||
- **Performance Insights**: Identify bottlenecks and optimization opportunities
|
||||
|
||||
### 3. Session Management
|
||||
|
||||
Organize your monitoring with structured sessions:
|
||||
|
||||
- **Session Tracking**: Group related operations into logical sessions
|
||||
- **Success/Failure Rates**: Track session outcomes for reliability monitoring
|
||||
- **Custom Metadata**: Add context to sessions for better analysis
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Initialize Early**: Always initialize AgentOps before importing Mem0 classes
|
||||
2. **Session Management**: Use meaningful session names and end sessions appropriately
|
||||
3. **Error Handling**: Wrap operations in try-catch blocks and report failures
|
||||
4. **Tagging**: Use tags to organize different types of memory operations
|
||||
5. **Environment Separation**: Use different projects or tags for dev/staging/prod
|
||||
|
||||
## Help & Resources
|
||||
|
||||
- [AgentOps Documentation](https://docs.agentops.ai/)
|
||||
- [AgentOps Dashboard](https://app.agentops.ai/)
|
||||
- [Mem0 Platform](https://app.mem0.ai/)
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
|
||||
@@ -238,16 +238,24 @@ The Mem0's graph supports the following operations:
|
||||
### Add Memories
|
||||
|
||||
<Note>
|
||||
If you are using Mem0 with Graph Memory, it is recommended to pass `user_id`. Use `userId` in NodeSDK.
|
||||
Mem0 with Graph Memory supports both `user_id` and `agent_id` parameters. You can use either or both to organize your memories. Use `userId` and `agentId` in NodeSDK.
|
||||
</Note>
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Using only user_id
|
||||
m.add("I like pizza", user_id="alice")
|
||||
|
||||
# Using both user_id and agent_id
|
||||
m.add("I like pizza", user_id="alice", agent_id="food-assistant")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Using only userId
|
||||
memory.add("I like pizza", { userId: "alice" });
|
||||
|
||||
// Using both userId and agentId
|
||||
memory.add("I like pizza", { userId: "alice", agentId: "food-assistant" });
|
||||
```
|
||||
|
||||
```json Output
|
||||
@@ -260,11 +268,19 @@ memory.add("I like pizza", { userId: "alice" });
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Get all memories for a user
|
||||
m.get_all(user_id="alice")
|
||||
|
||||
# Get all memories for a specific agent belonging to a user
|
||||
m.get_all(user_id="alice", agent_id="food-assistant")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Get all memories for a user
|
||||
memory.getAll({ userId: "alice" });
|
||||
|
||||
// Get all memories for a specific agent belonging to a user
|
||||
memory.getAll({ userId: "alice", agentId: "food-assistant" });
|
||||
```
|
||||
|
||||
```json Output
|
||||
@@ -277,7 +293,8 @@ memory.getAll({ userId: "alice" });
|
||||
'metadata': None,
|
||||
'created_at': '2024-08-20T14:09:27.588719-07:00',
|
||||
'updated_at': None,
|
||||
'user_id': 'alice'
|
||||
'user_id': 'alice',
|
||||
'agent_id': 'food-assistant'
|
||||
}
|
||||
],
|
||||
'entities': [
|
||||
@@ -295,11 +312,19 @@ memory.getAll({ userId: "alice" });
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Search memories for a user
|
||||
m.search("tell me my name.", user_id="alice")
|
||||
|
||||
# Search memories for a specific agent belonging to a user
|
||||
m.search("tell me my name.", user_id="alice", agent_id="food-assistant")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Search memories for a user
|
||||
memory.search("tell me my name.", { userId: "alice" });
|
||||
|
||||
// Search memories for a specific agent belonging to a user
|
||||
memory.search("tell me my name.", { userId: "alice", agentId: "food-assistant" });
|
||||
```
|
||||
|
||||
```json Output
|
||||
@@ -312,7 +337,8 @@ memory.search("tell me my name.", { userId: "alice" });
|
||||
'metadata': None,
|
||||
'created_at': '2024-08-20T14:09:27.588719-07:00',
|
||||
'updated_at': None,
|
||||
'user_id': 'alice'
|
||||
'user_id': 'alice',
|
||||
'agent_id': 'food-assistant'
|
||||
}
|
||||
],
|
||||
'entities': [
|
||||
@@ -331,11 +357,19 @@ memory.search("tell me my name.", { userId: "alice" });
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Delete all memories for a user
|
||||
m.delete_all(user_id="alice")
|
||||
|
||||
# Delete all memories for a specific agent belonging to a user
|
||||
m.delete_all(user_id="alice", agent_id="food-assistant")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Delete all memories for a user
|
||||
memory.deleteAll({ userId: "alice" });
|
||||
|
||||
// Delete all memories for a specific agent belonging to a user
|
||||
memory.deleteAll({ userId: "alice", agentId: "food-assistant" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -516,6 +550,42 @@ memory.search("Who is spiderman?", { userId: "alice123" });
|
||||
|
||||
> **Note:** The Graph Memory implementation is not standalone. You will be adding/retrieving memories to the vector store and the graph store simultaneously.
|
||||
|
||||
## Using Multiple Agents with Graph Memory
|
||||
|
||||
When working with multiple agents, you can use the `agent_id` parameter to organize memories by both user and agent. This allows you to:
|
||||
|
||||
1. Create agent-specific knowledge graphs
|
||||
2. Share common knowledge between agents
|
||||
3. Isolate sensitive or specialized information to specific agents
|
||||
|
||||
### Example: Multi-Agent Setup
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Add memories for different agents
|
||||
m.add("I prefer Italian cuisine", user_id="bob", agent_id="food-assistant")
|
||||
m.add("I'm allergic to peanuts", user_id="bob", agent_id="health-assistant")
|
||||
m.add("I live in Seattle", user_id="bob") # Shared across all agents
|
||||
|
||||
# Search within specific agent context
|
||||
food_preferences = m.search("What food do I like?", user_id="bob", agent_id="food-assistant")
|
||||
health_info = m.search("What are my allergies?", user_id="bob", agent_id="health-assistant")
|
||||
location = m.search("Where do I live?", user_id="bob") # Searches across all agents
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Add memories for different agents
|
||||
memory.add("I prefer Italian cuisine", { userId: "bob", agentId: "food-assistant" });
|
||||
memory.add("I'm allergic to peanuts", { userId: "bob", agentId: "health-assistant" });
|
||||
memory.add("I live in Seattle", { userId: "bob" }); // Shared across all agents
|
||||
|
||||
// Search within specific agent context
|
||||
const foodPreferences = memory.search("What food do I like?", { userId: "bob", agentId: "food-assistant" });
|
||||
const healthInfo = memory.search("What are my allergies?", { userId: "bob", agentId: "health-assistant" });
|
||||
const location = memory.search("Where do I live?", { userId: "bob" }); // Searches across all agents
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
If you want to use a managed version of Mem0, please check out [Mem0](https://mem0.dev/pd). If you have any questions, please feel free to reach out to us using one of the following methods:
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
<Snippet file="get-help.mdx" />
|
||||
|
||||
+6
-6
@@ -1769,27 +1769,27 @@
|
||||
"x-code-samples": [
|
||||
{
|
||||
"lang": "Python",
|
||||
"source": "# To use the Python SDK, install the package:\n# pip install mem0ai\n\nfrom mem0 import MemoryClient\nclient = MemoryClient(api_key=\"your_api_key\", org_id=\"your_org_id\", project_id=\"your_project_id\")\n\n# Update a memory\nmemory_id = \"<memory_id>\"\nmessage = \"Your updated memory message here\"\nclient.update(memory_id, message)"
|
||||
"source": "# To use the Python SDK, install the package:\n# pip install mem0ai\n\nfrom mem0 import MemoryClient\nclient = MemoryClient(api_key=\"your_api_key\", org_id=\"your_org_id\", project_id=\"your_project_id\")\n\n# Update a memory\nmemory_id = \"<memory_id>\"\nclient.update(\n memory_id=memory_id,\n text=\"Your updated memory message here\",\n metadata={\"category\": \"example\"}\n)"
|
||||
},
|
||||
{
|
||||
"lang": "JavaScript",
|
||||
"source": "// To use the JavaScript SDK, install the package:\n// npm i mem0ai\n\nimport MemoryClient from 'mem0ai';\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\n// Update a specific memory\nconst memory_id=<memory_id>\nconst message=\"Your updated memory message here\"\nclient.update(memory_id, message)\n .then(result => console.log(result))\n .catch(error => console.error(error));"
|
||||
"source": "// To use the JavaScript SDK, install the package:\n// npm i mem0ai\n\nimport MemoryClient from 'mem0ai';\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\n// Update a specific memory\nconst memory_id = \"<memory_id>\";\nclient.update(memory_id, { \n text: \"Your updated memory message here\",\n metadata: { category: \"example\" }\n})\n .then(result => console.log(result))\n .catch(error => console.error(error));"
|
||||
},
|
||||
{
|
||||
"lang": "cURL",
|
||||
"source": "curl --request PUT \\\n --url https://api.mem0.ai/v1/memories/{memory_id}/ \\\n --header 'Authorization: Token <api-key>' \\\n --header 'Content-Type: application/json' \\\n --data '{\"text\": \"Your updated memory text here\"}'"
|
||||
"source": "curl --request PUT \\\n --url https://api.mem0.ai/v1/memories/{memory_id}/ \\\n --header 'Authorization: Token <api-key>' \\\n --header 'Content-Type: application/json' \\\n --data '{\"text\": \"Your updated memory text here\", \"metadata\": {\"category\": \"example\"}}'"
|
||||
},
|
||||
{
|
||||
"lang": "Go",
|
||||
"source": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"net/http\"\n\t\"io/ioutil\"\n)\n\nfunc main() {\n\n\turl := \"https://api.mem0.ai/v1/memories/{memory_id}/\"\n\n\tpayload := strings.NewReader(`{\n\t\"text\": \"Your updated memory text here\"\n}`)\n\n\treq, _ := http.NewRequest(\"PUT\", url, payload)\n\n\treq.Header.Add(\"Authorization\", \"Token <api-key>\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\tres, _ := http.DefaultClient.Do(req)\n\n\tdefer res.Body.Close()\n\tbody, _ := ioutil.ReadAll(res.Body)\n\n\tfmt.Println(res)\n\tfmt.Println(string(body))\n\n}"
|
||||
"source": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"net/http\"\n\t\"io/ioutil\"\n)\n\nfunc main() {\n\n\turl := \"https://api.mem0.ai/v1/memories/{memory_id}/\"\n\n\tpayload := strings.NewReader(`{\n\t\"text\": \"Your updated memory text here\",\n\t\"metadata\": {\n\t\t\"category\": \"example\"\n\t}\n}`)\n\n\treq, _ := http.NewRequest(\"PUT\", url, payload)\n\n\treq.Header.Add(\"Authorization\", \"Token <api-key>\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\tres, _ := http.DefaultClient.Do(req)\n\n\tdefer res.Body.Close()\n\tbody, _ := ioutil.ReadAll(res.Body)\n\n\tfmt.Println(res)\n\tfmt.Println(string(body))\n\n}"
|
||||
},
|
||||
{
|
||||
"lang": "PHP",
|
||||
"source": "<?php\n\n$curl = curl_init();\n\ncurl_setopt_array($curl, [\n CURLOPT_URL => \"https://api.mem0.ai/v1/memories/{memory_id}/\",\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_ENCODING => \"\",\n CURLOPT_MAXREDIRS => 10,\n CURLOPT_TIMEOUT => 30,\n CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\n CURLOPT_CUSTOMREQUEST => \"PUT\",\n CURLOPT_HTTPHEADER => [\n \"Authorization: Token <api-key>\",\n \"Content-Type: application/json\"\n ],\n CURLOPT_POSTFIELDS => json_encode({\n \"text\": \"Your updated memory text here\"\n })\n]);\n\n$response = curl_exec($curl);\n$err = curl_error($curl);\n\ncurl_close($curl);\n\nif ($err) {\n echo \"cURL Error #:\" . $err;\n} else {\n echo $response;\n}"
|
||||
"source": "<?php\n\n$curl = curl_init();\n\ncurl_setopt_array($curl, [\n CURLOPT_URL => \"https://api.mem0.ai/v1/memories/{memory_id}/\",\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_ENCODING => \"\",\n CURLOPT_MAXREDIRS => 10,\n CURLOPT_TIMEOUT => 30,\n CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\n CURLOPT_CUSTOMREQUEST => \"PUT\",\n CURLOPT_HTTPHEADER => [\n \"Authorization: Token <api-key>\",\n \"Content-Type: application/json\"\n ],\n CURLOPT_POSTFIELDS => json_encode([\n \"text\" => \"Your updated memory text here\",\n \"metadata\" => [\"category\" => \"example\"]\n ])\n]);\n\n$response = curl_exec($curl);\n$err = curl_error($curl);\n\ncurl_close($curl);\n\nif ($err) {\n echo \"cURL Error #:\" . $err;\n} else {\n echo $response;\n}"
|
||||
},
|
||||
{
|
||||
"lang": "Java",
|
||||
"source": "HttpResponse<String> response = Unirest.put(\"https://api.mem0.ai/v1/memories/{memory_id}/\")\n .header(\"Authorization\", \"Token <api-key>\")\n .header(\"Content-Type\", \"application/json\")\n .body({\"text\": \"Your updated memory text here\"})\n .asString();"
|
||||
"source": "HttpResponse<String> response = Unirest.put(\"https://api.mem0.ai/v1/memories/{memory_id}/\")\n .header(\"Authorization\", \"Token <api-key>\")\n .header(\"Content-Type\", \"application/json\")\n .body(\"{\\\"text\\\": \\\"Your updated memory text here\\\", \\\"metadata\\\": {\\\"category\\\": \\\"example\\\"}}\")\n .asString();"
|
||||
}
|
||||
],
|
||||
"x-codegen-request-body-name": "data"
|
||||
|
||||
@@ -1780,24 +1780,26 @@ curl -X GET "https://api.mem0.ai/v1/memories/<memory-id-here>/history/" \
|
||||
|
||||
### 4.6 Update Memory
|
||||
|
||||
Update a memory with new data.
|
||||
Update a memory with new data. You can update the memory's text, metadata, or both.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
message = "I recently tried chicken and I loved it. I'm thinking of trying more non-vegetarian dishes.."
|
||||
client.update(memory_id, message)
|
||||
client.update(
|
||||
memory_id="<memory-id-here>",
|
||||
text="I am now a vegetarian.",
|
||||
metadata={"diet": "vegetarian"}
|
||||
)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
const message = "I recently tried chicken and I loved it. I'm thinking of trying more non-vegetarian dishes..";
|
||||
client.update("memory-id-here", message)
|
||||
client.update("memory-id-here", { text: "I am now a vegetarian.", metadata: { diet: "vegetarian" } })
|
||||
.then(result => console.log(result))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X PUT "https://api.mem0.ai/v1/memories/memory-id-here" \
|
||||
curl -X PUT "https://api.mem0.ai/v1/memories/<memory-id-here>" \
|
||||
-H "Authorization: Token your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
|
||||
+14
-2
@@ -333,11 +333,23 @@ const memory = new Memory();
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
# For a user
|
||||
result = m.add("I like to drink coffee in the morning and go for a walk.", user_id="alice", metadata={"category": "preferences"})
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I like to drink coffee in the morning and go for a walk"
|
||||
}
|
||||
]
|
||||
result = m.add(messages, user_id="alice", metadata={"category": "preferences"})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
const result = memory.add("I like to drink coffee in the morning and go for a walk.", { userId: "alice", metadata: { category: "preferences" } });
|
||||
const messages = [
|
||||
{
|
||||
role: "user",
|
||||
content: "I like to drink coffee in the morning and go for a walk"
|
||||
}
|
||||
];
|
||||
const result = memory.add(messages, { userId: "alice", metadata: { category: "preferences" } });
|
||||
```
|
||||
|
||||
```json Output
|
||||
|
||||
Generated
+2
-2
@@ -2552,7 +2552,7 @@ azure = ["adlfs (>=2024.2.0)"]
|
||||
clip = ["open-clip", "pillow", "torch"]
|
||||
dev = ["pre-commit", "ruff"]
|
||||
docs = ["mkdocs", "mkdocs-jupyter", "mkdocs-material", "mkdocstrings[python]"]
|
||||
embeddings = ["awscli (>=1.29.57)", "boto3 (>=1.28.57)", "botocore (>=1.31.57)", "cohere", "google-generativeai", "huggingface-hub", "instructorembedding", "open-clip-torch", "openai (>=1.6.1)", "pillow", "sentence-transformers", "torch"]
|
||||
embeddings = ["awscli (>=1.29.57)", "boto3 (>=1.28.57)", "botocore (>=1.31.57)", "cohere", "google-generativeai", "huggingface-hub", "instructorembedding", "open-clip-torch", "openai (>=1.6.1)", "pillow", "sentence-transformers", "torch", "google-genai"]
|
||||
tests = ["aiohttp", "boto3", "duckdb", "pandas (>=1.4)", "polars (>=0.19)", "pytest", "pytest-asyncio", "pytest-mock", "pytz", "tantivy"]
|
||||
|
||||
[[package]]
|
||||
@@ -7129,7 +7129,7 @@ cffi = ["cffi (>=1.11)"]
|
||||
aws = ["langchain-aws"]
|
||||
elasticsearch = ["elasticsearch"]
|
||||
gmail = ["google-api-core", "google-api-python-client", "google-auth", "google-auth-httplib2", "google-auth-oauthlib", "requests"]
|
||||
google = ["google-generativeai"]
|
||||
google = ["google-generativeai", "google-genai"]
|
||||
googledrive = ["google-api-python-client", "google-auth-httplib2", "google-auth-oauthlib"]
|
||||
lancedb = ["lancedb"]
|
||||
llama2 = ["replicate"]
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Simple Voice Agent with Memory: Personal Food Assistant.
|
||||
A food assistant that remembers your dietary preferences and speaks recommendations
|
||||
Powered by Agno + Cartesia + Mem0
|
||||
|
||||
export MEM0_API_KEY=your_mem0_api_key
|
||||
export OPENAI_API_KEY=your_openai_api_key
|
||||
export CARTESIA_API_KEY=your_cartesia_api_key
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
from agno.tools.cartesia import CartesiaTools
|
||||
from agno.utils.audio import write_audio_to_file
|
||||
from mem0 import MemoryClient
|
||||
|
||||
memory_client = MemoryClient()
|
||||
USER_ID = "food_user_01"
|
||||
|
||||
# Agent instructions
|
||||
agent_instructions = dedent(
|
||||
"""Follow these steps SEQUENTIALLY to provide personalized food recommendations with voice:
|
||||
1. Analyze the user's food request and identify what type of recommendation they need.
|
||||
2. Consider their dietary preferences, restrictions, and cooking habits from memory context.
|
||||
3. Generate a personalized food recommendation based on their stored preferences.
|
||||
4. Analyze the appropriate tone for the response (helpful, enthusiastic, cautious for allergies).
|
||||
5. Call `list_voices` to retrieve available voices.
|
||||
6. Select a voice that matches the helpful, friendly tone.
|
||||
7. Call `text_to_speech` to generate the final audio recommendation.
|
||||
"""
|
||||
)
|
||||
|
||||
# Simple agent that remembers food preferences
|
||||
food_agent = Agent(
|
||||
name="Personal Food Assistant",
|
||||
description="Provides personalized food recommendations with memory and generates voice responses using Cartesia TTS tools.",
|
||||
instructions=agent_instructions,
|
||||
model=OpenAIChat(id="gpt-4o"),
|
||||
tools=[CartesiaTools(voice_localize_enabled=True)],
|
||||
show_tool_calls=True,
|
||||
)
|
||||
|
||||
|
||||
def get_food_recommendation(user_query: str, user_id):
|
||||
"""Get food recommendation with memory context"""
|
||||
|
||||
# Search memory for relevant food preferences
|
||||
memories_result = memory_client.search(
|
||||
query=user_query,
|
||||
user_id=user_id,
|
||||
limit=5
|
||||
)
|
||||
|
||||
# Add memory context to the message
|
||||
memories = [f"- {result['memory']}" for result in memories_result]
|
||||
memory_context = "Memories about user that might be relevant:\n" + "\n".join(memories)
|
||||
|
||||
# Combine memory context with user request
|
||||
full_request = f"""
|
||||
{memory_context}
|
||||
|
||||
User: {user_query}
|
||||
|
||||
Answer the user query based on provided context and create a voice note.
|
||||
"""
|
||||
|
||||
# Generate response with voice (same pattern as translator)
|
||||
food_agent.print_response(full_request)
|
||||
response = food_agent.run_response
|
||||
|
||||
# Save audio file
|
||||
if response.audio:
|
||||
import time
|
||||
timestamp = int(time.time())
|
||||
filename = f"food_recommendation_{timestamp}.mp3"
|
||||
write_audio_to_file(
|
||||
response.audio[0].base64_audio,
|
||||
filename=filename,
|
||||
)
|
||||
print(f"Audio saved as {filename}")
|
||||
|
||||
return response.content
|
||||
|
||||
|
||||
def initialize_food_memory(user_id):
|
||||
"""Initialize memory with food preferences"""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hi, I'm Sarah. I'm vegetarian and lactose intolerant. I love spicy food, especially Thai and Indian cuisine.",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Hello Sarah! I've noted that you're vegetarian, lactose intolerant, and love spicy Thai and Indian food.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I prefer quick breakfasts since I'm always rushing, but I like cooking elaborate dinners. I also meal prep on Sundays.",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Got it! Quick breakfasts, elaborate dinners, and Sunday meal prep. I'll remember this for future recommendations.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I'm trying to eat more protein. I like quinoa, lentils, chickpeas, and tofu. I hate mushrooms though.",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Perfect! I'll focus on protein-rich options like quinoa, lentils, chickpeas, and tofu, and avoid mushrooms.",
|
||||
},
|
||||
]
|
||||
|
||||
memory_client.add(messages, user_id=user_id)
|
||||
print("Food preferences stored in memory")
|
||||
|
||||
|
||||
# Initialize the memory for the user once in order for the agent to learn the user preference
|
||||
initialize_food_memory(user_id=USER_ID)
|
||||
|
||||
print(get_food_recommendation("Which type of restaurants should I go tonight for dinner and cuisines preferred?", user_id=USER_ID))
|
||||
# OUTPUT: 🎵 Audio saved as food_recommendation_1750162610.mp3
|
||||
# For dinner tonight, considering your love for healthy spic optionsy, you could try a nice Thai, Indian, or Mexican restaurant.
|
||||
# You might find dishes with quinoa, chickpeas, tofu, and fresh herbs delightful. Enjoy your dinner!
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "2.1.30",
|
||||
"version": "2.1.32",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -229,7 +229,7 @@ export default class MemoryClient {
|
||||
}
|
||||
|
||||
if (options.api_version) {
|
||||
options.version = options.api_version.toString();
|
||||
options.version = options.api_version.toString() || "v2";
|
||||
}
|
||||
|
||||
const payload = this._preparePayload(messages, options);
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface MemoryOptions {
|
||||
timestamp?: number;
|
||||
output_format?: string | OutputFormat;
|
||||
async_mode?: boolean;
|
||||
filter_memories?: boolean;
|
||||
}
|
||||
|
||||
export interface ProjectOptions {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import type { TelemetryClient, TelemetryOptions } from "./telemetry.types";
|
||||
|
||||
let version = "2.1.26";
|
||||
let version = "2.1.32";
|
||||
|
||||
// Safely check for process.env in different environments
|
||||
let MEM0_TELEMETRY = true;
|
||||
|
||||
+497
-104
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,7 @@ class BaseLlmConfig(ABC):
|
||||
sarvam_base_url: Optional[str] = "https://api.sarvam.ai/v1",
|
||||
# LM Studio specific
|
||||
lmstudio_base_url: Optional[str] = "http://localhost:1234/v1",
|
||||
lmstudio_response_format: dict = None,
|
||||
# AWS Bedrock specific
|
||||
aws_access_key_id: Optional[str] = None,
|
||||
aws_secret_access_key: Optional[str] = None,
|
||||
@@ -95,6 +96,8 @@ class BaseLlmConfig(ABC):
|
||||
:type sarvam_base_url: Optional[str], optional
|
||||
:param lmstudio_base_url: LM Studio base URL to be use, defaults to "http://localhost:1234/v1"
|
||||
:type lmstudio_base_url: Optional[str], optional
|
||||
:param lmstudio_response_format: LM Studio response format to be use, defaults to None
|
||||
:type lmstudio_response_format: Optional[Dict], optional
|
||||
"""
|
||||
|
||||
self.model = model
|
||||
@@ -134,6 +137,7 @@ class BaseLlmConfig(ABC):
|
||||
|
||||
# LM Studio specific
|
||||
self.lmstudio_base_url = lmstudio_base_url
|
||||
self.lmstudio_response_format = lmstudio_response_format
|
||||
|
||||
# AWS Bedrock specific
|
||||
self.aws_access_key_id = aws_access_key_id
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class BaiduDBConfig(BaseModel):
|
||||
endpoint: str = Field("http://localhost:8287", description="Endpoint URL for Baidu VectorDB")
|
||||
account: str = Field("root", description="Account for Baidu VectorDB")
|
||||
api_key: str = Field(None, description="API Key for Baidu VectorDB")
|
||||
database_name: str = Field("mem0", description="Name of the database")
|
||||
table_name: str = Field("mem0", description="Name of the table")
|
||||
embedding_model_dims: int = Field(1536, description="Dimensions of the embedding model")
|
||||
metric_type: str = Field("L2", description="Metric type for similarity search")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
allowed_fields = set(cls.model_fields.keys())
|
||||
input_fields = set(values.keys())
|
||||
extra_fields = input_fields - allowed_fields
|
||||
if extra_fields:
|
||||
raise ValueError(
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = {
|
||||
"arbitrary_types_allowed": True,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import Any, Dict, Optional, Callable, List
|
||||
|
||||
from pydantic import BaseModel, Field, root_validator
|
||||
|
||||
|
||||
class MongoVectorConfig(BaseModel):
|
||||
"""Configuration for MongoDB vector database."""
|
||||
|
||||
db_name: str = Field("mem0_db", description="Name of the MongoDB database")
|
||||
collection_name: str = Field("mem0", description="Name of the MongoDB collection")
|
||||
embedding_model_dims: Optional[int] = Field(1536, description="Dimensions of the embedding vectors")
|
||||
user: Optional[str] = Field(None, description="MongoDB user for authentication")
|
||||
password: Optional[str] = Field(None, description="Password for the MongoDB user")
|
||||
host: Optional[str] = Field("localhost", description="MongoDB host. Default is 'localhost'")
|
||||
port: Optional[int] = Field(27017, description="MongoDB port. Default is 27017")
|
||||
|
||||
@root_validator(pre=True)
|
||||
def check_auth_and_connection(cls, values):
|
||||
user = values.get("user")
|
||||
password = values.get("password")
|
||||
if (user is None) != (password is None):
|
||||
raise ValueError("Both 'user' and 'password' must be provided together or omitted together.")
|
||||
|
||||
host = values.get("host")
|
||||
port = values.get("port")
|
||||
if host is None:
|
||||
raise ValueError("The 'host' must be provided.")
|
||||
if port is None:
|
||||
raise ValueError("The 'port' must be provided.")
|
||||
return values
|
||||
|
||||
@root_validator(pre=True)
|
||||
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
allowed_fields = set(cls.__fields__)
|
||||
input_fields = set(values.keys())
|
||||
extra_fields = input_fields - allowed_fields
|
||||
if extra_fields:
|
||||
raise ValueError(
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. "
|
||||
f"Please provide only the following fields: {', '.join(allowed_fields)}."
|
||||
)
|
||||
return values
|
||||
+59
-39
@@ -2,9 +2,9 @@ import os
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
try:
|
||||
import google.generativeai as genai
|
||||
from google.generativeai import GenerativeModel, protos
|
||||
from google.generativeai.types import content_types
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"The 'google-generativeai' library is required. Please install it using 'pip install google-generativeai'."
|
||||
@@ -22,66 +22,71 @@ class GeminiLLM(LLMBase):
|
||||
self.config.model = "gemini-1.5-flash-latest"
|
||||
|
||||
api_key = self.config.api_key or os.getenv("GEMINI_API_KEY")
|
||||
genai.configure(api_key=api_key)
|
||||
self.client = GenerativeModel(model_name=self.config.model)
|
||||
self.client_gemini = genai.Client(
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
def _parse_response(self, response, tools):
|
||||
"""
|
||||
Process the response based on whether tools are used or not.
|
||||
|
||||
Args:
|
||||
response: The raw response from API.
|
||||
response: The raw response from the API.
|
||||
tools: The list of tools provided in the request.
|
||||
|
||||
Returns:
|
||||
str or dict: The processed response.
|
||||
"""
|
||||
candidate = response.candidates[0]
|
||||
content = candidate.content.parts[0].text if candidate.content.parts else None
|
||||
|
||||
if tools:
|
||||
processed_response = {
|
||||
"content": (content if (content := response.candidates[0].content.parts[0].text) else None),
|
||||
"content": content,
|
||||
"tool_calls": [],
|
||||
}
|
||||
|
||||
for part in response.candidates[0].content.parts:
|
||||
if fn := part.function_call:
|
||||
if isinstance(fn, protos.FunctionCall):
|
||||
fn_call = type(fn).to_dict(fn)
|
||||
processed_response["tool_calls"].append({"name": fn_call["name"], "arguments": fn_call["args"]})
|
||||
continue
|
||||
processed_response["tool_calls"].append({"name": fn.name, "arguments": fn.args})
|
||||
for part in candidate.content.parts:
|
||||
fn = getattr(part, "function_call", None)
|
||||
if fn:
|
||||
processed_response["tool_calls"].append({
|
||||
"name": fn.name,
|
||||
"arguments": fn.args,
|
||||
})
|
||||
|
||||
return processed_response
|
||||
else:
|
||||
return response.candidates[0].content.parts[0].text
|
||||
|
||||
def _reformat_messages(self, messages: List[Dict[str, str]]):
|
||||
return content
|
||||
|
||||
|
||||
def _reformat_messages(self, messages: List[Dict[str, str]]) -> List[types.Content]:
|
||||
"""
|
||||
Reformat messages for Gemini.
|
||||
Reformat messages for Gemini using google.genai.types.
|
||||
|
||||
Args:
|
||||
messages: The list of messages provided in the request.
|
||||
|
||||
Returns:
|
||||
list: The list of messages in the required format.
|
||||
list: A list of types.Content objects with proper role and parts.
|
||||
"""
|
||||
new_messages = []
|
||||
|
||||
for message in messages:
|
||||
if message["role"] == "system":
|
||||
content = "THIS IS A SYSTEM PROMPT. YOU MUST OBEY THIS: " + message["content"]
|
||||
|
||||
else:
|
||||
content = message["content"]
|
||||
|
||||
new_messages.append(
|
||||
{
|
||||
"parts": content,
|
||||
"role": "model" if message["role"] == "model" else "user",
|
||||
}
|
||||
types.Content(
|
||||
role="model" if message["role"] == "model" else "user",
|
||||
parts=[types.Part(text=content)]
|
||||
)
|
||||
)
|
||||
|
||||
return new_messages
|
||||
|
||||
|
||||
def _reformat_tools(self, tools: Optional[List[Dict]]):
|
||||
"""
|
||||
Reformat tools for Gemini.
|
||||
@@ -126,6 +131,7 @@ class GeminiLLM(LLMBase):
|
||||
tools: Optional[List[Dict]] = None,
|
||||
tool_choice: str = "auto",
|
||||
):
|
||||
|
||||
"""
|
||||
Generate a response based on the given messages using Gemini.
|
||||
|
||||
@@ -149,23 +155,37 @@ class GeminiLLM(LLMBase):
|
||||
params["response_mime_type"] = "application/json"
|
||||
if "schema" in response_format:
|
||||
params["response_schema"] = response_format["schema"]
|
||||
|
||||
tool_config = None
|
||||
if tool_choice:
|
||||
tool_config = content_types.to_tool_config(
|
||||
{
|
||||
"function_calling_config": {
|
||||
"mode": tool_choice,
|
||||
"allowed_function_names": (
|
||||
[tool["function"]["name"] for tool in tools] if tool_choice == "any" else None
|
||||
),
|
||||
}
|
||||
}
|
||||
tool_config = types.ToolConfig(
|
||||
function_calling_config=types.FunctionCallingConfig(
|
||||
mode=tool_choice.upper(), # Assuming 'any' should become 'ANY', etc.
|
||||
allowed_function_names=[
|
||||
tool["function"]["name"] for tool in tools
|
||||
] if tool_choice == "any" else None
|
||||
)
|
||||
)
|
||||
|
||||
response = self.client.generate_content(
|
||||
contents=self._reformat_messages(messages),
|
||||
tools=self._reformat_tools(tools),
|
||||
generation_config=genai.GenerationConfig(**params),
|
||||
tool_config=tool_config,
|
||||
)
|
||||
print(f"Tool config: {tool_config}")
|
||||
print(f"Params: {params}" )
|
||||
print(f"Messages: {messages}")
|
||||
print(f"Tools: {tools}")
|
||||
print(f"Reformatted messages: {self._reformat_messages(messages)}")
|
||||
print(f"Reformatted tools: {self._reformat_tools(tools)}")
|
||||
|
||||
response = self.client_gemini.models.generate_content(
|
||||
model=self.config.model,
|
||||
contents=self._reformat_messages(messages),
|
||||
config=types.GenerateContentConfig(
|
||||
temperature= self.config.temperature,
|
||||
max_output_tokens= self.config.max_tokens,
|
||||
top_p= self.config.top_p,
|
||||
tools=self._reformat_tools(tools),
|
||||
tool_config=tool_config,
|
||||
|
||||
),
|
||||
)
|
||||
print(f"Response test: {response}")
|
||||
|
||||
return self._parse_response(response, tools)
|
||||
|
||||
@@ -46,6 +46,8 @@ class LMStudioLLM(LLMBase):
|
||||
}
|
||||
if response_format:
|
||||
params["response_format"] = response_format
|
||||
if self.config.lmstudio_response_format is not None:
|
||||
params["response_format"] = self.config.lmstudio_response_format
|
||||
|
||||
response = self.client.chat.completions.create(**params)
|
||||
return response.choices[0].message.content
|
||||
|
||||
+226
-138
@@ -80,8 +80,8 @@ class MemoryGraph:
|
||||
|
||||
# TODO: Batch queries with APOC plugin
|
||||
# TODO: Add more filter support
|
||||
deleted_entities = self._delete_entities(to_be_deleted, filters["user_id"])
|
||||
added_entities = self._add_entities(to_be_added, filters["user_id"], entity_type_map)
|
||||
deleted_entities = self._delete_entities(to_be_deleted, filters)
|
||||
added_entities = self._add_entities(to_be_added, filters, entity_type_map)
|
||||
|
||||
return {"deleted_entities": deleted_entities, "added_entities": added_entities}
|
||||
|
||||
@@ -122,32 +122,35 @@ class MemoryGraph:
|
||||
return search_results
|
||||
|
||||
def delete_all(self, filters):
|
||||
cypher = f"""
|
||||
MATCH (n {self.node_label} {{user_id: $user_id}})
|
||||
DETACH DELETE n
|
||||
"""
|
||||
params = {"user_id": filters["user_id"]}
|
||||
if filters.get("agent_id"):
|
||||
cypher = f"""
|
||||
MATCH (n {self.node_label} {{user_id: $user_id, agent_id: $agent_id}})
|
||||
DETACH DELETE n
|
||||
"""
|
||||
params = {"user_id": filters["user_id"], "agent_id": filters["agent_id"]}
|
||||
else:
|
||||
cypher = f"""
|
||||
MATCH (n {self.node_label} {{user_id: $user_id}})
|
||||
DETACH DELETE n
|
||||
"""
|
||||
params = {"user_id": filters["user_id"]}
|
||||
self.graph.query(cypher, params=params)
|
||||
|
||||
def get_all(self, filters, limit=100):
|
||||
"""
|
||||
Retrieves all nodes and relationships from the graph database based on optional filtering criteria.
|
||||
|
||||
Args:
|
||||
filters (dict): A dictionary containing filters to be applied during the retrieval.
|
||||
limit (int): The maximum number of nodes and relationships to retrieve. Defaults to 100.
|
||||
Returns:
|
||||
list: A list of dictionaries, each containing:
|
||||
- 'contexts': The base data store response for each memory.
|
||||
- 'entities': A list of strings representing the nodes and relationships
|
||||
"""
|
||||
# return all nodes and relationships
|
||||
def get_all(self, filters, limit=100):
|
||||
agent_filter = ""
|
||||
params = {"user_id": filters["user_id"], "limit": limit}
|
||||
if filters.get("agent_id"):
|
||||
agent_filter = "AND n.agent_id = $agent_id AND m.agent_id = $agent_id"
|
||||
params["agent_id"] = filters["agent_id"]
|
||||
|
||||
query = f"""
|
||||
MATCH (n {self.node_label} {{user_id: $user_id}})-[r]->(m {self.node_label} {{user_id: $user_id}})
|
||||
WHERE 1=1 {agent_filter}
|
||||
RETURN n.name AS source, type(r) AS relationship, m.name AS target
|
||||
LIMIT $limit
|
||||
"""
|
||||
results = self.graph.query(query, params={"user_id": filters["user_id"], "limit": limit})
|
||||
results = self.graph.query(query, params=params)
|
||||
|
||||
final_results = []
|
||||
for result in results:
|
||||
@@ -163,6 +166,7 @@ class MemoryGraph:
|
||||
|
||||
return final_results
|
||||
|
||||
|
||||
def _retrieve_nodes_from_data(self, data, filters):
|
||||
"""Extracts all the entities mentioned in the query."""
|
||||
_tools = [EXTRACT_ENTITIES_TOOL]
|
||||
@@ -197,23 +201,27 @@ class MemoryGraph:
|
||||
return entity_type_map
|
||||
|
||||
def _establish_nodes_relations_from_data(self, data, filters, entity_type_map):
|
||||
"""Eshtablish relations among the extracted nodes."""
|
||||
"""Establish relations among the extracted nodes."""
|
||||
|
||||
# Compose user identification string for prompt
|
||||
user_identity = f"user_id: {filters['user_id']}"
|
||||
if filters.get("agent_id"):
|
||||
user_identity += f", agent_id: {filters['agent_id']}"
|
||||
|
||||
if self.config.graph_store.custom_prompt:
|
||||
system_content = EXTRACT_RELATIONS_PROMPT.replace("USER_ID", user_identity)
|
||||
# Add the custom prompt line if configured
|
||||
system_content = system_content.replace(
|
||||
"CUSTOM_PROMPT", f"4. {self.config.graph_store.custom_prompt}"
|
||||
)
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": EXTRACT_RELATIONS_PROMPT.replace("USER_ID", filters["user_id"]).replace(
|
||||
"CUSTOM_PROMPT", f"4. {self.config.graph_store.custom_prompt}"
|
||||
),
|
||||
},
|
||||
{"role": "system", "content": system_content},
|
||||
{"role": "user", "content": data},
|
||||
]
|
||||
else:
|
||||
system_content = EXTRACT_RELATIONS_PROMPT.replace("USER_ID", user_identity)
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": EXTRACT_RELATIONS_PROMPT.replace("USER_ID", filters["user_id"]),
|
||||
},
|
||||
{"role": "system", "content": system_content},
|
||||
{"role": "user", "content": f"List of entities: {list(entity_type_map.keys())}. \n\nText: {data}"},
|
||||
]
|
||||
|
||||
@@ -227,8 +235,8 @@ class MemoryGraph:
|
||||
)
|
||||
|
||||
entities = []
|
||||
if extracted_entities["tool_calls"]:
|
||||
entities = extracted_entities["tool_calls"][0]["arguments"]["entities"]
|
||||
if extracted_entities.get("tool_calls"):
|
||||
entities = extracted_entities["tool_calls"][0].get("arguments", {}).get("entities", [])
|
||||
|
||||
entities = self._remove_spaces_from_entities(entities)
|
||||
logger.debug(f"Extracted entities: {entities}")
|
||||
@@ -237,32 +245,43 @@ class MemoryGraph:
|
||||
def _search_graph_db(self, node_list, filters, limit=100):
|
||||
"""Search similar nodes among and their respective incoming and outgoing relations."""
|
||||
result_relations = []
|
||||
agent_filter = ""
|
||||
if filters.get("agent_id"):
|
||||
agent_filter = "AND n.agent_id = $agent_id AND m.agent_id = $agent_id"
|
||||
|
||||
for node in node_list:
|
||||
n_embedding = self.embedding_model.embed(node)
|
||||
|
||||
cypher_query = f"""
|
||||
MATCH (n {self.node_label})
|
||||
WHERE n.embedding IS NOT NULL AND n.user_id = $user_id
|
||||
{agent_filter}
|
||||
WITH n, round(2 * vector.similarity.cosine(n.embedding, $n_embedding) - 1, 4) AS similarity // denormalize for backward compatibility
|
||||
WHERE similarity >= $threshold
|
||||
CALL (n) {{
|
||||
MATCH (n)-[r]->(m)
|
||||
CALL {{
|
||||
MATCH (n)-[r]->(m)
|
||||
WHERE m.user_id = $user_id {agent_filter.replace("n.", "m.")}
|
||||
RETURN n.name AS source, elementId(n) AS source_id, type(r) AS relationship, elementId(r) AS relation_id, m.name AS destination, elementId(m) AS destination_id
|
||||
UNION
|
||||
MATCH (m)-[r]->(n)
|
||||
MATCH (m)-[r]->(n)
|
||||
WHERE m.user_id = $user_id {agent_filter.replace("n.", "m.")}
|
||||
RETURN m.name AS source, elementId(m) AS source_id, type(r) AS relationship, elementId(r) AS relation_id, n.name AS destination, elementId(n) AS destination_id
|
||||
}}
|
||||
WITH distinct source, source_id, relationship, relation_id, destination, destination_id, similarity //deduplicate
|
||||
WITH distinct source, source_id, relationship, relation_id, destination, destination_id, similarity
|
||||
RETURN source, source_id, relationship, relation_id, destination, destination_id, similarity
|
||||
ORDER BY similarity DESC
|
||||
LIMIT $limit
|
||||
"""
|
||||
|
||||
params = {
|
||||
"n_embedding": n_embedding,
|
||||
"threshold": self.threshold,
|
||||
"user_id": filters["user_id"],
|
||||
"limit": limit,
|
||||
}
|
||||
if filters.get("agent_id"):
|
||||
params["agent_id"] = filters["agent_id"]
|
||||
|
||||
ans = self.graph.query(cypher_query, params=params)
|
||||
result_relations.extend(ans)
|
||||
|
||||
@@ -271,7 +290,13 @@ class MemoryGraph:
|
||||
def _get_delete_entities_from_search_output(self, search_output, data, filters):
|
||||
"""Get the entities to be deleted from the search output."""
|
||||
search_output_string = format_entities(search_output)
|
||||
system_prompt, user_prompt = get_delete_messages(search_output_string, data, filters["user_id"])
|
||||
|
||||
# Compose user identification string for prompt
|
||||
user_identity = f"user_id: {filters['user_id']}"
|
||||
if filters.get("agent_id"):
|
||||
user_identity += f", agent_id: {filters['agent_id']}"
|
||||
|
||||
system_prompt, user_prompt = get_delete_messages(search_output_string, data, user_identity)
|
||||
|
||||
_tools = [DELETE_MEMORY_TOOL_GRAPH]
|
||||
if self.llm_provider in ["azure_openai_structured", "openai_structured"]:
|
||||
@@ -288,44 +313,59 @@ class MemoryGraph:
|
||||
)
|
||||
|
||||
to_be_deleted = []
|
||||
for item in memory_updates["tool_calls"]:
|
||||
if item["name"] == "delete_graph_memory":
|
||||
to_be_deleted.append(item["arguments"])
|
||||
# in case if it is not in the correct format
|
||||
for item in memory_updates.get("tool_calls", []):
|
||||
if item.get("name") == "delete_graph_memory":
|
||||
to_be_deleted.append(item.get("arguments"))
|
||||
# Clean entities formatting
|
||||
to_be_deleted = self._remove_spaces_from_entities(to_be_deleted)
|
||||
logger.debug(f"Deleted relationships: {to_be_deleted}")
|
||||
return to_be_deleted
|
||||
|
||||
def _delete_entities(self, to_be_deleted, user_id):
|
||||
def _delete_entities(self, to_be_deleted, filters):
|
||||
"""Delete the entities from the graph."""
|
||||
user_id = filters["user_id"]
|
||||
agent_id = filters.get("agent_id", None)
|
||||
results = []
|
||||
|
||||
for item in to_be_deleted:
|
||||
source = item["source"]
|
||||
destination = item["destination"]
|
||||
relationship = item["relationship"]
|
||||
|
||||
# Build the agent filter for the query
|
||||
agent_filter = ""
|
||||
params = {
|
||||
"source_name": source,
|
||||
"dest_name": destination,
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
if agent_id:
|
||||
agent_filter = "AND n.agent_id = $agent_id AND m.agent_id = $agent_id"
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
# Delete the specific relationship between nodes
|
||||
cypher = f"""
|
||||
MATCH (n {self.node_label} {{name: $source_name, user_id: $user_id}})
|
||||
-[r:{relationship}]->
|
||||
(m {self.node_label} {{name: $dest_name, user_id: $user_id}})
|
||||
WHERE 1=1 {agent_filter}
|
||||
DELETE r
|
||||
RETURN
|
||||
n.name AS source,
|
||||
m.name AS target,
|
||||
type(r) AS relationship
|
||||
"""
|
||||
params = {
|
||||
"source_name": source,
|
||||
"dest_name": destination,
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
result = self.graph.query(cypher, params=params)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def _add_entities(self, to_be_added, user_id, entity_type_map):
|
||||
def _add_entities(self, to_be_added, filters, entity_type_map):
|
||||
"""Add the new entities to the graph. Merge the nodes if they already exist."""
|
||||
user_id = filters["user_id"]
|
||||
agent_id = filters.get("agent_id", None)
|
||||
results = []
|
||||
for item in to_be_added:
|
||||
# entities
|
||||
@@ -346,65 +386,80 @@ class MemoryGraph:
|
||||
dest_embedding = self.embedding_model.embed(destination)
|
||||
|
||||
# search for the nodes with the closest embeddings
|
||||
source_node_search_result = self._search_source_node(source_embedding, user_id, threshold=0.9)
|
||||
destination_node_search_result = self._search_destination_node(dest_embedding, user_id, threshold=0.9)
|
||||
source_node_search_result = self._search_source_node(source_embedding, filters, threshold=0.9)
|
||||
destination_node_search_result = self._search_destination_node(dest_embedding, filters, threshold=0.9)
|
||||
|
||||
# TODO: Create a cypher query and common params for all the cases
|
||||
if not destination_node_search_result and source_node_search_result:
|
||||
cypher = f"""
|
||||
MATCH (source)
|
||||
WHERE elementId(source) = $source_id
|
||||
SET source.mentions = coalesce(source.mentions, 0) + 1
|
||||
WITH source
|
||||
MERGE (destination {destination_label} {{name: $destination_name, user_id: $user_id}})
|
||||
ON CREATE SET
|
||||
destination.created = timestamp(),
|
||||
destination.mentions = 1
|
||||
{destination_extra_set}
|
||||
ON MATCH SET
|
||||
destination.mentions = coalesce(destination.mentions, 0) + 1
|
||||
WITH source, destination
|
||||
CALL db.create.setNodeVectorProperty(destination, 'embedding', $destination_embedding)
|
||||
WITH source, destination
|
||||
MERGE (source)-[r:{relationship}]->(destination)
|
||||
ON CREATE SET
|
||||
r.created = timestamp(),
|
||||
r.mentions = 1
|
||||
ON MATCH SET
|
||||
r.mentions = coalesce(r.mentions, 0) + 1
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
# Build destination MERGE properties
|
||||
merge_props = ["name: $destination_name", "user_id: $user_id"]
|
||||
if agent_id:
|
||||
merge_props.append("agent_id: $agent_id")
|
||||
merge_props_str = ", ".join(merge_props)
|
||||
|
||||
cypher = f"""
|
||||
MATCH (source)
|
||||
WHERE elementId(source) = $source_id
|
||||
SET source.mentions = coalesce(source.mentions, 0) + 1
|
||||
WITH source
|
||||
MERGE (destination {destination_label} {{{merge_props_str}}})
|
||||
ON CREATE SET
|
||||
destination.created = timestamp(),
|
||||
destination.mentions = 1
|
||||
{destination_extra_set}
|
||||
ON MATCH SET
|
||||
destination.mentions = coalesce(destination.mentions, 0) + 1
|
||||
WITH source, destination
|
||||
CALL db.create.setNodeVectorProperty(destination, 'embedding', $destination_embedding)
|
||||
WITH source, destination
|
||||
MERGE (source)-[r:{relationship}]->(destination)
|
||||
ON CREATE SET
|
||||
r.created = timestamp(),
|
||||
r.mentions = 1
|
||||
ON MATCH SET
|
||||
r.mentions = coalesce(r.mentions, 0) + 1
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
|
||||
params = {
|
||||
"source_id": source_node_search_result[0]["elementId(source_candidate)"],
|
||||
"destination_name": destination,
|
||||
"destination_embedding": dest_embedding,
|
||||
"user_id": user_id,
|
||||
}
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
elif destination_node_search_result and not source_node_search_result:
|
||||
# Build source MERGE properties
|
||||
merge_props = ["name: $source_name", "user_id: $user_id"]
|
||||
if agent_id:
|
||||
merge_props.append("agent_id: $agent_id")
|
||||
merge_props_str = ", ".join(merge_props)
|
||||
|
||||
cypher = f"""
|
||||
MATCH (destination)
|
||||
WHERE elementId(destination) = $destination_id
|
||||
SET destination.mentions = coalesce(destination.mentions, 0) + 1
|
||||
WITH destination
|
||||
MERGE (source {source_label} {{name: $source_name, user_id: $user_id}})
|
||||
ON CREATE SET
|
||||
source.created = timestamp(),
|
||||
source.mentions = 1
|
||||
{source_extra_set}
|
||||
ON MATCH SET
|
||||
source.mentions = coalesce(source.mentions, 0) + 1
|
||||
WITH source, destination
|
||||
CALL db.create.setNodeVectorProperty(source, 'embedding', $source_embedding)
|
||||
WITH source, destination
|
||||
MERGE (source)-[r:{relationship}]->(destination)
|
||||
ON CREATE SET
|
||||
r.created = timestamp(),
|
||||
r.mentions = 1
|
||||
ON MATCH SET
|
||||
r.mentions = coalesce(r.mentions, 0) + 1
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
MATCH (destination)
|
||||
WHERE elementId(destination) = $destination_id
|
||||
SET destination.mentions = coalesce(destination.mentions, 0) + 1
|
||||
WITH destination
|
||||
MERGE (source {source_label} {{{merge_props_str}}})
|
||||
ON CREATE SET
|
||||
source.created = timestamp(),
|
||||
source.mentions = 1
|
||||
{source_extra_set}
|
||||
ON MATCH SET
|
||||
source.mentions = coalesce(source.mentions, 0) + 1
|
||||
WITH source, destination
|
||||
CALL db.create.setNodeVectorProperty(source, 'embedding', $source_embedding)
|
||||
WITH source, destination
|
||||
MERGE (source)-[r:{relationship}]->(destination)
|
||||
ON CREATE SET
|
||||
r.created = timestamp(),
|
||||
r.mentions = 1
|
||||
ON MATCH SET
|
||||
r.mentions = coalesce(r.mentions, 0) + 1
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
|
||||
params = {
|
||||
"destination_id": destination_node_search_result[0]["elementId(destination_candidate)"],
|
||||
@@ -412,53 +467,68 @@ class MemoryGraph:
|
||||
"source_embedding": source_embedding,
|
||||
"user_id": user_id,
|
||||
}
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
elif source_node_search_result and destination_node_search_result:
|
||||
cypher = f"""
|
||||
MATCH (source)
|
||||
WHERE elementId(source) = $source_id
|
||||
SET source.mentions = coalesce(source.mentions, 0) + 1
|
||||
WITH source
|
||||
MATCH (destination)
|
||||
WHERE elementId(destination) = $destination_id
|
||||
SET destination.mentions = coalesce(destination.mentions) + 1
|
||||
MERGE (source)-[r:{relationship}]->(destination)
|
||||
ON CREATE SET
|
||||
r.created_at = timestamp(),
|
||||
r.updated_at = timestamp(),
|
||||
r.mentions = 1
|
||||
ON MATCH SET r.mentions = coalesce(r.mentions, 0) + 1
|
||||
|
||||
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
MATCH (source)
|
||||
WHERE elementId(source) = $source_id
|
||||
SET source.mentions = coalesce(source.mentions, 0) + 1
|
||||
WITH source
|
||||
MATCH (destination)
|
||||
WHERE elementId(destination) = $destination_id
|
||||
SET destination.mentions = coalesce(destination.mentions, 0) + 1
|
||||
MERGE (source)-[r:{relationship}]->(destination)
|
||||
ON CREATE SET
|
||||
r.created_at = timestamp(),
|
||||
r.updated_at = timestamp(),
|
||||
r.mentions = 1
|
||||
ON MATCH SET r.mentions = coalesce(r.mentions, 0) + 1
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
|
||||
params = {
|
||||
"source_id": source_node_search_result[0]["elementId(source_candidate)"],
|
||||
"destination_id": destination_node_search_result[0]["elementId(destination_candidate)"],
|
||||
"user_id": user_id,
|
||||
}
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
else:
|
||||
# Build dynamic MERGE props for both source and destination
|
||||
source_props = ["name: $source_name", "user_id: $user_id"]
|
||||
dest_props = ["name: $dest_name", "user_id: $user_id"]
|
||||
if agent_id:
|
||||
source_props.append("agent_id: $agent_id")
|
||||
dest_props.append("agent_id: $agent_id")
|
||||
source_props_str = ", ".join(source_props)
|
||||
dest_props_str = ", ".join(dest_props)
|
||||
|
||||
cypher = f"""
|
||||
MERGE (source {source_label} {{name: $source_name, user_id: $user_id}})
|
||||
ON CREATE SET source.created = timestamp(),
|
||||
source.mentions = 1
|
||||
{source_extra_set}
|
||||
ON MATCH SET source.mentions = coalesce(source.mentions, 0) + 1
|
||||
WITH source
|
||||
CALL db.create.setNodeVectorProperty(source, 'embedding', $source_embedding)
|
||||
WITH source
|
||||
MERGE (destination {destination_label} {{name: $dest_name, user_id: $user_id}})
|
||||
ON CREATE SET destination.created = timestamp(),
|
||||
destination.mentions = 1
|
||||
{destination_extra_set}
|
||||
ON MATCH SET destination.mentions = coalesce(destination.mentions, 0) + 1
|
||||
WITH source, destination
|
||||
CALL db.create.setNodeVectorProperty(destination, 'embedding', $source_embedding)
|
||||
WITH source, destination
|
||||
MERGE (source)-[rel:{relationship}]->(destination)
|
||||
ON CREATE SET rel.created = timestamp(), rel.mentions = 1
|
||||
ON MATCH SET rel.mentions = coalesce(rel.mentions, 0) + 1
|
||||
RETURN source.name AS source, type(rel) AS relationship, destination.name AS target
|
||||
"""
|
||||
MERGE (source {source_label} {{{source_props_str}}})
|
||||
ON CREATE SET source.created = timestamp(),
|
||||
source.mentions = 1
|
||||
{source_extra_set}
|
||||
ON MATCH SET source.mentions = coalesce(source.mentions, 0) + 1
|
||||
WITH source
|
||||
CALL db.create.setNodeVectorProperty(source, 'embedding', $source_embedding)
|
||||
WITH source
|
||||
MERGE (destination {destination_label} {{{dest_props_str}}})
|
||||
ON CREATE SET destination.created = timestamp(),
|
||||
destination.mentions = 1
|
||||
{destination_extra_set}
|
||||
ON MATCH SET destination.mentions = coalesce(destination.mentions, 0) + 1
|
||||
WITH source, destination
|
||||
CALL db.create.setNodeVectorProperty(destination, 'embedding', $dest_embedding)
|
||||
WITH source, destination
|
||||
MERGE (source)-[rel:{relationship}]->(destination)
|
||||
ON CREATE SET rel.created = timestamp(), rel.mentions = 1
|
||||
ON MATCH SET rel.mentions = coalesce(rel.mentions, 0) + 1
|
||||
RETURN source.name AS source, type(rel) AS relationship, destination.name AS target
|
||||
"""
|
||||
|
||||
params = {
|
||||
"source_name": source,
|
||||
"dest_name": destination,
|
||||
@@ -466,6 +536,8 @@ class MemoryGraph:
|
||||
"dest_embedding": dest_embedding,
|
||||
"user_id": user_id,
|
||||
}
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
result = self.graph.query(cypher, params=params)
|
||||
results.append(result)
|
||||
return results
|
||||
@@ -477,11 +549,16 @@ class MemoryGraph:
|
||||
item["destination"] = item["destination"].lower().replace(" ", "_")
|
||||
return entity_list
|
||||
|
||||
def _search_source_node(self, source_embedding, user_id, threshold=0.9):
|
||||
def _search_source_node(self, source_embedding, filters, threshold=0.9):
|
||||
agent_filter = ""
|
||||
if filters.get("agent_id"):
|
||||
agent_filter = "AND source_candidate.agent_id = $agent_id"
|
||||
|
||||
cypher = f"""
|
||||
MATCH (source_candidate {self.node_label})
|
||||
WHERE source_candidate.embedding IS NOT NULL
|
||||
AND source_candidate.user_id = $user_id
|
||||
{agent_filter}
|
||||
|
||||
WITH source_candidate,
|
||||
round(2 * vector.similarity.cosine(source_candidate.embedding, $source_embedding) - 1, 4) AS source_similarity // denormalize for backward compatibility
|
||||
@@ -496,18 +573,26 @@ class MemoryGraph:
|
||||
|
||||
params = {
|
||||
"source_embedding": source_embedding,
|
||||
"user_id": user_id,
|
||||
"user_id": filters["user_id"],
|
||||
"threshold": threshold,
|
||||
}
|
||||
if filters.get("agent_id"):
|
||||
params["agent_id"] = filters["agent_id"]
|
||||
|
||||
result = self.graph.query(cypher, params=params)
|
||||
return result
|
||||
|
||||
def _search_destination_node(self, destination_embedding, user_id, threshold=0.9):
|
||||
|
||||
def _search_destination_node(self, destination_embedding, filters, threshold=0.9):
|
||||
agent_filter = ""
|
||||
if filters.get("agent_id"):
|
||||
agent_filter = "AND destination_candidate.agent_id = $agent_id"
|
||||
|
||||
cypher = f"""
|
||||
MATCH (destination_candidate {self.node_label})
|
||||
WHERE destination_candidate.embedding IS NOT NULL
|
||||
AND destination_candidate.user_id = $user_id
|
||||
{agent_filter}
|
||||
|
||||
WITH destination_candidate,
|
||||
round(2 * vector.similarity.cosine(destination_candidate.embedding, $destination_embedding) - 1, 4) AS destination_similarity // denormalize for backward compatibility
|
||||
@@ -520,11 +605,14 @@ class MemoryGraph:
|
||||
|
||||
RETURN elementId(destination_candidate)
|
||||
"""
|
||||
|
||||
params = {
|
||||
"destination_embedding": destination_embedding,
|
||||
"user_id": user_id,
|
||||
"user_id": filters["user_id"],
|
||||
"threshold": threshold,
|
||||
}
|
||||
if filters.get("agent_id"):
|
||||
params["agent_id"] = filters["agent_id"]
|
||||
|
||||
result = self.graph.query(cypher, params=params)
|
||||
return result
|
||||
|
||||
+45
-49
@@ -28,8 +28,8 @@ from mem0.memory.utils import (
|
||||
get_fact_retrieval_messages,
|
||||
parse_messages,
|
||||
parse_vision_messages,
|
||||
remove_code_blocks,
|
||||
process_telemetry_filters,
|
||||
remove_code_blocks,
|
||||
)
|
||||
from mem0.utils.factory import EmbedderFactory, LlmFactory, VectorStoreFactory
|
||||
|
||||
@@ -338,10 +338,9 @@ class Memory(MemoryBase):
|
||||
except Exception as e:
|
||||
logging.error(f"Error in new_retrieved_facts: {e}")
|
||||
new_retrieved_facts = []
|
||||
|
||||
|
||||
if not new_retrieved_facts:
|
||||
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
|
||||
return []
|
||||
|
||||
retrieved_old_memory = []
|
||||
new_message_embeddings = {}
|
||||
@@ -369,24 +368,27 @@ class Memory(MemoryBase):
|
||||
temp_uuid_mapping[str(idx)] = item["id"]
|
||||
retrieved_old_memory[idx]["id"] = str(idx)
|
||||
|
||||
function_calling_prompt = get_update_memory_messages(
|
||||
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
|
||||
)
|
||||
|
||||
try:
|
||||
response: str = self.llm.generate_response(
|
||||
messages=[{"role": "user", "content": function_calling_prompt}],
|
||||
response_format={"type": "json_object"},
|
||||
if new_retrieved_facts:
|
||||
function_calling_prompt = get_update_memory_messages(
|
||||
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in new memory actions response: {e}")
|
||||
response = ""
|
||||
|
||||
try:
|
||||
response = remove_code_blocks(response)
|
||||
new_memories_with_actions = json.loads(response)
|
||||
except Exception as e:
|
||||
logging.error(f"Invalid JSON response: {e}")
|
||||
try:
|
||||
response: str = self.llm.generate_response(
|
||||
messages=[{"role": "user", "content": function_calling_prompt}],
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in new memory actions response: {e}")
|
||||
response = ""
|
||||
|
||||
try:
|
||||
response = remove_code_blocks(response)
|
||||
new_memories_with_actions = json.loads(response)
|
||||
except Exception as e:
|
||||
logging.error(f"Invalid JSON response: {e}")
|
||||
new_memories_with_actions = {}
|
||||
else:
|
||||
new_memories_with_actions = {}
|
||||
|
||||
returned_memories = []
|
||||
@@ -1162,13 +1164,11 @@ class AsyncMemory(MemoryBase):
|
||||
response = remove_code_blocks(response)
|
||||
new_retrieved_facts = json.loads(response)["facts"]
|
||||
except Exception as e:
|
||||
new_retrieved_facts = []
|
||||
|
||||
if not new_retrieved_facts:
|
||||
logger.info("No new facts retrieved from input. Skipping memory update LLM call.")
|
||||
return []
|
||||
logging.error(f"Error in new_retrieved_facts: {e}")
|
||||
new_retrieved_facts = []
|
||||
|
||||
if not new_retrieved_facts:
|
||||
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
|
||||
|
||||
retrieved_old_memory = []
|
||||
new_message_embeddings = {}
|
||||
@@ -1200,31 +1200,25 @@ class AsyncMemory(MemoryBase):
|
||||
temp_uuid_mapping[str(idx)] = item["id"]
|
||||
retrieved_old_memory[idx]["id"] = str(idx)
|
||||
|
||||
function_calling_prompt = get_update_memory_messages(
|
||||
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
|
||||
)
|
||||
try:
|
||||
response = await asyncio.to_thread(
|
||||
self.llm.generate_response,
|
||||
messages=[{"role": "user", "content": function_calling_prompt}],
|
||||
response_format={"type": "json_object"},
|
||||
if new_retrieved_facts:
|
||||
function_calling_prompt = get_update_memory_messages(
|
||||
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
|
||||
)
|
||||
except Exception as e:
|
||||
response = ""
|
||||
logging.error(f"Error in new memory actions response: {e}")
|
||||
response = ""
|
||||
try:
|
||||
response = remove_code_blocks(response)
|
||||
new_memories_with_actions = json.loads(response)
|
||||
except Exception as e:
|
||||
new_memories_with_actions = {}
|
||||
|
||||
if not new_memories_with_actions:
|
||||
logger.info("No new facts retrieved from input (async). Skipping memory update LLM call.")
|
||||
return []
|
||||
|
||||
logging.error(f"Invalid JSON response: {e}")
|
||||
new_memories_with_actions = {}
|
||||
try:
|
||||
response = await asyncio.to_thread(
|
||||
self.llm.generate_response,
|
||||
messages=[{"role": "user", "content": function_calling_prompt}],
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in new memory actions response: {e}")
|
||||
response = ""
|
||||
try:
|
||||
response = remove_code_blocks(response)
|
||||
new_memories_with_actions = json.loads(response)
|
||||
except Exception as e:
|
||||
logging.error(f"Invalid JSON response: {e}")
|
||||
new_memories_with_actions = {}
|
||||
|
||||
returned_memories = []
|
||||
try:
|
||||
@@ -1421,7 +1415,9 @@ class AsyncMemory(MemoryBase):
|
||||
async def _get_all_from_vector_store(self, filters, limit):
|
||||
memories_result = await asyncio.to_thread(self.vector_store.list, filters=filters, limit=limit)
|
||||
actual_memories = (
|
||||
memories_result[0] if isinstance(memories_result, tuple) and len(memories_result) > 0 else memories_result
|
||||
memories_result[0]
|
||||
if isinstance(memories_result, (tuple, list)) and len(memories_result) > 0
|
||||
else memories_result
|
||||
)
|
||||
|
||||
promoted_payload_keys = [
|
||||
|
||||
+211
-86
@@ -118,11 +118,19 @@ class MemoryGraph:
|
||||
return search_results
|
||||
|
||||
def delete_all(self, filters):
|
||||
cypher = """
|
||||
MATCH (n {user_id: $user_id})
|
||||
DETACH DELETE n
|
||||
"""
|
||||
params = {"user_id": filters["user_id"]}
|
||||
"""Delete all nodes and relationships for a user or specific agent."""
|
||||
if filters.get("agent_id"):
|
||||
cypher = """
|
||||
MATCH (n:Entity {user_id: $user_id, agent_id: $agent_id})
|
||||
DETACH DELETE n
|
||||
"""
|
||||
params = {"user_id": filters["user_id"], "agent_id": filters["agent_id"]}
|
||||
else:
|
||||
cypher = """
|
||||
MATCH (n:Entity {user_id: $user_id})
|
||||
DETACH DELETE n
|
||||
"""
|
||||
params = {"user_id": filters["user_id"]}
|
||||
self.graph.query(cypher, params=params)
|
||||
|
||||
def get_all(self, filters, limit=100):
|
||||
@@ -131,20 +139,31 @@ class MemoryGraph:
|
||||
|
||||
Args:
|
||||
filters (dict): A dictionary containing filters to be applied during the retrieval.
|
||||
Supports 'user_id' (required) and 'agent_id' (optional).
|
||||
limit (int): The maximum number of nodes and relationships to retrieve. Defaults to 100.
|
||||
Returns:
|
||||
list: A list of dictionaries, each containing:
|
||||
- 'contexts': The base data store response for each memory.
|
||||
- 'entities': A list of strings representing the nodes and relationships
|
||||
- 'source': The source node name.
|
||||
- 'relationship': The relationship type.
|
||||
- 'target': The target node name.
|
||||
"""
|
||||
|
||||
# return all nodes and relationships
|
||||
query = """
|
||||
MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity {user_id: $user_id})
|
||||
RETURN n.name AS source, type(r) AS relationship, m.name AS target
|
||||
LIMIT $limit
|
||||
"""
|
||||
results = self.graph.query(query, params={"user_id": filters["user_id"], "limit": limit})
|
||||
# Build query based on whether agent_id is provided
|
||||
if filters.get("agent_id"):
|
||||
query = """
|
||||
MATCH (n:Entity {user_id: $user_id, agent_id: $agent_id})-[r]->(m:Entity {user_id: $user_id, agent_id: $agent_id})
|
||||
RETURN n.name AS source, type(r) AS relationship, m.name AS target
|
||||
LIMIT $limit
|
||||
"""
|
||||
params = {"user_id": filters["user_id"], "agent_id": filters["agent_id"], "limit": limit}
|
||||
else:
|
||||
query = """
|
||||
MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity {user_id: $user_id})
|
||||
RETURN n.name AS source, type(r) AS relationship, m.name AS target
|
||||
LIMIT $limit
|
||||
"""
|
||||
params = {"user_id": filters["user_id"], "limit": limit}
|
||||
|
||||
results = self.graph.query(query, params=params)
|
||||
|
||||
final_results = []
|
||||
for result in results:
|
||||
@@ -241,33 +260,65 @@ class MemoryGraph:
|
||||
for node in node_list:
|
||||
n_embedding = self.embedding_model.embed(node)
|
||||
|
||||
cypher_query = """
|
||||
MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
|
||||
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
|
||||
YIELD node1, node2, similarity
|
||||
WITH node1, node2, similarity, r
|
||||
WHERE similarity >= $threshold
|
||||
RETURN node1.user_id AS source, id(node1) AS source_id, type(r) AS relationship, id(r) AS relation_id, node2.user_id AS destination, id(node2) AS destination_id, similarity
|
||||
UNION
|
||||
MATCH (n:Entity {user_id: $user_id})<-[r]-(m:Entity)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
|
||||
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
|
||||
YIELD node1, node2, similarity
|
||||
WITH node1, node2, similarity, r
|
||||
WHERE similarity >= $threshold
|
||||
RETURN node2.name AS source, id(node2) AS source_id, type(r) AS relationship, id(r) AS relation_id, node1.name AS destination, id(node1) AS destination_id, similarity
|
||||
ORDER BY similarity DESC
|
||||
LIMIT $limit;
|
||||
"""
|
||||
params = {
|
||||
"n_embedding": n_embedding,
|
||||
"threshold": self.threshold,
|
||||
"user_id": filters["user_id"],
|
||||
"limit": limit,
|
||||
}
|
||||
# Build query based on whether agent_id is provided
|
||||
if filters.get("agent_id"):
|
||||
cypher_query = """
|
||||
MATCH (n:Entity {user_id: $user_id, agent_id: $agent_id})-[r]->(m:Entity)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
|
||||
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
|
||||
YIELD node1, node2, similarity
|
||||
WITH node1, node2, similarity, r
|
||||
WHERE similarity >= $threshold
|
||||
RETURN node1.name AS source, id(node1) AS source_id, type(r) AS relationship, id(r) AS relation_id, node2.name AS destination, id(node2) AS destination_id, similarity
|
||||
UNION
|
||||
MATCH (n:Entity {user_id: $user_id, agent_id: $agent_id})<-[r]-(m:Entity)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
|
||||
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
|
||||
YIELD node1, node2, similarity
|
||||
WITH node1, node2, similarity, r
|
||||
WHERE similarity >= $threshold
|
||||
RETURN node2.name AS source, id(node2) AS source_id, type(r) AS relationship, id(r) AS relation_id, node1.name AS destination, id(node1) AS destination_id, similarity
|
||||
ORDER BY similarity DESC
|
||||
LIMIT $limit;
|
||||
"""
|
||||
params = {
|
||||
"n_embedding": n_embedding,
|
||||
"threshold": self.threshold,
|
||||
"user_id": filters["user_id"],
|
||||
"agent_id": filters["agent_id"],
|
||||
"limit": limit,
|
||||
}
|
||||
else:
|
||||
cypher_query = """
|
||||
MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
|
||||
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
|
||||
YIELD node1, node2, similarity
|
||||
WITH node1, node2, similarity, r
|
||||
WHERE similarity >= $threshold
|
||||
RETURN node1.name AS source, id(node1) AS source_id, type(r) AS relationship, id(r) AS relation_id, node2.name AS destination, id(node2) AS destination_id, similarity
|
||||
UNION
|
||||
MATCH (n:Entity {user_id: $user_id})<-[r]-(m:Entity)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
|
||||
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
|
||||
YIELD node1, node2, similarity
|
||||
WITH node1, node2, similarity, r
|
||||
WHERE similarity >= $threshold
|
||||
RETURN node2.name AS source, id(node2) AS source_id, type(r) AS relationship, id(r) AS relation_id, node1.name AS destination, id(node1) AS destination_id, similarity
|
||||
ORDER BY similarity DESC
|
||||
LIMIT $limit;
|
||||
"""
|
||||
params = {
|
||||
"n_embedding": n_embedding,
|
||||
"threshold": self.threshold,
|
||||
"user_id": filters["user_id"],
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
ans = self.graph.query(cypher_query, params=params)
|
||||
result_relations.extend(ans)
|
||||
|
||||
@@ -300,38 +351,54 @@ class MemoryGraph:
|
||||
logger.debug(f"Deleted relationships: {to_be_deleted}")
|
||||
return to_be_deleted
|
||||
|
||||
def _delete_entities(self, to_be_deleted, user_id):
|
||||
def _delete_entities(self, to_be_deleted, filters):
|
||||
"""Delete the entities from the graph."""
|
||||
user_id = filters["user_id"]
|
||||
agent_id = filters.get("agent_id", None)
|
||||
results = []
|
||||
|
||||
for item in to_be_deleted:
|
||||
source = item["source"]
|
||||
destination = item["destination"]
|
||||
relationship = item["relationship"]
|
||||
|
||||
# Build the agent filter for the query
|
||||
agent_filter = ""
|
||||
params = {
|
||||
"source_name": source,
|
||||
"dest_name": destination,
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
if agent_id:
|
||||
agent_filter = "AND n.agent_id = $agent_id AND m.agent_id = $agent_id"
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
# Delete the specific relationship between nodes
|
||||
cypher = f"""
|
||||
MATCH (n:Entity {{name: $source_name, user_id: $user_id}})
|
||||
-[r:{relationship}]->
|
||||
(m {{name: $dest_name, user_id: $user_id}})
|
||||
(m:Entity {{name: $dest_name, user_id: $user_id}})
|
||||
WHERE 1=1 {agent_filter}
|
||||
DELETE r
|
||||
RETURN
|
||||
n.name AS source,
|
||||
m.name AS target,
|
||||
type(r) AS relationship
|
||||
"""
|
||||
params = {
|
||||
"source_name": source,
|
||||
"dest_name": destination,
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
result = self.graph.query(cypher, params=params)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
# added Entity label to all nodes for vector search to work
|
||||
def _add_entities(self, to_be_added, user_id, entity_type_map):
|
||||
def _add_entities(self, to_be_added, filters, entity_type_map):
|
||||
"""Add the new entities to the graph. Merge the nodes if they already exist."""
|
||||
user_id = filters["user_id"]
|
||||
agent_id = filters.get("agent_id", None)
|
||||
results = []
|
||||
|
||||
for item in to_be_added:
|
||||
# entities
|
||||
source = item["source"]
|
||||
@@ -346,18 +413,21 @@ class MemoryGraph:
|
||||
source_embedding = self.embedding_model.embed(source)
|
||||
dest_embedding = self.embedding_model.embed(destination)
|
||||
|
||||
# search for the nodes with the closest embeddings; this is basically
|
||||
# comparison of one embedding to all embeddings in a graph -> vector
|
||||
# search with cosine similarity metric
|
||||
source_node_search_result = self._search_source_node(source_embedding, user_id, threshold=0.9)
|
||||
destination_node_search_result = self._search_destination_node(dest_embedding, user_id, threshold=0.9)
|
||||
# search for the nodes with the closest embeddings
|
||||
source_node_search_result = self._search_source_node(source_embedding, filters, threshold=0.9)
|
||||
destination_node_search_result = self._search_destination_node(dest_embedding, filters, threshold=0.9)
|
||||
|
||||
# Prepare agent_id for node creation
|
||||
agent_id_clause = ""
|
||||
if agent_id:
|
||||
agent_id_clause = ", agent_id: $agent_id"
|
||||
|
||||
# TODO: Create a cypher query and common params for all the cases
|
||||
if not destination_node_search_result and source_node_search_result:
|
||||
cypher = f"""
|
||||
MATCH (source:Entity)
|
||||
WHERE id(source) = $source_id
|
||||
MERGE (destination:{destination_type}:Entity {{name: $destination_name, user_id: $user_id}})
|
||||
MERGE (destination:{destination_type}:Entity {{name: $destination_name, user_id: $user_id{agent_id_clause}}})
|
||||
ON CREATE SET
|
||||
destination.created = timestamp(),
|
||||
destination.embedding = $destination_embedding,
|
||||
@@ -374,11 +444,14 @@ class MemoryGraph:
|
||||
"destination_embedding": dest_embedding,
|
||||
"user_id": user_id,
|
||||
}
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
elif destination_node_search_result and not source_node_search_result:
|
||||
cypher = f"""
|
||||
MATCH (destination:Entity)
|
||||
WHERE id(destination) = $destination_id
|
||||
MERGE (source:{source_type}:Entity {{name: $source_name, user_id: $user_id}})
|
||||
MERGE (source:{source_type}:Entity {{name: $source_name, user_id: $user_id{agent_id_clause}}})
|
||||
ON CREATE SET
|
||||
source.created = timestamp(),
|
||||
source.embedding = $source_embedding,
|
||||
@@ -395,6 +468,9 @@ class MemoryGraph:
|
||||
"source_embedding": source_embedding,
|
||||
"user_id": user_id,
|
||||
}
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
elif source_node_search_result and destination_node_search_result:
|
||||
cypher = f"""
|
||||
MATCH (source:Entity)
|
||||
@@ -412,12 +488,15 @@ class MemoryGraph:
|
||||
"destination_id": destination_node_search_result[0]["id(destination_candidate)"],
|
||||
"user_id": user_id,
|
||||
}
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
else:
|
||||
cypher = f"""
|
||||
MERGE (n:{source_type}:Entity {{name: $source_name, user_id: $user_id}})
|
||||
MERGE (n:{source_type}:Entity {{name: $source_name, user_id: $user_id{agent_id_clause}}})
|
||||
ON CREATE SET n.created = timestamp(), n.embedding = $source_embedding, n:Entity
|
||||
ON MATCH SET n.embedding = $source_embedding
|
||||
MERGE (m:{destination_type}:Entity {{name: $dest_name, user_id: $user_id}})
|
||||
MERGE (m:{destination_type}:Entity {{name: $dest_name, user_id: $user_id{agent_id_clause}}})
|
||||
ON CREATE SET m.created = timestamp(), m.embedding = $dest_embedding, m:Entity
|
||||
ON MATCH SET m.embedding = $dest_embedding
|
||||
MERGE (n)-[rel:{relationship}]->(m)
|
||||
@@ -431,6 +510,9 @@ class MemoryGraph:
|
||||
"dest_embedding": dest_embedding,
|
||||
"user_id": user_id,
|
||||
}
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
result = self.graph.query(cypher, params=params)
|
||||
results.append(result)
|
||||
return results
|
||||
@@ -442,37 +524,80 @@ class MemoryGraph:
|
||||
item["destination"] = item["destination"].lower().replace(" ", "_")
|
||||
return entity_list
|
||||
|
||||
def _search_source_node(self, source_embedding, user_id, threshold=0.9):
|
||||
cypher = """
|
||||
CALL vector_search.search("memzero", 1, $source_embedding)
|
||||
YIELD distance, node, similarity
|
||||
WITH node AS source_candidate, similarity
|
||||
WHERE source_candidate.user_id = $user_id AND similarity >= $threshold
|
||||
RETURN id(source_candidate);
|
||||
"""
|
||||
|
||||
params = {
|
||||
"source_embedding": source_embedding,
|
||||
"user_id": user_id,
|
||||
"threshold": threshold,
|
||||
}
|
||||
def _search_source_node(self, source_embedding, filters, threshold=0.9):
|
||||
"""Search for source nodes with similar embeddings."""
|
||||
user_id = filters["user_id"]
|
||||
agent_id = filters.get("agent_id", None)
|
||||
|
||||
if agent_id:
|
||||
cypher = """
|
||||
CALL vector_search.search("memzero", 1, $source_embedding)
|
||||
YIELD distance, node, similarity
|
||||
WITH node AS source_candidate, similarity
|
||||
WHERE source_candidate.user_id = $user_id
|
||||
AND source_candidate.agent_id = $agent_id
|
||||
AND similarity >= $threshold
|
||||
RETURN id(source_candidate);
|
||||
"""
|
||||
params = {
|
||||
"source_embedding": source_embedding,
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"threshold": threshold,
|
||||
}
|
||||
else:
|
||||
cypher = """
|
||||
CALL vector_search.search("memzero", 1, $source_embedding)
|
||||
YIELD distance, node, similarity
|
||||
WITH node AS source_candidate, similarity
|
||||
WHERE source_candidate.user_id = $user_id
|
||||
AND similarity >= $threshold
|
||||
RETURN id(source_candidate);
|
||||
"""
|
||||
params = {
|
||||
"source_embedding": source_embedding,
|
||||
"user_id": user_id,
|
||||
"threshold": threshold,
|
||||
}
|
||||
|
||||
result = self.graph.query(cypher, params=params)
|
||||
return result
|
||||
|
||||
def _search_destination_node(self, destination_embedding, user_id, threshold=0.9):
|
||||
cypher = """
|
||||
CALL vector_search.search("memzero", 1, $destination_embedding)
|
||||
YIELD distance, node, similarity
|
||||
WITH node AS destination_candidate, similarity
|
||||
WHERE node.user_id = $user_id AND similarity >= $threshold
|
||||
RETURN id(destination_candidate);
|
||||
"""
|
||||
params = {
|
||||
"destination_embedding": destination_embedding,
|
||||
"user_id": user_id,
|
||||
"threshold": threshold,
|
||||
}
|
||||
def _search_destination_node(self, destination_embedding, filters, threshold=0.9):
|
||||
"""Search for destination nodes with similar embeddings."""
|
||||
user_id = filters["user_id"]
|
||||
agent_id = filters.get("agent_id", None)
|
||||
|
||||
if agent_id:
|
||||
cypher = """
|
||||
CALL vector_search.search("memzero", 1, $destination_embedding)
|
||||
YIELD distance, node, similarity
|
||||
WITH node AS destination_candidate, similarity
|
||||
WHERE node.user_id = $user_id
|
||||
AND node.agent_id = $agent_id
|
||||
AND similarity >= $threshold
|
||||
RETURN id(destination_candidate);
|
||||
"""
|
||||
params = {
|
||||
"destination_embedding": destination_embedding,
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"threshold": threshold,
|
||||
}
|
||||
else:
|
||||
cypher = """
|
||||
CALL vector_search.search("memzero", 1, $destination_embedding)
|
||||
YIELD distance, node, similarity
|
||||
WITH node AS destination_candidate, similarity
|
||||
WHERE node.user_id = $user_id
|
||||
AND similarity >= $threshold
|
||||
RETURN id(destination_candidate);
|
||||
"""
|
||||
params = {
|
||||
"destination_embedding": destination_embedding,
|
||||
"user_id": user_id,
|
||||
"threshold": threshold,
|
||||
}
|
||||
|
||||
result = self.graph.query(cypher, params=params)
|
||||
return result
|
||||
|
||||
@@ -79,6 +79,7 @@ class VectorStoreFactory:
|
||||
"upstash_vector": "mem0.vector_stores.upstash_vector.UpstashVector",
|
||||
"azure_ai_search": "mem0.vector_stores.azure_ai_search.AzureAISearch",
|
||||
"pinecone": "mem0.vector_stores.pinecone.PineconeDB",
|
||||
"mongodb": "mem0.vector_stores.mongodb.MongoDB",
|
||||
"redis": "mem0.vector_stores.redis.RedisDB",
|
||||
"elasticsearch": "mem0.vector_stores.elasticsearch.ElasticsearchDB",
|
||||
"vertex_ai_vector_search": "mem0.vector_stores.vertex_ai_vector_search.GoogleMatchingEngine",
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mem0.vector_stores.base import VectorStoreBase
|
||||
|
||||
try:
|
||||
import pymochow
|
||||
from pymochow.configuration import Configuration
|
||||
from pymochow.auth.bce_credentials import BceCredentials
|
||||
from pymochow.model.enum import FieldType, MetricType, IndexType, TableState, ServerErrCode
|
||||
from pymochow.model.schema import Field, Schema, VectorIndex, FilteringIndex, HNSWParams, AutoBuildRowCountIncrement
|
||||
from pymochow.model.table import Partition, Row, VectorSearchConfig, VectorTopkSearchRequest, FloatVector
|
||||
from pymochow.exception import ServerError
|
||||
except ImportError:
|
||||
raise ImportError("The 'pymochow' library is required. Please install it using 'pip install pymochow'.")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OutputData(BaseModel):
|
||||
id: Optional[str] # memory id
|
||||
score: Optional[float] # distance
|
||||
payload: Optional[Dict] # metadata
|
||||
|
||||
|
||||
class BaiduDB(VectorStoreBase):
|
||||
def __init__(
|
||||
self,
|
||||
endpoint: str,
|
||||
account: str,
|
||||
api_key: str,
|
||||
database_name: str,
|
||||
table_name: str,
|
||||
embedding_model_dims: int,
|
||||
metric_type: MetricType,
|
||||
) -> None:
|
||||
"""Initialize the BaiduDB database.
|
||||
|
||||
Args:
|
||||
endpoint (str): Endpoint URL for Baidu VectorDB.
|
||||
account (str): Account for Baidu VectorDB.
|
||||
api_key (str): API Key for Baidu VectorDB.
|
||||
database_name (str): Name of the database.
|
||||
table_name (str): Name of the table.
|
||||
embedding_model_dims (int): Dimensions of the embedding model.
|
||||
metric_type (MetricType): Metric type for similarity search.
|
||||
"""
|
||||
self.endpoint = endpoint
|
||||
self.account = account
|
||||
self.api_key = api_key
|
||||
self.database_name = database_name
|
||||
self.table_name = table_name
|
||||
self.embedding_model_dims = embedding_model_dims
|
||||
self.metric_type = metric_type
|
||||
|
||||
# Initialize Mochow client
|
||||
config = Configuration(credentials=BceCredentials(account, api_key), endpoint=endpoint)
|
||||
self.client = pymochow.MochowClient(config)
|
||||
|
||||
# Ensure database and table exist
|
||||
self._create_database_if_not_exists()
|
||||
self.create_col(
|
||||
name=self.table_name,
|
||||
vector_size=self.embedding_model_dims,
|
||||
distance=self.metric_type,
|
||||
)
|
||||
|
||||
def _create_database_if_not_exists(self):
|
||||
"""Create database if it doesn't exist."""
|
||||
try:
|
||||
# Check if database exists
|
||||
databases = self.client.list_databases()
|
||||
db_exists = any(db.database_name == self.database_name for db in databases)
|
||||
if not db_exists:
|
||||
self._database = self.client.create_database(self.database_name)
|
||||
logger.info(f"Created database: {self.database_name}")
|
||||
else:
|
||||
self._database = self.client.database(self.database_name)
|
||||
logger.info(f"Database {self.database_name} already exists")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating database: {e}")
|
||||
raise
|
||||
|
||||
def create_col(self, name, vector_size, distance):
|
||||
"""Create a new table.
|
||||
|
||||
Args:
|
||||
name (str): Name of the table to create.
|
||||
vector_size (int): Dimension of the vector.
|
||||
distance (str): Metric type for similarity search.
|
||||
"""
|
||||
# Check if table already exists
|
||||
try:
|
||||
tables = self._database.list_table()
|
||||
table_exists = any(table.table_name == name for table in tables)
|
||||
if table_exists:
|
||||
logger.info(f"Table {name} already exists. Skipping creation.")
|
||||
self._table = self._database.describe_table(name)
|
||||
return
|
||||
|
||||
# Convert distance string to MetricType enum
|
||||
metric_type = None
|
||||
for k, v in MetricType.__members__.items():
|
||||
if k == distance:
|
||||
metric_type = v
|
||||
if metric_type is None:
|
||||
raise ValueError(f"Unsupported metric_type: {distance}")
|
||||
|
||||
# Define table schema
|
||||
fields = [
|
||||
Field(
|
||||
"id", FieldType.STRING, primary_key=True, partition_key=True, auto_increment=False, not_null=True
|
||||
),
|
||||
Field("vector", FieldType.FLOAT_VECTOR, dimension=vector_size),
|
||||
Field("metadata", FieldType.JSON),
|
||||
]
|
||||
|
||||
# Create vector index
|
||||
indexes = [
|
||||
VectorIndex(
|
||||
index_name="vector_idx",
|
||||
index_type=IndexType.HNSW,
|
||||
field="vector",
|
||||
metric_type=metric_type,
|
||||
params=HNSWParams(m=16, efconstruction=200),
|
||||
auto_build=True,
|
||||
auto_build_index_policy=AutoBuildRowCountIncrement(row_count_increment=10000),
|
||||
),
|
||||
FilteringIndex(index_name="metadata_filtering_idx", fields=["metadata"]),
|
||||
]
|
||||
|
||||
schema = Schema(fields=fields, indexes=indexes)
|
||||
|
||||
# Create table
|
||||
self._table = self._database.create_table(
|
||||
table_name=name, replication=3, partition=Partition(partition_num=1), schema=schema
|
||||
)
|
||||
logger.info(f"Created table: {name}")
|
||||
|
||||
# Wait for table to be ready
|
||||
while True:
|
||||
time.sleep(2)
|
||||
table = self._database.describe_table(name)
|
||||
if table.state == TableState.NORMAL:
|
||||
logger.info(f"Table {name} is ready.")
|
||||
break
|
||||
logger.info(f"Waiting for table {name} to be ready, current state: {table.state}")
|
||||
self._table = table
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating table: {e}")
|
||||
raise
|
||||
|
||||
def insert(self, vectors, payloads=None, ids=None):
|
||||
"""Insert vectors into the table.
|
||||
|
||||
Args:
|
||||
vectors (List[List[float]]): List of vectors to insert.
|
||||
payloads (List[Dict], optional): List of payloads corresponding to vectors.
|
||||
ids (List[str], optional): List of IDs corresponding to vectors.
|
||||
"""
|
||||
# Prepare data for insertion
|
||||
for idx, vector, metadata in zip(ids, vectors, payloads):
|
||||
row = Row(id=idx, vector=vector, metadata=metadata)
|
||||
self._table.upsert(rows=[row])
|
||||
|
||||
def search(self, query: str, vectors: list, limit: int = 5, filters: dict = None) -> list:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (str): Query string.
|
||||
vectors (List[float]): Query vector.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (Dict, optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
Returns:
|
||||
list: Search results.
|
||||
"""
|
||||
# Add filters if provided
|
||||
search_filter = None
|
||||
if filters:
|
||||
search_filter = self._create_filter(filters)
|
||||
|
||||
# Create AnnSearch for vector search
|
||||
request = VectorTopkSearchRequest(
|
||||
vector_field="vector",
|
||||
vector=FloatVector(vectors),
|
||||
limit=limit,
|
||||
filter=search_filter,
|
||||
config=VectorSearchConfig(ef=200),
|
||||
)
|
||||
|
||||
# Perform search
|
||||
projections = ["id", "metadata"]
|
||||
res = self._table.vector_search(request=request, projections=projections)
|
||||
|
||||
# Parse results
|
||||
output = []
|
||||
for row in res.rows:
|
||||
row_data = row.get("row", {})
|
||||
output_data = OutputData(
|
||||
id=row_data.get("id"), score=row.get("score", 0.0), payload=row_data.get("metadata", {})
|
||||
)
|
||||
output.append(output_data)
|
||||
|
||||
return output
|
||||
|
||||
def delete(self, vector_id):
|
||||
"""
|
||||
Delete a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to delete.
|
||||
"""
|
||||
self._table.delete(primary_key={"id": vector_id})
|
||||
|
||||
def update(self, vector_id=None, vector=None, payload=None):
|
||||
"""
|
||||
Update a vector and its payload.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to update.
|
||||
vector (List[float], optional): Updated vector.
|
||||
payload (Dict, optional): Updated payload.
|
||||
"""
|
||||
row = Row(id=vector_id, vector=vector, metadata=payload)
|
||||
self._table.upsert(rows=[row])
|
||||
|
||||
def get(self, vector_id):
|
||||
"""
|
||||
Retrieve a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to retrieve.
|
||||
|
||||
Returns:
|
||||
OutputData: Retrieved vector.
|
||||
"""
|
||||
projections = ["id", "metadata"]
|
||||
result = self._table.query(primary_key={"id": vector_id}, projections=projections)
|
||||
row = result.row
|
||||
return OutputData(id=row.get("id"), score=None, payload=row.get("metadata", {}))
|
||||
|
||||
def list_cols(self):
|
||||
"""
|
||||
List all tables (collections).
|
||||
|
||||
Returns:
|
||||
List[str]: List of table names.
|
||||
"""
|
||||
tables = self._database.list_table()
|
||||
return [table.table_name for table in tables]
|
||||
|
||||
def delete_col(self):
|
||||
"""Delete the table."""
|
||||
try:
|
||||
tables = self._database.list_table()
|
||||
|
||||
# skip drop table if table not exists
|
||||
table_exists = any(table.table_name == self.table_name for table in tables)
|
||||
if not table_exists:
|
||||
logger.info(f"Table {self.table_name} does not exist, skipping deletion")
|
||||
return
|
||||
|
||||
# Delete the table
|
||||
self._database.drop_table(self.table_name)
|
||||
logger.info(f"Initiated deletion of table {self.table_name}")
|
||||
|
||||
# Wait for table to be completely deleted
|
||||
while True:
|
||||
time.sleep(2)
|
||||
try:
|
||||
self._database.describe_table(self.table_name)
|
||||
logger.info(f"Waiting for table {self.table_name} to be deleted...")
|
||||
except ServerError as e:
|
||||
if e.code == ServerErrCode.TABLE_NOT_EXIST:
|
||||
logger.info(f"Table {self.table_name} has been completely deleted")
|
||||
break
|
||||
logger.error(f"Error checking table status: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting table: {e}")
|
||||
raise
|
||||
|
||||
def col_info(self):
|
||||
"""
|
||||
Get information about the table.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Table information.
|
||||
"""
|
||||
return self._table.stats()
|
||||
|
||||
def list(self, filters: dict = None, limit: int = 100) -> list:
|
||||
"""
|
||||
List all vectors in the table.
|
||||
|
||||
Args:
|
||||
filters (Dict, optional): Filters to apply to the list.
|
||||
limit (int, optional): Number of vectors to return. Defaults to 100.
|
||||
|
||||
Returns:
|
||||
List[OutputData]: List of vectors.
|
||||
"""
|
||||
projections = ["id", "metadata"]
|
||||
list_filter = self._create_filter(filters) if filters else None
|
||||
result = self._table.select(filter=list_filter, projections=projections, limit=limit)
|
||||
|
||||
memories = []
|
||||
for row in result.rows:
|
||||
obj = OutputData(id=row.get("id"), score=None, payload=row.get("metadata", {}))
|
||||
memories.append(obj)
|
||||
|
||||
return [memories]
|
||||
|
||||
def reset(self):
|
||||
"""Reset the table by deleting and recreating it."""
|
||||
logger.warning(f"Resetting table {self.table_name}...")
|
||||
try:
|
||||
self.delete_col()
|
||||
self.create_col(
|
||||
name=self.table_name,
|
||||
vector_size=self.embedding_model_dims,
|
||||
distance=self.metric_type,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error resetting table: {e}")
|
||||
raise
|
||||
|
||||
def _create_filter(self, filters: dict) -> str:
|
||||
"""
|
||||
Create filter expression for queries.
|
||||
|
||||
Args:
|
||||
filters (dict): Filter conditions.
|
||||
|
||||
Returns:
|
||||
str: Filter expression.
|
||||
"""
|
||||
conditions = []
|
||||
for key, value in filters.items():
|
||||
if isinstance(value, str):
|
||||
conditions.append(f'metadata["{key}"] = "{value}"')
|
||||
else:
|
||||
conditions.append(f'metadata["{key}"] = {value}')
|
||||
return " AND ".join(conditions)
|
||||
@@ -15,7 +15,9 @@ class VectorStoreConfig(BaseModel):
|
||||
"chroma": "ChromaDbConfig",
|
||||
"pgvector": "PGVectorConfig",
|
||||
"pinecone": "PineconeConfig",
|
||||
"mongodb": "MongoDBConfig",
|
||||
"milvus": "MilvusDBConfig",
|
||||
"baidu": "BaiduDBConfig",
|
||||
"upstash_vector": "UpstashVectorConfig",
|
||||
"azure_ai_search": "AzureAISearchConfig",
|
||||
"redis": "RedisDBConfig",
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any, Callable
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from pymongo import MongoClient
|
||||
from pymongo.operations import SearchIndexModel
|
||||
from pymongo.errors import PyMongoError
|
||||
except ImportError:
|
||||
raise ImportError("The 'pymongo' library is required. Please install it using 'pip install pymongo'.")
|
||||
|
||||
from mem0.vector_stores.base import VectorStoreBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class OutputData(BaseModel):
|
||||
id: Optional[str]
|
||||
score: Optional[float]
|
||||
payload: Optional[dict]
|
||||
|
||||
|
||||
class MongoVector(VectorStoreBase):
|
||||
VECTOR_TYPE = "knnVector"
|
||||
SIMILARITY_METRIC = "cosine"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db_name: str,
|
||||
collection_name: str,
|
||||
embedding_model_dims: int,
|
||||
mongo_uri: str
|
||||
):
|
||||
"""
|
||||
Initialize the MongoDB vector store with vector search capabilities.
|
||||
|
||||
Args:
|
||||
db_name (str): Database name
|
||||
collection_name (str): Collection name
|
||||
embedding_model_dims (int): Dimension of the embedding vector
|
||||
mongo_uri (str): MongoDB connection URI
|
||||
"""
|
||||
self.collection_name = collection_name
|
||||
self.embedding_model_dims = embedding_model_dims
|
||||
self.db_name = db_name
|
||||
|
||||
self.client = MongoClient(
|
||||
mongo_uri
|
||||
)
|
||||
self.db = self.client[db_name]
|
||||
self.collection = self.create_col()
|
||||
|
||||
def create_col(self):
|
||||
"""Create new collection with vector search index."""
|
||||
try:
|
||||
database = self.client[self.db_name]
|
||||
collection_names = database.list_collection_names()
|
||||
if self.collection_name not in collection_names:
|
||||
logger.info(f"Collection '{self.collection_name}' does not exist. Creating it now.")
|
||||
collection = database[self.collection_name]
|
||||
# Insert and remove a placeholder document to create the collection
|
||||
collection.insert_one({"_id": 0, "placeholder": True})
|
||||
collection.delete_one({"_id": 0})
|
||||
logger.info(f"Collection '{self.collection_name}' created successfully.")
|
||||
else:
|
||||
collection = database[self.collection_name]
|
||||
|
||||
self.index_name = f"{self.collection_name}_vector_index"
|
||||
found_indexes = list(collection.list_search_indexes(name=self.index_name))
|
||||
if found_indexes:
|
||||
logger.info(f"Search index '{self.index_name}' already exists in collection '{self.collection_name}'.")
|
||||
else:
|
||||
search_index_model = SearchIndexModel(
|
||||
name=self.index_name,
|
||||
definition={
|
||||
"mappings": {
|
||||
"dynamic": False,
|
||||
"fields": {
|
||||
"embedding": {
|
||||
"type": self.VECTOR_TYPE,
|
||||
"dimensions": self.embedding_model_dims,
|
||||
"similarity": self.SIMILARITY_METRIC,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
collection.create_search_index(search_index_model)
|
||||
logger.info(
|
||||
f"Search index '{self.index_name}' created successfully for collection '{self.collection_name}'."
|
||||
)
|
||||
return collection
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error creating collection and search index: {e}")
|
||||
return None
|
||||
|
||||
def insert(
|
||||
self, vectors: List[List[float]], payloads: Optional[List[Dict]] = None, ids: Optional[List[str]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Insert vectors into the collection.
|
||||
|
||||
Args:
|
||||
vectors (List[List[float]]): List of vectors to insert.
|
||||
payloads (List[Dict], optional): List of payloads corresponding to vectors.
|
||||
ids (List[str], optional): List of IDs corresponding to vectors.
|
||||
"""
|
||||
logger.info(f"Inserting {len(vectors)} vectors into collection '{self.collection_name}'.")
|
||||
|
||||
data = []
|
||||
for vector, payload, _id in zip(vectors, payloads or [{}] * len(vectors), ids or [None] * len(vectors)):
|
||||
document = {"_id": _id, "embedding": vector, "payload": payload}
|
||||
data.append(document)
|
||||
try:
|
||||
self.collection.insert_many(data)
|
||||
logger.info(f"Inserted {len(data)} documents into '{self.collection_name}'.")
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error inserting data: {e}")
|
||||
|
||||
def search(self, query: str, query_vector: List[float], limit=5, filters: Optional[Dict] = None) -> List[OutputData]:
|
||||
"""
|
||||
Search for similar vectors using the vector search index.
|
||||
|
||||
Args:
|
||||
query (str): Query string
|
||||
query_vector (List[float]): Query vector.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (Dict, optional): Filters to apply to the search.
|
||||
|
||||
Returns:
|
||||
List[OutputData]: Search results.
|
||||
"""
|
||||
|
||||
found_indexes = list(self.collection.list_search_indexes(name=self.index_name))
|
||||
if not found_indexes:
|
||||
logger.error(f"Index '{self.index_name}' does not exist.")
|
||||
return []
|
||||
|
||||
results = []
|
||||
try:
|
||||
collection = self.client[self.db_name][self.collection_name]
|
||||
pipeline = [
|
||||
{
|
||||
"$vectorSearch": {
|
||||
"index": self.index_name,
|
||||
"limit": limit,
|
||||
"numCandidates": limit,
|
||||
"queryVector": query_vector,
|
||||
"path": "embedding",
|
||||
}
|
||||
},
|
||||
{"$set": {"score": {"$meta": "vectorSearchScore"}}},
|
||||
{"$project": {"embedding": 0}},
|
||||
]
|
||||
results = list(collection.aggregate(pipeline))
|
||||
logger.info(f"Vector search completed. Found {len(results)} documents.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during vector search for query {query}: {e}")
|
||||
return []
|
||||
|
||||
output = [OutputData(id=str(doc["_id"]), score=doc.get("score"), payload=doc.get("payload")) for doc in results]
|
||||
return output
|
||||
|
||||
def delete(self, vector_id: str) -> None:
|
||||
"""
|
||||
Delete a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to delete.
|
||||
"""
|
||||
try:
|
||||
result = self.collection.delete_one({"_id": vector_id})
|
||||
if result.deleted_count > 0:
|
||||
logger.info(f"Deleted document with ID '{vector_id}'.")
|
||||
else:
|
||||
logger.warning(f"No document found with ID '{vector_id}' to delete.")
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error deleting document: {e}")
|
||||
|
||||
def update(self, vector_id: str, vector: Optional[List[float]] = None, payload: Optional[Dict] = None) -> None:
|
||||
"""
|
||||
Update a vector and its payload.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to update.
|
||||
vector (List[float], optional): Updated vector.
|
||||
payload (Dict, optional): Updated payload.
|
||||
"""
|
||||
update_fields = {}
|
||||
if vector is not None:
|
||||
update_fields["embedding"] = vector
|
||||
if payload is not None:
|
||||
update_fields["payload"] = payload
|
||||
|
||||
if update_fields:
|
||||
try:
|
||||
result = self.collection.update_one({"_id": vector_id}, {"$set": update_fields})
|
||||
if result.matched_count > 0:
|
||||
logger.info(f"Updated document with ID '{vector_id}'.")
|
||||
else:
|
||||
logger.warning(f"No document found with ID '{vector_id}' to update.")
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error updating document: {e}")
|
||||
|
||||
def get(self, vector_id: str) -> Optional[OutputData]:
|
||||
"""
|
||||
Retrieve a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to retrieve.
|
||||
|
||||
Returns:
|
||||
Optional[OutputData]: Retrieved vector or None if not found.
|
||||
"""
|
||||
try:
|
||||
doc = self.collection.find_one({"_id": vector_id})
|
||||
if doc:
|
||||
logger.info(f"Retrieved document with ID '{vector_id}'.")
|
||||
return OutputData(id=str(doc["_id"]), score=None, payload=doc.get("payload"))
|
||||
else:
|
||||
logger.warning(f"Document with ID '{vector_id}' not found.")
|
||||
return None
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error retrieving document: {e}")
|
||||
return None
|
||||
|
||||
def list_cols(self) -> List[str]:
|
||||
"""
|
||||
List all collections in the database.
|
||||
|
||||
Returns:
|
||||
List[str]: List of collection names.
|
||||
"""
|
||||
try:
|
||||
collections = self.db.list_collection_names()
|
||||
logger.info(f"Listing collections in database '{self.db_name}': {collections}")
|
||||
return collections
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error listing collections: {e}")
|
||||
return []
|
||||
|
||||
def delete_col(self) -> None:
|
||||
"""Delete the collection."""
|
||||
try:
|
||||
self.collection.drop()
|
||||
logger.info(f"Deleted collection '{self.collection_name}'.")
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error deleting collection: {e}")
|
||||
|
||||
def col_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get information about the collection.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Collection information.
|
||||
"""
|
||||
try:
|
||||
stats = self.db.command("collstats", self.collection_name)
|
||||
info = {"name": self.collection_name, "count": stats.get("count"), "size": stats.get("size")}
|
||||
logger.info(f"Collection info: {info}")
|
||||
return info
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error getting collection info: {e}")
|
||||
return {}
|
||||
|
||||
def list(self, filters: Optional[Dict] = None, limit: int = 100) -> List[OutputData]:
|
||||
"""
|
||||
List vectors in the collection.
|
||||
|
||||
Args:
|
||||
filters (Dict, optional): Filters to apply to the list.
|
||||
limit (int, optional): Number of vectors to return.
|
||||
|
||||
Returns:
|
||||
List[OutputData]: List of vectors.
|
||||
"""
|
||||
try:
|
||||
query = filters or {}
|
||||
cursor = self.collection.find(query).limit(limit)
|
||||
results = [OutputData(id=str(doc["_id"]), score=None, payload=doc.get("payload")) for doc in cursor]
|
||||
logger.info(f"Retrieved {len(results)} documents from collection '{self.collection_name}'.")
|
||||
return results
|
||||
except PyMongoError as e:
|
||||
logger.error(f"Error listing documents: {e}")
|
||||
return []
|
||||
|
||||
def reset(self):
|
||||
"""Reset the index by deleting and recreating it."""
|
||||
logger.warning(f"Resetting index {self.collection_name}...")
|
||||
self.delete_col()
|
||||
self.collection = self.create_col(self.collection_name)
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""Close the database connection when the object is deleted."""
|
||||
if hasattr(self, "client"):
|
||||
self.client.close()
|
||||
logger.info("MongoClient connection closed.")
|
||||
@@ -84,18 +84,18 @@ class OpenSearchDB(VectorStoreBase):
|
||||
}
|
||||
|
||||
if not self.client.indices.exists(index=name):
|
||||
logger.warning(f"Creating index {name}, it might take 1-2 minutes...")
|
||||
self.client.indices.create(index=name, body=index_settings)
|
||||
logger.info(f"Created index {name}")
|
||||
|
||||
# Wait for index to be ready
|
||||
max_retries = 60 # 60 seconds timeout
|
||||
max_retries = 180 # 3 minutes timeout
|
||||
retry_count = 0
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
# Check if index is ready by attempting a simple search
|
||||
self.client.search(index=name, body={"query": {"match_all": {}}})
|
||||
logger.info(f"Index {name} is ready")
|
||||
time.sleep(1)
|
||||
logger.info(f"Index {name} is ready")
|
||||
return
|
||||
except Exception:
|
||||
retry_count += 1
|
||||
|
||||
@@ -5,8 +5,7 @@ from typing import Any, Dict, List, Optional, Union
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from pinecone import Pinecone, PodSpec, ServerlessSpec
|
||||
from pinecone.data.dataclasses.vector import Vector
|
||||
from pinecone import Pinecone, PodSpec, ServerlessSpec, Vector
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Pinecone requires extra dependencies. Install with `pip install pinecone pinecone-text`"
|
||||
|
||||
Generated
+1
-1
@@ -2200,4 +2200,4 @@ graph = ["langchain-neo4j", "neo4j", "rank-bm25"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9,<4.0"
|
||||
content-hash = "07f2aee9c596c2d2470df085b92551b7b7e3c19cabe61ae5bee7505395601417"
|
||||
content-hash = "07f2aee9c596c2d2470df085b92551b7b7e3c19cabe61ae5bee7505395601417"
|
||||
+59
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0ai"
|
||||
version = "0.1.107r1"
|
||||
version = "0.1.110"
|
||||
description = "Long-term memory for AI Agents"
|
||||
authors = [
|
||||
{ name = "Mem0", email = "founders@mem0.ai" }
|
||||
@@ -26,6 +26,34 @@ graph = [
|
||||
"neo4j>=5.23.1",
|
||||
"rank-bm25>=0.2.2",
|
||||
]
|
||||
vector_stores = [
|
||||
"vecs>=0.4.0",
|
||||
"chromadb>=0.4.24",
|
||||
"weaviate-client>=4.4.0",
|
||||
"pinecone<7.0.0",
|
||||
"pinecone-text>=0.1.1",
|
||||
"faiss-cpu>=1.7.4",
|
||||
"upstash-vector>=0.1.0",
|
||||
"azure-search-documents>=11.4.0b8",
|
||||
]
|
||||
llms = [
|
||||
"groq>=0.3.0",
|
||||
"together>=0.2.10",
|
||||
"litellm>=0.1.0",
|
||||
"ollama>=0.1.0",
|
||||
"vertexai>=0.1.0",
|
||||
"google-generativeai>=0.3.0",
|
||||
"google-genai>=1.0.0",
|
||||
|
||||
]
|
||||
extras = [
|
||||
"boto3>=1.34.0",
|
||||
"langchain-community>=0.0.0",
|
||||
"sentence-transformers>=2.2.2",
|
||||
"elasticsearch>=8.0.0",
|
||||
"opensearch-py>=2.0.0",
|
||||
"langchain-memgraph>=0.1.0",
|
||||
]
|
||||
test = [
|
||||
"pytest>=8.2.2",
|
||||
"pytest-mock>=3.14.0",
|
||||
@@ -53,6 +81,36 @@ only-include = ["mem0"]
|
||||
[tool.hatch.build.targets.wheel.shared-data]
|
||||
"README.md" = "README.md"
|
||||
|
||||
[tool.hatch.envs.dev_py_3_9]
|
||||
python = "3.9"
|
||||
features = [
|
||||
"test",
|
||||
"graph",
|
||||
"vector_stores",
|
||||
"llms",
|
||||
"extras",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.dev_py_3_10]
|
||||
python = "3.10"
|
||||
features = [
|
||||
"test",
|
||||
"graph",
|
||||
"vector_stores",
|
||||
"llms",
|
||||
"extras",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.dev_py_3_11]
|
||||
python = "3.11"
|
||||
features = [
|
||||
"test",
|
||||
"graph",
|
||||
"vector_stores",
|
||||
"llms",
|
||||
"extras",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.default.scripts]
|
||||
format = [
|
||||
"ruff format",
|
||||
|
||||
@@ -42,3 +42,30 @@ def test_generate_response_without_tools(mock_lm_studio_client):
|
||||
)
|
||||
|
||||
assert response == "I'm doing well, thank you for asking!"
|
||||
|
||||
def test_generate_response_specifying_response_format(mock_lm_studio_client):
|
||||
config = BaseLlmConfig(
|
||||
model="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
|
||||
temperature=0.7,
|
||||
max_tokens=100,
|
||||
top_p=1.0,
|
||||
lmstudio_response_format={"type": "json_schema"}, # Specifying the response format in config
|
||||
)
|
||||
llm = LMStudioLLM(config)
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello, how are you?"},
|
||||
]
|
||||
|
||||
response = llm.generate_response(messages)
|
||||
|
||||
mock_lm_studio_client.chat.completions.create.assert_called_once_with(
|
||||
model="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
max_tokens=100,
|
||||
top_p=1.0,
|
||||
response_format={"type": "json_schema"},
|
||||
)
|
||||
|
||||
assert response == "I'm doing well, thank you for asking!"
|
||||
@@ -40,10 +40,12 @@ class TestAddToVectorStoreErrors:
|
||||
|
||||
return memory
|
||||
|
||||
def test_empty_llm_response_fact_extraction(self, mock_memory, caplog):
|
||||
def test_empty_llm_response_fact_extraction(self, mocker, mock_memory, caplog):
|
||||
"""Test empty response from LLM during fact extraction"""
|
||||
# Setup
|
||||
mock_memory.llm.generate_response.return_value = ""
|
||||
mock_capture_event = mocker.MagicMock()
|
||||
mocker.patch("mem0.memory.main.capture_event", mock_capture_event)
|
||||
|
||||
# Execute
|
||||
with caplog.at_level(logging.ERROR):
|
||||
@@ -52,9 +54,10 @@ class TestAddToVectorStoreErrors:
|
||||
)
|
||||
|
||||
# Verify
|
||||
assert mock_memory.llm.generate_response.call_count == 2
|
||||
assert mock_memory.llm.generate_response.call_count == 1
|
||||
assert result == [] # Should return empty list when no memories processed
|
||||
assert "Error in new_retrieved_facts" in caplog.text
|
||||
assert mock_capture_event.call_count == 1
|
||||
|
||||
def test_empty_llm_response_memory_actions(self, mock_memory, caplog):
|
||||
"""Test empty response from LLM during memory actions"""
|
||||
@@ -94,25 +97,31 @@ class TestAsyncAddToVectorStoreErrors:
|
||||
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
||||
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
||||
mock_async_memory.llm.generate_response.return_value = ""
|
||||
mock_capture_event = mocker.MagicMock()
|
||||
mocker.patch("mem0.memory.main.capture_event", mock_capture_event)
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
result = await mock_async_memory._add_to_vector_store(
|
||||
messages=[{"role": "user", "content": "test"}], metadata={}, filters={}, infer=True
|
||||
messages=[{"role": "user", "content": "test"}], metadata={}, effective_filters={}, infer=True
|
||||
)
|
||||
|
||||
assert mock_async_memory.llm.generate_response.call_count == 1
|
||||
assert result == []
|
||||
assert "Error in new_retrieved_facts" in caplog.text
|
||||
assert mock_capture_event.call_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_empty_llm_response_memory_actions(self, mock_async_memory, caplog, mocker):
|
||||
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
||||
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
||||
mock_async_memory.llm.generate_response.side_effect = ['{"facts": ["test fact"]}', ""]
|
||||
mock_capture_event = mocker.MagicMock()
|
||||
mocker.patch("mem0.memory.main.capture_event", mock_capture_event)
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
result = await mock_async_memory._add_to_vector_store(
|
||||
messages=[{"role": "user", "content": "test"}], metadata={}, filters={}, infer=True
|
||||
messages=[{"role": "user", "content": "test"}], metadata={}, effective_filters={}, infer=True
|
||||
)
|
||||
|
||||
assert result == []
|
||||
assert "Invalid JSON response" in caplog.text
|
||||
assert mock_capture_event.call_count == 1
|
||||
|
||||
+9
-3
@@ -19,13 +19,14 @@ def mock_openai():
|
||||
def memory_instance():
|
||||
with (
|
||||
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
|
||||
patch("mem0.utils.factory.VectorStoreFactory") as mock_vector_store,
|
||||
patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store,
|
||||
patch("mem0.utils.factory.LlmFactory") as mock_llm,
|
||||
patch("mem0.memory.telemetry.capture_event"),
|
||||
patch("mem0.memory.graph_memory.MemoryGraph"),
|
||||
):
|
||||
mock_embedder.create.return_value = Mock()
|
||||
mock_vector_store.create.return_value = Mock()
|
||||
mock_vector_store.create.return_value.search.return_value = []
|
||||
mock_llm.create.return_value = Mock()
|
||||
|
||||
config = MemoryConfig(version="v1.1")
|
||||
@@ -37,13 +38,14 @@ def memory_instance():
|
||||
def memory_custom_instance():
|
||||
with (
|
||||
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
|
||||
patch("mem0.utils.factory.VectorStoreFactory") as mock_vector_store,
|
||||
patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store,
|
||||
patch("mem0.utils.factory.LlmFactory") as mock_llm,
|
||||
patch("mem0.memory.telemetry.capture_event"),
|
||||
patch("mem0.memory.graph_memory.MemoryGraph"),
|
||||
):
|
||||
mock_embedder.create.return_value = Mock()
|
||||
mock_vector_store.create.return_value = Mock()
|
||||
mock_vector_store.create.return_value.search.return_value = []
|
||||
mock_llm.create.return_value = Mock()
|
||||
|
||||
config = MemoryConfig(
|
||||
@@ -250,7 +252,11 @@ def test_get_all(memory_instance, version, enable_graph, expected_result):
|
||||
|
||||
def test_custom_prompts(memory_custom_instance):
|
||||
messages = [{"role": "user", "content": "Test message"}]
|
||||
from mem0.embeddings.mock import MockEmbeddings
|
||||
memory_custom_instance.llm.generate_response = Mock()
|
||||
memory_custom_instance.llm.generate_response.return_value = '{"facts": ["fact1", "fact2"]}'
|
||||
memory_custom_instance.embedding_model = MockEmbeddings()
|
||||
|
||||
|
||||
with patch("mem0.memory.main.parse_messages", return_value="Test message") as mock_parse_messages:
|
||||
with patch(
|
||||
@@ -273,7 +279,7 @@ def test_custom_prompts(memory_custom_instance):
|
||||
## custom update memory prompt
|
||||
##
|
||||
mock_get_update_memory_messages.assert_called_once_with(
|
||||
[], [], memory_custom_instance.config.custom_update_memory_prompt
|
||||
[], ["fact1", "fact2"], memory_custom_instance.config.custom_update_memory_prompt
|
||||
)
|
||||
|
||||
memory_custom_instance.llm.generate_response.assert_any_call(
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
from unittest.mock import Mock, patch, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0.vector_stores.baidu import BaiduDB, OutputData
|
||||
from pymochow.model.enum import MetricType, TableState, ServerErrCode
|
||||
from pymochow.model.schema import Field, Schema, VectorIndex, FilteringIndex, HNSWParams, AutoBuildRowCountIncrement
|
||||
from pymochow.model.table import Partition, Row, VectorSearchConfig, VectorTopkSearchRequest, FloatVector, Table
|
||||
from pymochow.exception import ServerError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mochow_client():
|
||||
with patch("pymochow.MochowClient") as mock_client:
|
||||
yield mock_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_configuration():
|
||||
with patch("pymochow.configuration.Configuration") as mock_config:
|
||||
yield mock_config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bce_credentials():
|
||||
with patch("pymochow.auth.bce_credentials.BceCredentials") as mock_creds:
|
||||
yield mock_creds
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_table():
|
||||
mock_table = Mock(spec=Table)
|
||||
# 设置 Table 类的属性
|
||||
type(mock_table).database_name = PropertyMock(return_value="test_db")
|
||||
type(mock_table).table_name = PropertyMock(return_value="test_table")
|
||||
type(mock_table).schema = PropertyMock(return_value=Mock())
|
||||
type(mock_table).replication = PropertyMock(return_value=1)
|
||||
type(mock_table).partition = PropertyMock(return_value=Mock())
|
||||
type(mock_table).enable_dynamic_field = PropertyMock(return_value=False)
|
||||
type(mock_table).description = PropertyMock(return_value="")
|
||||
type(mock_table).create_time = PropertyMock(return_value="")
|
||||
type(mock_table).state = PropertyMock(return_value=TableState.NORMAL)
|
||||
type(mock_table).aliases = PropertyMock(return_value=[])
|
||||
return mock_table
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mochow_instance(mock_mochow_client, mock_configuration, mock_bce_credentials, mock_table):
|
||||
mock_database = Mock()
|
||||
mock_client_instance = Mock()
|
||||
|
||||
# Mock the client creation
|
||||
mock_mochow_client.return_value = mock_client_instance
|
||||
|
||||
# Mock database operations
|
||||
mock_client_instance.list_databases.return_value = []
|
||||
mock_client_instance.create_database.return_value = mock_database
|
||||
mock_client_instance.database.return_value = mock_database
|
||||
|
||||
# Mock table operations
|
||||
mock_database.list_table.return_value = []
|
||||
mock_database.create_table.return_value = mock_table
|
||||
mock_database.describe_table.return_value = Mock(state=TableState.NORMAL)
|
||||
mock_database.table.return_value = mock_table
|
||||
|
||||
return BaiduDB(
|
||||
endpoint="http://localhost:8287",
|
||||
account="test_account",
|
||||
api_key="test_api_key",
|
||||
database_name="test_db",
|
||||
table_name="test_table",
|
||||
embedding_model_dims=128,
|
||||
metric_type="COSINE",
|
||||
)
|
||||
|
||||
|
||||
def test_insert(mochow_instance, mock_mochow_client):
|
||||
vectors = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
|
||||
payloads = [{"name": "vector1"}, {"name": "vector2"}]
|
||||
ids = ["id1", "id2"]
|
||||
|
||||
mochow_instance.insert(vectors=vectors, payloads=payloads, ids=ids)
|
||||
|
||||
# Verify table.upsert was called with correct data
|
||||
assert mochow_instance._table.upsert.call_count == 2
|
||||
calls = mochow_instance._table.upsert.call_args_list
|
||||
|
||||
# Check first call
|
||||
first_row = calls[0][1]["rows"][0]
|
||||
assert first_row._data["id"] == "id1"
|
||||
assert first_row._data["vector"] == [0.1, 0.2, 0.3]
|
||||
assert first_row._data["metadata"] == {"name": "vector1"}
|
||||
|
||||
# Check second call
|
||||
second_row = calls[1][1]["rows"][0]
|
||||
assert second_row._data["id"] == "id2"
|
||||
assert second_row._data["vector"] == [0.4, 0.5, 0.6]
|
||||
assert second_row._data["metadata"] == {"name": "vector2"}
|
||||
|
||||
|
||||
def test_search(mochow_instance, mock_mochow_client):
|
||||
# Mock search results
|
||||
mock_search_results = Mock()
|
||||
mock_search_results.rows = [
|
||||
{"row": {"id": "id1", "metadata": {"name": "vector1"}}, "score": 0.1},
|
||||
{"row": {"id": "id2", "metadata": {"name": "vector2"}}, "score": 0.2},
|
||||
]
|
||||
mochow_instance._table.vector_search.return_value = mock_search_results
|
||||
|
||||
vectors = [0.1, 0.2, 0.3]
|
||||
results = mochow_instance.search(query="test", vectors=vectors, limit=2)
|
||||
|
||||
# Verify search was called with correct parameters
|
||||
mochow_instance._table.vector_search.assert_called_once()
|
||||
call_args = mochow_instance._table.vector_search.call_args
|
||||
request = call_args[0][0] if call_args[0] else call_args[1]["request"]
|
||||
|
||||
assert isinstance(request, VectorTopkSearchRequest)
|
||||
assert request._vector_field == "vector"
|
||||
assert isinstance(request._vector, FloatVector)
|
||||
assert request._vector._floats == vectors
|
||||
assert request._limit == 2
|
||||
assert isinstance(request._config, VectorSearchConfig)
|
||||
assert request._config._ef == 200
|
||||
|
||||
# Verify results
|
||||
assert len(results) == 2
|
||||
assert results[0].id == "id1"
|
||||
assert results[0].score == 0.1
|
||||
assert results[0].payload == {"name": "vector1"}
|
||||
assert results[1].id == "id2"
|
||||
assert results[1].score == 0.2
|
||||
assert results[1].payload == {"name": "vector2"}
|
||||
|
||||
|
||||
def test_search_with_filters(mochow_instance, mock_mochow_client):
|
||||
mochow_instance._table.vector_search.return_value = Mock(rows=[])
|
||||
|
||||
vectors = [0.1, 0.2, 0.3]
|
||||
filters = {"user_id": "user123", "agent_id": "agent456"}
|
||||
|
||||
mochow_instance.search(query="test", vectors=vectors, limit=2, filters=filters)
|
||||
|
||||
# Verify search was called with filter
|
||||
call_args = mochow_instance._table.vector_search.call_args
|
||||
request = call_args[0][0] if call_args[0] else call_args[1]["request"]
|
||||
|
||||
assert request._filter == 'metadata["user_id"] = "user123" AND metadata["agent_id"] = "agent456"'
|
||||
|
||||
|
||||
def test_delete(mochow_instance, mock_mochow_client):
|
||||
vector_id = "id1"
|
||||
mochow_instance.delete(vector_id=vector_id)
|
||||
|
||||
mochow_instance._table.delete.assert_called_once_with(primary_key={"id": vector_id})
|
||||
|
||||
|
||||
def test_update(mochow_instance, mock_mochow_client):
|
||||
vector_id = "id1"
|
||||
new_vector = [0.7, 0.8, 0.9]
|
||||
new_payload = {"name": "updated_vector"}
|
||||
|
||||
mochow_instance.update(vector_id=vector_id, vector=new_vector, payload=new_payload)
|
||||
|
||||
mochow_instance._table.upsert.assert_called_once()
|
||||
call_args = mochow_instance._table.upsert.call_args
|
||||
row = call_args[0][0] if call_args[0] else call_args[1]["rows"][0]
|
||||
|
||||
assert row._data["id"] == vector_id
|
||||
assert row._data["vector"] == new_vector
|
||||
assert row._data["metadata"] == new_payload
|
||||
|
||||
|
||||
def test_get(mochow_instance, mock_mochow_client):
|
||||
# Mock query result
|
||||
mock_result = Mock()
|
||||
mock_result.row = {"id": "id1", "metadata": {"name": "vector1"}}
|
||||
mochow_instance._table.query.return_value = mock_result
|
||||
|
||||
result = mochow_instance.get(vector_id="id1")
|
||||
|
||||
mochow_instance._table.query.assert_called_once_with(primary_key={"id": "id1"}, projections=["id", "metadata"])
|
||||
|
||||
assert result.id == "id1"
|
||||
assert result.score is None
|
||||
assert result.payload == {"name": "vector1"}
|
||||
|
||||
|
||||
def test_list(mochow_instance, mock_mochow_client):
|
||||
# Mock select result
|
||||
mock_result = Mock()
|
||||
mock_result.rows = [{"id": "id1", "metadata": {"name": "vector1"}}, {"id": "id2", "metadata": {"name": "vector2"}}]
|
||||
mochow_instance._table.select.return_value = mock_result
|
||||
|
||||
results = mochow_instance.list(limit=2)
|
||||
|
||||
mochow_instance._table.select.assert_called_once_with(filter=None, projections=["id", "metadata"], limit=2)
|
||||
|
||||
assert len(results[0]) == 2
|
||||
assert results[0][0].id == "id1"
|
||||
assert results[0][1].id == "id2"
|
||||
|
||||
|
||||
def test_list_cols(mochow_instance, mock_mochow_client):
|
||||
# Mock table list
|
||||
mock_tables = [
|
||||
Mock(spec=Table, database_name="test_db", table_name="table1"),
|
||||
Mock(spec=Table, database_name="test_db", table_name="table2"),
|
||||
]
|
||||
mochow_instance._database.list_table.return_value = mock_tables
|
||||
|
||||
result = mochow_instance.list_cols()
|
||||
|
||||
assert result == ["table1", "table2"]
|
||||
|
||||
|
||||
def test_delete_col_not_exists(mochow_instance, mock_mochow_client):
|
||||
# 使用正确的 ServerErrCode 枚举值
|
||||
mochow_instance._database.drop_table.side_effect = ServerError(
|
||||
"Table not exists", code=ServerErrCode.TABLE_NOT_EXIST
|
||||
)
|
||||
|
||||
# Should not raise exception
|
||||
mochow_instance.delete_col()
|
||||
|
||||
|
||||
def test_col_info(mochow_instance, mock_mochow_client):
|
||||
mock_table_info = {"table_name": "test_table", "fields": []}
|
||||
mochow_instance._table.stats.return_value = mock_table_info
|
||||
|
||||
result = mochow_instance.col_info()
|
||||
|
||||
assert result == mock_table_info
|
||||
@@ -0,0 +1,176 @@
|
||||
import time
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from mem0.vector_stores.mongodb import MongoVector
|
||||
from pymongo.operations import SearchIndexModel
|
||||
|
||||
@pytest.fixture
|
||||
@patch("mem0.vector_stores.mongodb.MongoClient")
|
||||
def mongo_vector_fixture(mock_mongo_client):
|
||||
mock_client = mock_mongo_client.return_value
|
||||
mock_db = mock_client["test_db"]
|
||||
mock_collection = mock_db["test_collection"]
|
||||
mock_collection.list_search_indexes.return_value = []
|
||||
mock_collection.aggregate.return_value = []
|
||||
mock_collection.find_one.return_value = None
|
||||
mock_collection.find.return_value = []
|
||||
mock_db.list_collection_names.return_value = []
|
||||
|
||||
mongo_vector = MongoVector(
|
||||
db_name="test_db",
|
||||
collection_name="test_collection",
|
||||
embedding_model_dims=1536,
|
||||
user="username",
|
||||
password="password",
|
||||
)
|
||||
return mongo_vector, mock_collection, mock_db
|
||||
|
||||
def test_initalize_create_col(mongo_vector_fixture):
|
||||
mongo_vector, mock_collection, mock_db = mongo_vector_fixture
|
||||
assert mongo_vector.collection_name == "test_collection"
|
||||
assert mongo_vector.embedding_model_dims == 1536
|
||||
assert mongo_vector.db_name == "test_db"
|
||||
|
||||
# Verify create_col being called
|
||||
mock_db.list_collection_names.assert_called_once()
|
||||
mock_collection.insert_one.assert_called_once_with({"_id": 0, "placeholder": True})
|
||||
mock_collection.delete_one.assert_called_once_with({"_id": 0})
|
||||
assert mongo_vector.index_name == "test_collection_vector_index"
|
||||
mock_collection.list_search_indexes.assert_called_once_with(name="test_collection_vector_index")
|
||||
mock_collection.create_search_index.assert_called_once()
|
||||
args, _ = mock_collection.create_search_index.call_args
|
||||
search_index_model = args[0].document
|
||||
assert search_index_model == {
|
||||
"name": "test_collection_vector_index",
|
||||
"definition": {
|
||||
"mappings": {
|
||||
"dynamic": False,
|
||||
"fields": {
|
||||
"embedding": {
|
||||
"type": "knnVector",
|
||||
"d": 1536,
|
||||
"similarity": "cosine",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert mongo_vector.collection == mock_collection
|
||||
|
||||
def test_insert(mongo_vector_fixture):
|
||||
mongo_vector, mock_collection, _ = mongo_vector_fixture
|
||||
vectors = [[0.1] * 1536, [0.2] * 1536]
|
||||
payloads = [{"name": "vector1"}, {"name": "vector2"}]
|
||||
ids = ["id1", "id2"]
|
||||
|
||||
mongo_vector.insert(vectors, payloads, ids)
|
||||
expected_records=[
|
||||
({"_id": ids[0], "embedding": vectors[0], "payload": payloads[0]}),
|
||||
({"_id": ids[1], "embedding": vectors[1], "payload": payloads[1]})
|
||||
]
|
||||
mock_collection.insert_many.assert_called_once_with(expected_records)
|
||||
|
||||
def test_search(mongo_vector_fixture):
|
||||
mongo_vector, mock_collection, _ = mongo_vector_fixture
|
||||
query_vector = [0.1] * 1536
|
||||
mock_collection.aggregate.return_value = [
|
||||
{"_id": "id1", "score": 0.9, "payload": {"key": "value1"}},
|
||||
{"_id": "id2", "score": 0.8, "payload": {"key": "value2"}},
|
||||
]
|
||||
mock_collection.list_search_indexes.return_value = ["test_collection_vector_index"]
|
||||
|
||||
results = mongo_vector.search("query_str", query_vector, limit=2)
|
||||
mock_collection.list_search_indexes.assert_called_with(name="test_collection_vector_index")
|
||||
mock_collection.aggregate.assert_called_once_with([
|
||||
{
|
||||
"$vectorSearch": {
|
||||
"index": "test_collection_vector_index",
|
||||
"limit": 2,
|
||||
"numCandidates": 2,
|
||||
"queryVector": query_vector,
|
||||
"path": "embedding",
|
||||
},
|
||||
},
|
||||
{"$set": {"score": {"$meta": "vectorSearchScore"}}},
|
||||
{"$project": {"embedding": 0}},
|
||||
])
|
||||
assert len(results) == 2
|
||||
assert results[0].id == "id1"
|
||||
assert results[0].score == 0.9
|
||||
assert results[1].id == "id2"
|
||||
assert results[1].score == 0.8
|
||||
|
||||
def test_delete(mongo_vector_fixture):
|
||||
mongo_vector, mock_collection, _ = mongo_vector_fixture
|
||||
mock_delete_result = MagicMock()
|
||||
mock_delete_result.deleted_count = 1
|
||||
mock_collection.delete_one.return_value = mock_delete_result
|
||||
|
||||
mongo_vector.delete("id1")
|
||||
mock_collection.delete_one.assert_called_with({"_id": "id1"})
|
||||
|
||||
def test_update(mongo_vector_fixture):
|
||||
mongo_vector, mock_collection, _ = mongo_vector_fixture
|
||||
mock_update_result = MagicMock()
|
||||
mock_update_result.matched_count = 1
|
||||
mock_collection.update_one.return_value = mock_update_result
|
||||
idValue = "id1"
|
||||
vectorValue = [0.2] * 1536
|
||||
payloadValue = {"key": "updated"}
|
||||
|
||||
mongo_vector.update(idValue, vector=vectorValue, payload=payloadValue)
|
||||
mock_collection.update_one.assert_called_once_with(
|
||||
{"_id": idValue},
|
||||
{"$set": {"embedding": vectorValue, "payload": payloadValue}},
|
||||
)
|
||||
|
||||
def test_get(mongo_vector_fixture):
|
||||
mongo_vector, mock_collection, _ = mongo_vector_fixture
|
||||
mock_collection.find_one.return_value = {"_id": "id1", "payload": {"key": "value1"}}
|
||||
|
||||
result = mongo_vector.get("id1")
|
||||
assert result is not None
|
||||
assert result.id == "id1"
|
||||
assert result.payload == {"key": "value1"}
|
||||
|
||||
def test_list_cols(mongo_vector_fixture):
|
||||
mongo_vector, _, mock_db = mongo_vector_fixture
|
||||
mock_db.list_collection_names.return_value = ["col1", "col2"]
|
||||
|
||||
collections = mongo_vector.list_cols()
|
||||
assert collections == ["col1", "col2"]
|
||||
|
||||
def test_delete_col(mongo_vector_fixture):
|
||||
mongo_vector, mock_collection, _ = mongo_vector_fixture
|
||||
|
||||
mongo_vector.delete_col()
|
||||
mock_collection.drop.assert_called_once()
|
||||
|
||||
def test_col_info(mongo_vector_fixture):
|
||||
mongo_vector, _, mock_db = mongo_vector_fixture
|
||||
mock_db.command.return_value = {"count": 10, "size": 1024}
|
||||
|
||||
info = mongo_vector.col_info()
|
||||
mock_db.command.assert_called_once_with("collstats", "test_collection")
|
||||
assert info["name"] == "test_collection"
|
||||
assert info["count"] == 10
|
||||
assert info["size"] == 1024
|
||||
|
||||
def test_list(mongo_vector_fixture):
|
||||
mongo_vector, mock_collection, _ = mongo_vector_fixture
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.limit.return_value = [
|
||||
{"_id": "id1", "payload": {"key": "value1"}},
|
||||
{"_id": "id2", "payload": {"key": "value2"}},
|
||||
]
|
||||
mock_collection.find.return_value = mock_cursor
|
||||
|
||||
query_filters = {"_id": {"$in": ["id1", "id2"]}}
|
||||
results = mongo_vector.list(filters=query_filters, limit=2)
|
||||
mock_collection.find.assert_called_once_with(query_filters)
|
||||
mock_cursor.limit.assert_called_once_with(2)
|
||||
assert len(results) == 2
|
||||
assert results[0].id == "id1"
|
||||
assert results[0].payload == {"key": "value1"}
|
||||
assert results[1].id == "id2"
|
||||
assert results[1].payload == {"key": "value2"}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/vercel-ai-provider",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.6",
|
||||
"description": "Vercel AI Provider for providing memory to LLMs",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -28,6 +28,7 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "1.1.12",
|
||||
"@ai-sdk/cohere": "1.1.12",
|
||||
"@ai-sdk/google": "1.2.18",
|
||||
"@ai-sdk/groq": "1.1.11",
|
||||
"@ai-sdk/openai": "1.1.15",
|
||||
"@ai-sdk/provider": "1.0.9",
|
||||
|
||||
Generated
+36
@@ -14,6 +14,9 @@ importers:
|
||||
'@ai-sdk/cohere':
|
||||
specifier: 1.1.12
|
||||
version: 1.1.12(zod@3.24.2)
|
||||
'@ai-sdk/google':
|
||||
specifier: 1.2.18
|
||||
version: 1.2.18(zod@3.24.2)
|
||||
'@ai-sdk/groq':
|
||||
specifier: 1.1.11
|
||||
version: 1.1.11(zod@3.24.2)
|
||||
@@ -84,6 +87,12 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
|
||||
'@ai-sdk/google@1.2.18':
|
||||
resolution: {integrity: sha512-8B70+i+uB12Ae6Sn6B9Oc6W0W/XorGgc88Nx0pyUrcxFOdytHBaAVhTPqYsO3LLClfjYN8pQ9GMxd5cpGEnUcA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
|
||||
'@ai-sdk/groq@1.1.11':
|
||||
resolution: {integrity: sha512-Y5WUyWuxkQarl4AVGeIMbNSp4/XiwW/mxp9SKeagfDhflVnQHd2ggISVD6HiOBQhznusITjWYYC66DJeBn0v6A==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -105,10 +114,20 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.8':
|
||||
resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
|
||||
'@ai-sdk/provider@1.0.9':
|
||||
resolution: {integrity: sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@1.1.18':
|
||||
resolution: {integrity: sha512-2wlWug6NVAc8zh3pgqtvwPkSNTdA6Q4x9CmrNXCeHcXfJkJ+MuHFQz/I7Wb7mLRajf0DAxsFLIhHyBCEuTkDNw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2751,6 +2770,12 @@ snapshots:
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.2)
|
||||
zod: 3.24.2
|
||||
|
||||
'@ai-sdk/google@1.2.18(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.2)
|
||||
zod: 3.24.2
|
||||
|
||||
'@ai-sdk/groq@1.1.11(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.9
|
||||
@@ -2772,10 +2797,21 @@ snapshots:
|
||||
optionalDependencies:
|
||||
zod: 3.24.2
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.8(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
nanoid: 3.3.11
|
||||
secure-json-parse: 2.7.0
|
||||
zod: 3.24.2
|
||||
|
||||
'@ai-sdk/provider@1.0.9':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@1.1.18(react@19.1.0)(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.2)
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface Mem0ConfigSettings {
|
||||
rerank?: boolean;
|
||||
enable_graph?: boolean;
|
||||
output_format?: string;
|
||||
filter_memories?: boolean;
|
||||
}
|
||||
|
||||
export interface Mem0ChatConfig extends Mem0ConfigSettings, Mem0ProviderSettings {}
|
||||
|
||||
Reference in New Issue
Block a user