Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18c870ec79 | ||
|
|
3e5f68ee90 | ||
|
|
a0cd4065d9 | ||
|
|
7c0c4a03c4 | ||
|
|
a8ace18607 | ||
|
|
df43f904d1 | ||
|
|
a5a07d711b | ||
|
|
a40268dd51 | ||
|
|
c59752c6d6 | ||
|
|
aa334fb569 | ||
|
|
40a5e87022 | ||
|
|
4dec9ace88 | ||
|
|
e1dc27276b | ||
|
|
9a12ea7b3c | ||
|
|
e10a509645 | ||
|
|
fe3f10adb8 | ||
|
|
53c91fb107 | ||
|
|
ecc596b11f | ||
|
|
be37fca1bb |
+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).
|
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).
|
We use `hatch` for managing development environments. To set up:
|
||||||
|
|
||||||
Please DO NOT use pip or conda to install the dependencies. Instead, use poetry:
|
|
||||||
|
|
||||||
```bash
|
```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
|
# The environment will automatically install all dev dependencies
|
||||||
|
# Run tests within the activated shell:
|
||||||
poetry shell
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📌 Pre-commit
|
### 📌 Pre-commit
|
||||||
@@ -40,16 +41,21 @@ pre-commit install
|
|||||||
|
|
||||||
### 🧪 Testing
|
### 🧪 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
|
```bash
|
||||||
poetry run pytest tests
|
# Run tests with default Python version
|
||||||
|
|
||||||
# or
|
|
||||||
|
|
||||||
make test
|
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,7 +12,7 @@ install:
|
|||||||
|
|
||||||
install_all:
|
install_all:
|
||||||
pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
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 \
|
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
|
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j rank-bm25
|
||||||
|
|
||||||
# Format code with ruff
|
# Format code with ruff
|
||||||
@@ -41,3 +41,12 @@ clean:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
hatch run 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
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ The v2 get memories API is powerful and flexible, allowing for more precise memo
|
|||||||
- `lte`: Less than or equal to
|
- `lte`: Less than or equal to
|
||||||
- `gt`: Greater than
|
- `gt`: Greater than
|
||||||
- `lt`: Less than
|
- `lt`: Less than
|
||||||
|
- `ne`: Not equal to
|
||||||
|
- `icontains`: Case-insensitive containment check
|
||||||
|
- `*`: Wildcard character that matches everything
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```python Code
|
```python Code
|
||||||
@@ -41,3 +44,22 @@ memories = m.get_all(
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```python Wildcard Example
|
||||||
|
# Using wildcard to get all memories for a specific user across all run_ids
|
||||||
|
memories = m.get_all(
|
||||||
|
filters={
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"user_id": "alex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_id": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
version="v2"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret
|
|||||||
- `lt`: Less than
|
- `lt`: Less than
|
||||||
- `ne`: Not equal to
|
- `ne`: Not equal to
|
||||||
- `icontains`: Case-insensitive containment check
|
- `icontains`: Case-insensitive containment check
|
||||||
|
- `*`: Wildcard character that matches everything
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```python Code
|
```python Code
|
||||||
@@ -49,3 +50,23 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```python Wildcard Example
|
||||||
|
# Using wildcard to match all run_ids for a specific user
|
||||||
|
all_memories = m.search(
|
||||||
|
query="What are Alice's hobbies?",
|
||||||
|
version="v2",
|
||||||
|
filters={
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"user_id": "alice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_id": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|||||||
@@ -288,6 +288,14 @@ mode: "wide"
|
|||||||
|
|
||||||
<Tab title="TypeScript">
|
<Tab title="TypeScript">
|
||||||
|
|
||||||
|
<Update label="2025-06-06" description="v2.1.30">
|
||||||
|
**New Features:**
|
||||||
|
- **OSS:** Added Cloudflare support
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **OSS:** Fixed baseURL param in LLM Config.
|
||||||
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-05-30" description="v2.1.29">
|
<Update label="2025-05-30" description="v2.1.29">
|
||||||
**Improvements:**
|
**Improvements:**
|
||||||
- **Client:** Added Async Mode Param for `add` method.
|
- **Client:** Added Async Mode Param for `add` method.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ To view all supported llms, visit the [Supported LLMs](./models).
|
|||||||
<Card title="Gemini" href="/components/llms/models/gemini" />
|
<Card title="Gemini" href="/components/llms/models/gemini" />
|
||||||
<Card title="DeepSeek" href="/components/llms/models/deepseek" />
|
<Card title="DeepSeek" href="/components/llms/models/deepseek" />
|
||||||
<Card title="xAI" href="/components/llms/models/xAI" />
|
<Card title="xAI" href="/components/llms/models/xAI" />
|
||||||
<Card title="XAI" href="/components/llms/models/sarvam" />
|
<Card title="Sarvam AI" href="/components/llms/models/sarvam" />
|
||||||
<Card title="LM Studio" href="/components/llms/models/lmstudio" />
|
<Card title="LM Studio" href="/components/llms/models/lmstudio" />
|
||||||
<Card title="Langchain" href="/components/llms/models/langchain" />
|
<Card title="Langchain" href="/components/llms/models/langchain" />
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[Cloudflare Vectorize](https://developers.cloudflare.com/vectorize/) is a vector database offering from Cloudflare, allowing you to build AI-powered applications with vector embeddings.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```typescript TypeScript
|
||||||
|
import { Memory } from 'mem0ai/oss';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
vectorStore: {
|
||||||
|
provider: 'vectorize',
|
||||||
|
config: {
|
||||||
|
indexName: 'my-memory-index',
|
||||||
|
accountId: 'your-cloudflare-account-id',
|
||||||
|
apiKey: 'your-cloudflare-api-key',
|
||||||
|
dimension: 1536, // Optional: defaults to 1536
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const memory = new Memory(config);
|
||||||
|
const messages = [
|
||||||
|
{"role": "user", "content": "I'm looking for a good book to read."},
|
||||||
|
{"role": "assistant", "content": "Sure, what genre are you interested in?"},
|
||||||
|
{"role": "user", "content": "I enjoy fantasy novels with strong world-building."},
|
||||||
|
{"role": "assistant", "content": "Great! I'll keep that in mind for future recommendations."}
|
||||||
|
]
|
||||||
|
await memory.add(messages, { userId: "bob", metadata: { interest: "books" } });
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Let's see the available parameters for the `vectorize` config:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="TypeScript">
|
||||||
|
| Parameter | Description | Default Value |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `indexName` | The name of the Vectorize index | `None` (Required) |
|
||||||
|
| `accountId` | Your Cloudflare account ID | `None` (Required) |
|
||||||
|
| `apiKey` | Your Cloudflare API token | `None` (Required) |
|
||||||
|
| `dimension` | Dimensions of the embedding model | `1536` |
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
@@ -13,7 +13,7 @@ Mem0 includes built-in support for various popular databases. Memory can utilize
|
|||||||
See the list of supported vector databases below.
|
See the list of supported vector databases below.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis and in-memory vector database.
|
The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis,Vectorize and in-memory vector database.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
<CardGroup cols={3}>
|
<CardGroup cols={3}>
|
||||||
@@ -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="Upstash Vector" href="/components/vectordbs/dbs/upstash-vector"></Card>
|
||||||
<Card title="Milvus" href="/components/vectordbs/dbs/milvus"></Card>
|
<Card title="Milvus" href="/components/vectordbs/dbs/milvus"></Card>
|
||||||
<Card title="Pinecone" href="/components/vectordbs/dbs/pinecone"></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="Azure" href="/components/vectordbs/dbs/azure"></Card>
|
||||||
<Card title="Redis" href="/components/vectordbs/dbs/redis"></Card>
|
<Card title="Redis" href="/components/vectordbs/dbs/redis"></Card>
|
||||||
<Card title="Elasticsearch" href="/components/vectordbs/dbs/elasticsearch"></Card>
|
<Card title="Elasticsearch" href="/components/vectordbs/dbs/elasticsearch"></Card>
|
||||||
|
|||||||
@@ -137,6 +137,7 @@
|
|||||||
"components/vectordbs/dbs/pgvector",
|
"components/vectordbs/dbs/pgvector",
|
||||||
"components/vectordbs/dbs/milvus",
|
"components/vectordbs/dbs/milvus",
|
||||||
"components/vectordbs/dbs/pinecone",
|
"components/vectordbs/dbs/pinecone",
|
||||||
|
"components/vectordbs/dbs/mongodb",
|
||||||
"components/vectordbs/dbs/azure",
|
"components/vectordbs/dbs/azure",
|
||||||
"components/vectordbs/dbs/redis",
|
"components/vectordbs/dbs/redis",
|
||||||
"components/vectordbs/dbs/elasticsearch",
|
"components/vectordbs/dbs/elasticsearch",
|
||||||
|
|||||||
+147
-117
@@ -12,8 +12,7 @@ Before you begin, make sure you have:
|
|||||||
|
|
||||||
1. Installed Livekit Agents SDK with voice dependencies of silero and deepgram:
|
1. Installed Livekit Agents SDK with voice dependencies of silero and deepgram:
|
||||||
```bash
|
```bash
|
||||||
pip install livekit \
|
pip install livekit-agents[voice] \
|
||||||
livekit-agents \
|
|
||||||
livekit-plugins-silero \
|
livekit-plugins-silero \
|
||||||
livekit-plugins-deepgram \
|
livekit-plugins-deepgram \
|
||||||
livekit-plugins-openai
|
livekit-plugins-openai
|
||||||
@@ -38,7 +37,7 @@ OPENAI_API_KEY=your_openai_api_key
|
|||||||
|
|
||||||
## Code Breakdown
|
## Code Breakdown
|
||||||
|
|
||||||
Let's break down the key components of this implementation:
|
Let's break down the key components of this implementation using LiveKit Agents:
|
||||||
|
|
||||||
### 1. Setting Up Dependencies and Environment
|
### 1. Setting Up Dependencies and Environment
|
||||||
|
|
||||||
@@ -51,17 +50,19 @@ from typing import List, Dict, Any, Annotated
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from livekit.agents import (
|
from livekit.agents import (
|
||||||
|
Agent,
|
||||||
|
AgentSession,
|
||||||
AutoSubscribe,
|
AutoSubscribe,
|
||||||
JobContext,
|
JobContext,
|
||||||
JobProcess,
|
|
||||||
WorkerOptions,
|
|
||||||
cli,
|
|
||||||
llm,
|
llm,
|
||||||
metrics,
|
function_tool,
|
||||||
|
RunContext,
|
||||||
|
cli,
|
||||||
|
WorkerOptions,
|
||||||
|
ModelSettings,
|
||||||
)
|
)
|
||||||
from livekit import rtc, api
|
|
||||||
from livekit.agents.pipeline import VoicePipelineAgent
|
|
||||||
from livekit.plugins import deepgram, openai, silero
|
from livekit.plugins import deepgram, openai, silero
|
||||||
|
from livekit.plugins.turn_detector.multilingual import MultilingualModel
|
||||||
from mem0 import AsyncMemoryClient
|
from mem0 import AsyncMemoryClient
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
@@ -88,36 +89,45 @@ This section handles:
|
|||||||
### 2. Memory Enrichment Function
|
### 2. Memory Enrichment Function
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def _enrich_with_memory(agent: VoicePipelineAgent, chat_ctx: llm.ChatContext):
|
async def _enrich_with_memory(chat_ctx: llm.ChatContext):
|
||||||
"""Add memories and Augment chat context with relevant memories"""
|
"""Add memories and augment chat context with relevant memories"""
|
||||||
if not chat_ctx.messages:
|
if not chat_ctx.messages:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Store user message in Mem0
|
# Get the latest user message
|
||||||
user_msg = chat_ctx.messages[-1]
|
user_msg = chat_ctx.messages[-1]
|
||||||
|
if user_msg.role != "user":
|
||||||
|
return
|
||||||
|
|
||||||
|
user_content = user_msg.text_content()
|
||||||
|
if not user_content:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store user message in Mem0
|
||||||
await mem0.add(
|
await mem0.add(
|
||||||
[{"role": "user", "content": user_msg.content}],
|
[{"role": "user", "content": user_content}],
|
||||||
user_id=USER_ID
|
user_id=USER_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
# Search for relevant memories
|
# Search for relevant memories
|
||||||
results = await mem0.search(
|
results = await mem0.search(
|
||||||
user_msg.content,
|
user_content,
|
||||||
user_id=USER_ID,
|
user_id=USER_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Augment context with retrieved memories
|
# Augment context with retrieved memories
|
||||||
if results:
|
if results:
|
||||||
memories = ' '.join([result["memory"] for result in results])
|
memories = ' '.join([result["memory"] for result in results])
|
||||||
logger.info(f"Enriching with memory: {memories}")
|
logger.info(f"Enriching with memory: {memories}")
|
||||||
|
|
||||||
rag_msg = llm.ChatMessage.create(
|
# Add memory context as a assistant message
|
||||||
|
memory_msg = llm.ChatMessage.create(
|
||||||
text=f"Relevant Memory: {memories}\n",
|
text=f"Relevant Memory: {memories}\n",
|
||||||
role="assistant",
|
role="assistant",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Modify chat context with retrieved memories
|
# Modify chat context with retrieved memories
|
||||||
chat_ctx.messages[-1] = rag_msg
|
chat_ctx.messages[-1] = memory_msg
|
||||||
chat_ctx.messages.append(user_msg)
|
chat_ctx.messages.append(user_msg)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -130,62 +140,45 @@ This function:
|
|||||||
### 3. Prewarm and Entrypoint Functions
|
### 3. Prewarm and Entrypoint Functions
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def prewarm_process(proc: JobProcess):
|
def prewarm_process(proc):
|
||||||
# Preload silero VAD in memory to speed up session start
|
"""Preload components to speed up session start"""
|
||||||
proc.userdata["vad"] = silero.VAD.load()
|
proc.userdata["vad"] = silero.VAD.load()
|
||||||
|
|
||||||
async def entrypoint(ctx: JobContext):
|
async def entrypoint(ctx: JobContext):
|
||||||
|
"""Main entrypoint for the memory-enabled voice agent"""
|
||||||
|
|
||||||
# Connect to LiveKit room
|
# Connect to LiveKit room
|
||||||
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
||||||
|
|
||||||
# Wait for participant
|
|
||||||
participant = await ctx.wait_for_participant()
|
|
||||||
|
|
||||||
# Initialize Mem0 client
|
|
||||||
mem0 = AsyncMemoryClient()
|
|
||||||
|
|
||||||
# Define initial system context
|
# Create agent session with modern 1.0 architecture
|
||||||
initial_ctx = llm.ChatContext().append(
|
session = AgentSession(
|
||||||
role="system",
|
|
||||||
text=(
|
|
||||||
"""
|
|
||||||
You are a helpful voice assistant.
|
|
||||||
You are a travel guide named George and will help the user to plan a travel trip of their dreams.
|
|
||||||
You should help the user plan for various adventures like work retreats, family vacations or solo backpacking trips.
|
|
||||||
You should be careful to not suggest anything that would be dangerous, illegal or inappropriate.
|
|
||||||
You can remember past interactions and use them to inform your answers.
|
|
||||||
Use semantic memory retrieval to provide contextually relevant responses.
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create VoicePipelineAgent with memory capabilities
|
|
||||||
agent = VoicePipelineAgent(
|
|
||||||
chat_ctx=initial_ctx,
|
|
||||||
vad=silero.VAD.load(),
|
|
||||||
stt=deepgram.STT(),
|
stt=deepgram.STT(),
|
||||||
llm=openai.LLM(model="gpt-4o-mini"),
|
llm=openai.LLM(model="gpt-4o-mini"),
|
||||||
tts=openai.TTS(),
|
tts=openai.TTS(),
|
||||||
before_llm_cb=_enrich_with_memory,
|
vad=silero.VAD.load(),
|
||||||
|
turn_detection=MultilingualModel(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start agent and initial greeting
|
# Create memory-enabled agent
|
||||||
agent.start(ctx.room, participant)
|
agent = MemoryEnabledAgent()
|
||||||
await agent.say(
|
|
||||||
"Hello! I'm George. Can I help you plan an upcoming trip? ",
|
# Start the session
|
||||||
allow_interruptions=True
|
await session.start(
|
||||||
|
room=ctx.room,
|
||||||
|
agent=agent,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run the application
|
# Initial greeting
|
||||||
if __name__ == "__main__":
|
await session.generate_reply(
|
||||||
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm_process))
|
instructions="Greet the user warmly as George the travel guide and ask how you can help them plan their next adventure."
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
The entrypoint function:
|
The entrypoint function:
|
||||||
- Connects to LiveKit room
|
- Connects to LiveKit room
|
||||||
- Initializes Mem0 memory client
|
- Initializes Mem0 memory client
|
||||||
- Sets up initial system context
|
- Create agent session using `AgentSession` orchestrator with memory enrichment
|
||||||
- Creates a VoicePipelineAgent with memory enrichment
|
- Uses modern turn detection with `MultilingualModel()`
|
||||||
- Starts the agent with an initial greeting
|
- Starts the agent with an initial greeting
|
||||||
|
|
||||||
## Create a Memory-Enabled Voice Agent
|
## Create a Memory-Enabled Voice Agent
|
||||||
@@ -196,22 +189,22 @@ Now that we've explained each component, here's the complete implementation that
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict, Any, Annotated
|
from typing import AsyncIterable, Any
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from livekit.agents import (
|
from livekit.agents import (
|
||||||
AutoSubscribe,
|
Agent,
|
||||||
|
AgentSession,
|
||||||
JobContext,
|
JobContext,
|
||||||
JobProcess,
|
|
||||||
WorkerOptions,
|
|
||||||
cli,
|
|
||||||
llm,
|
llm,
|
||||||
metrics,
|
function_tool,
|
||||||
|
RunContext,
|
||||||
|
cli,
|
||||||
|
WorkerOptions,
|
||||||
|
ModelSettings,
|
||||||
)
|
)
|
||||||
from livekit import rtc, api
|
|
||||||
from livekit.agents.pipeline import VoicePipelineAgent
|
|
||||||
from livekit.plugins import deepgram, openai, silero
|
from livekit.plugins import deepgram, openai, silero
|
||||||
|
from livekit.plugins.turn_detector.multilingual import MultilingualModel
|
||||||
from mem0 import AsyncMemoryClient
|
from mem0 import AsyncMemoryClient
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
@@ -227,92 +220,129 @@ USER_ID = "voice_user"
|
|||||||
# Initialize Mem0 memory client
|
# Initialize Mem0 memory client
|
||||||
mem0 = AsyncMemoryClient()
|
mem0 = AsyncMemoryClient()
|
||||||
|
|
||||||
def prewarm_process(proc: JobProcess):
|
class MemoryEnabledAgent(Agent):
|
||||||
# Preload silero VAD in memory to speed up session start
|
"""Travel guide agent with Mem0 memory integration"""
|
||||||
proc.userdata["vad"] = silero.VAD.load()
|
|
||||||
|
|
||||||
async def entrypoint(ctx: JobContext):
|
def __init__(self):
|
||||||
# Connect to LiveKit room
|
super().__init__(
|
||||||
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
instructions="""
|
||||||
|
You are a helpful voice assistant.
|
||||||
# Wait for participant
|
You are a travel guide named George and will help the user to plan a travel trip of their dreams.
|
||||||
participant = await ctx.wait_for_participant()
|
You should help the user plan for various adventures like work retreats, family vacations or solo backpacking trips.
|
||||||
|
You should be careful to not suggest anything that would be dangerous, illegal or inappropriate.
|
||||||
async def _enrich_with_memory(agent: VoicePipelineAgent, chat_ctx: llm.ChatContext):
|
You can remember past interactions and use them to inform your answers.
|
||||||
"""Add memories and Augment chat context with relevant memories"""
|
Use semantic memory retrieval to provide contextually relevant responses.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
async def llm_node(
|
||||||
|
self,
|
||||||
|
chat_ctx: llm.ChatContext,
|
||||||
|
tools: list[llm.FunctionTool],
|
||||||
|
model_settings: ModelSettings,
|
||||||
|
) -> AsyncIterable[llm.ChatChunk]:
|
||||||
|
"""Override LLM node to add memory enrichment before inference"""
|
||||||
|
|
||||||
|
# Enrich context with memory before LLM inference
|
||||||
|
await self._enrich_with_memory(chat_ctx)
|
||||||
|
|
||||||
|
# Call default LLM node with enriched context
|
||||||
|
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
async def _enrich_with_memory(self, chat_ctx: llm.ChatContext):
|
||||||
|
"""Add memories and augment chat context with relevant memories"""
|
||||||
if not chat_ctx.messages:
|
if not chat_ctx.messages:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Store user message in Mem0
|
# Get the latest user message
|
||||||
user_msg = chat_ctx.messages[-1]
|
user_msg = chat_ctx.messages[-1]
|
||||||
|
if user_msg.role != "user":
|
||||||
|
return
|
||||||
|
|
||||||
|
user_content = user_msg.text_content()
|
||||||
|
if not user_content:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store user message in Mem0
|
||||||
await mem0.add(
|
await mem0.add(
|
||||||
[{"role": "user", "content": user_msg.content}],
|
[{"role": "user", "content": user_content}],
|
||||||
user_id=USER_ID
|
user_id=USER_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
# Search for relevant memories
|
# Search for relevant memories
|
||||||
results = await mem0.search(
|
results = await mem0.search(
|
||||||
user_msg.content,
|
user_content,
|
||||||
user_id=USER_ID,
|
user_id=USER_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Augment context with retrieved memories
|
# Augment context with retrieved memories
|
||||||
if results:
|
if results:
|
||||||
memories = ' '.join([result["memory"] for result in results])
|
memories = ' '.join([result["memory"] for result in results])
|
||||||
logger.info(f"Enriching with memory: {memories}")
|
logger.info(f"Enriching with memory: {memories}")
|
||||||
|
|
||||||
rag_msg = llm.ChatMessage.create(
|
# Add memory context as a assistant message
|
||||||
|
memory_msg = llm.ChatMessage.create(
|
||||||
text=f"Relevant Memory: {memories}\n",
|
text=f"Relevant Memory: {memories}\n",
|
||||||
role="assistant",
|
role="assistant",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Modify chat context with retrieved memories
|
# Modify chat context with retrieved memories
|
||||||
chat_ctx.messages[-1] = rag_msg
|
chat_ctx.messages[-1] = memory_msg
|
||||||
chat_ctx.messages.append(user_msg)
|
chat_ctx.messages.append(user_msg)
|
||||||
|
|
||||||
# Define initial system context
|
def prewarm_process(proc):
|
||||||
initial_ctx = llm.ChatContext().append(
|
"""Preload components to speed up session start"""
|
||||||
role="system",
|
proc.userdata["vad"] = silero.VAD.load()
|
||||||
text=(
|
|
||||||
"""
|
|
||||||
You are a helpful voice assistant.
|
|
||||||
You are a travel guide named George and will help the user to plan a travel trip of their dreams.
|
|
||||||
You should help the user plan for various adventures like work retreats, family vacations or solo backpacking trips.
|
|
||||||
You should be careful to not suggest anything that would be dangerous, illegal or inappropriate.
|
|
||||||
You can remember past interactions and use them to inform your answers.
|
|
||||||
Use semantic memory retrieval to provide contextually relevant responses.
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create VoicePipelineAgent with memory capabilities
|
async def entrypoint(ctx: JobContext):
|
||||||
agent = VoicePipelineAgent(
|
"""Main entrypoint for the memory-enabled voice agent"""
|
||||||
chat_ctx=initial_ctx,
|
|
||||||
vad=silero.VAD.load(),
|
# Connect to LiveKit room
|
||||||
|
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
||||||
|
|
||||||
|
# Initialize Mem0 client
|
||||||
|
mem0 = AsyncMemoryClient()
|
||||||
|
|
||||||
|
# Create agent session with modern 1.0 architecture
|
||||||
|
session = AgentSession(
|
||||||
stt=deepgram.STT(),
|
stt=deepgram.STT(),
|
||||||
llm=openai.LLM(model="gpt-4o-mini"),
|
llm=openai.LLM(model="gpt-4o-mini"),
|
||||||
tts=openai.TTS(),
|
tts=openai.TTS(),
|
||||||
before_llm_cb=_enrich_with_memory,
|
vad=silero.VAD.load(),
|
||||||
|
turn_detection=MultilingualModel(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start agent and initial greeting
|
# Create memory-enabled agent
|
||||||
agent.start(ctx.room, participant)
|
agent = MemoryEnabledAgent()
|
||||||
await agent.say(
|
|
||||||
"Hello! I'm George. Can I help you plan an upcoming trip? ",
|
# Start the session
|
||||||
|
await session.start(
|
||||||
|
room=ctx.room,
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initial greeting
|
||||||
|
await session.generate_reply(
|
||||||
|
instructions="Greet the user warmly as George the travel guide and ask how you can help them plan their next adventure.",
|
||||||
allow_interruptions=True
|
allow_interruptions=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm_process))
|
cli.run_app(WorkerOptions(
|
||||||
|
entrypoint_fnc=entrypoint,
|
||||||
|
prewarm_fnc=prewarm_process
|
||||||
|
))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Features of This Implementation
|
## Key Features of This Implementation
|
||||||
|
|
||||||
1. **Semantic Memory Retrieval**: Uses Mem0 to store and retrieve contextually relevant memories
|
1. **Semantic Memory Retrieval**: Uses Mem0 to store and retrieve contextually relevant memories
|
||||||
2. **Voice Interaction**: Leverages LiveKit for voice communication
|
2. **Voice Interaction**: Leverages LiveKit for voice communication with proper turn detection
|
||||||
3. **Intelligent Context Management**: Augments conversations with past interactions
|
3. **Intelligent Context Management**: Augments conversations with past interactions
|
||||||
4. **Travel Planning Specialization**: Focused on creating a helpful travel guide assistant
|
4. **Travel Planning Specialization**: Focused on creating a helpful travel guide assistant
|
||||||
|
5. **Function Tools**: Modern tool definition for enhanced capabilities
|
||||||
|
|
||||||
## Running the Example
|
## Running the Example
|
||||||
|
|
||||||
@@ -325,13 +355,13 @@ To run this example:
|
|||||||
```sh
|
```sh
|
||||||
python mem0-livekit-voice-agent.py start
|
python mem0-livekit-voice-agent.py start
|
||||||
```
|
```
|
||||||
5. After the script starts, you can interact with the voice agent using [Livekit's Agent Platform](https://agents-playground.livekit.io/) and Connect to the agent inorder to start conversations.
|
5. After the script starts, you can interact with the voice agent using [Livekit's Agent Platform](https://agents-playground.livekit.io/) and connect to the agent inorder to start conversations.
|
||||||
|
|
||||||
## Best Practices for Voice Agents with Memory
|
## Best Practices for Voice Agents with Memory
|
||||||
|
|
||||||
1. **Context Preservation**: Store enough context with each memory for effective retrieval
|
1. **Context Preservation**: Store enough context with each memory for effective retrieval
|
||||||
2. **Privacy Considerations**: Implement secure memory management
|
2. **Privacy Considerations**: Implement secure memory management
|
||||||
3. **Relevant Memory Filtering**: Use semantic search to retrieve only the most pertinent memories
|
3. **Relevant Memory Filtering**: Use semantic search to retrieve only the most relevant memories
|
||||||
4. **Error Handling**: Implement robust error handling for memory operations
|
4. **Error Handling**: Implement robust error handling for memory operations
|
||||||
|
|
||||||
## Debugging Function Tools
|
## Debugging Function Tools
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ iconType: "solid"
|
|||||||
|
|
||||||
<Snippet file="paper-release.mdx" />
|
<Snippet file="paper-release.mdx" />
|
||||||
|
|
||||||
|
## 🚀 Hosted OpenMemory MCP Now Available!
|
||||||
|
|
||||||
|
#### Sign Up Now - [app.openmemory.dev](https://app.openmemory.dev)
|
||||||
|
|
||||||
|
Everything you love about OpenMemory MCP but with zero setup.
|
||||||
|
|
||||||
|
✅ Works with all MCP-compatible tools (Claude Desktop, Cursor...)
|
||||||
|
✅ Same standard memory ops: `add_memories`, `search_memory`, etc
|
||||||
|
✅ One-click provisioning, no Docker required
|
||||||
|
✅ Powered by Mem0
|
||||||
|
|
||||||
|
Add shared, persistent, low-friction memory to your MCP-compatible clients in seconds.
|
||||||
|
|
||||||
|
### 🌟 Get Started Now
|
||||||
|
**Sign up and get your access key at [app.openmemory.dev](https://app.openmemory.dev)**
|
||||||
|
|
||||||
|
Example installation: `npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key`
|
||||||
|
|
||||||
OpenMemory is a local memory infrastructure powered by Mem0 that lets you carry your memory accross any AI app. It provides a unified memory layer that stays with you, enabling agents and assistants to remember what matters across applications.
|
OpenMemory is a local memory infrastructure powered by Mem0 that lets you carry your memory accross any AI app. It provides a unified memory layer that stays with you, enabling agents and assistants to remember what matters across applications.
|
||||||
|
|
||||||
<img src="https://github.com/user-attachments/assets/3c701757-ad82-4afa-bfbe-e049c2b4320b" alt="OpenMemory UI" />
|
<img src="https://github.com/user-attachments/assets/3c701757-ad82-4afa-bfbe-e049c2b4320b" alt="OpenMemory UI" />
|
||||||
|
|||||||
+103
-10
@@ -6,6 +6,46 @@ iconType: "solid"
|
|||||||
|
|
||||||
<Snippet file="paper-release.mdx" />
|
<Snippet file="paper-release.mdx" />
|
||||||
|
|
||||||
|
## 🚀 Hosted OpenMemory MCP Now Available!
|
||||||
|
|
||||||
|
#### Sign Up Now - [app.openmemory.dev](https://app.openmemory.dev)
|
||||||
|
|
||||||
|
Everything you love about OpenMemory MCP but with zero setup.
|
||||||
|
|
||||||
|
✅ Works with all MCP-compatible tools (Claude Desktop, Cursor...)
|
||||||
|
✅ Same standard memory ops: `add_memories`, `search_memory`, etc
|
||||||
|
✅ One-click provisioning, no Docker required
|
||||||
|
✅ Powered by Mem0
|
||||||
|
|
||||||
|
Add shared, persistent, low-friction memory to your MCP-compatible clients in seconds.
|
||||||
|
|
||||||
|
### 🌟 Get Started Now
|
||||||
|
**Sign up and get your access key at [app.openmemory.dev](https://app.openmemory.dev)**
|
||||||
|
|
||||||
|
Example installation: `npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key`
|
||||||
|
|
||||||
|
## Getting Started with Hosted OpenMemory
|
||||||
|
|
||||||
|
The fastest way to get started is with our hosted version - no setup required:
|
||||||
|
|
||||||
|
### 1. Get your API key
|
||||||
|
Visit [app.openmemory.dev](https://app.openmemory.dev) to sign up and get your `OPENMEMORY_API_KEY`.
|
||||||
|
|
||||||
|
### 2. Install and connect to your preferred client
|
||||||
|
Example commands (replace `your-key` with your actual API key):
|
||||||
|
|
||||||
|
For Claude Desktop: `npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key`
|
||||||
|
|
||||||
|
For Cursor: `npx @openmemory/install --client cursor --env OPENMEMORY_API_KEY=your-key`
|
||||||
|
|
||||||
|
For Windsurf: `npx @openmemory/install --client windsurf --env OPENMEMORY_API_KEY=your-key`
|
||||||
|
|
||||||
|
That's it! Your AI client now has persistent memory across sessions.
|
||||||
|
|
||||||
|
## Local Setup (Self-Hosted)
|
||||||
|
|
||||||
|
Prefer to run OpenMemory locally? Follow the instructions below for a self-hosted setup.
|
||||||
|
|
||||||
## OpenMemory Easy Setup
|
## OpenMemory Easy Setup
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -38,22 +78,75 @@ We suggest you follow the instructions below to set up OpenMemory on your local
|
|||||||
Getting started with OpenMemory is straight forward and takes just a few minutes to set up on your local machine. Follow these steps:
|
Getting started with OpenMemory is straight forward and takes just a few minutes to set up on your local machine. Follow these steps:
|
||||||
|
|
||||||
### Getting started
|
### Getting started
|
||||||
First clone the repository and then follow the instructions:
|
|
||||||
|
|
||||||
|
### 1. First clone the repository and then follow the instructions:
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/mem0ai/mem0.git
|
git clone https://github.com/mem0ai/mem0.git
|
||||||
cd mem0/openmemory
|
cd mem0/openmemory
|
||||||
|
|
||||||
# Create the backend .env file with your OpenAI key
|
|
||||||
make env
|
|
||||||
|
|
||||||
# Build the Docker images
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Start all services (API server, vector database, and MCP server components)
|
|
||||||
make up
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 2. Set Up Environment Variables
|
||||||
|
|
||||||
|
Before running the project, you need to configure environment variables for both the API and the UI.
|
||||||
|
|
||||||
|
You can do this in one of the following ways:
|
||||||
|
|
||||||
|
- **Manually**:
|
||||||
|
Create a `.env` file in each of the following directories:
|
||||||
|
- `/api/.env`
|
||||||
|
- `/ui/.env`
|
||||||
|
|
||||||
|
- **Using `.env.example` files**:
|
||||||
|
Copy and rename the example files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp api/.env.example api/.env
|
||||||
|
cp ui/.env.example ui/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Using Makefile** (if supported):
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make env
|
||||||
|
```
|
||||||
|
- #### Example `/api/.env`
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
OPENAI_API_KEY=sk-xxx
|
||||||
|
USER=<user-id> # The User Id you want to associate the memories with
|
||||||
|
```
|
||||||
|
- #### Example `/ui/.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8765
|
||||||
|
NEXT_PUBLIC_USER_ID=<user-id> # Same as the user id for environment variable in api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build and Run the Project
|
||||||
|
You can run the project using the following two commands:
|
||||||
|
```bash
|
||||||
|
make build # builds the mcp server and ui
|
||||||
|
make up # runs openmemory mcp server and ui
|
||||||
|
```
|
||||||
|
|
||||||
|
After running these commands, you will have:
|
||||||
|
- OpenMemory MCP server running at: http://localhost:8765 (API documentation available at http://localhost:8765/docs)
|
||||||
|
- OpenMemory UI running at: http://localhost:3000
|
||||||
|
|
||||||
|
#### UI not working on http://localhost:3000?
|
||||||
|
|
||||||
|
If the UI does not start properly on http://localhost:3000, try running it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
You can configure the MCP client using the following command (replace username with your username):
|
You can configure the MCP client using the following command (replace username with your username):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
|||||||
|
|
||||||
#### Search using custom filters
|
#### Search using custom filters
|
||||||
|
|
||||||
Our advanced search allows you to set custom search filters. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and text. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains). For more details, see [V2 Search Memories](/api-reference/memory/v2-search-memories).
|
Our advanced search allows you to set custom search filters. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and text. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains, *). The wildcard character (*) matches everything for a specific field. For more details, see [V2 Search Memories](/api-reference/memory/v2-search-memories).
|
||||||
|
|
||||||
Here you need to define `version` as `v2` in the search method.
|
Here you need to define `version` as `v2` in the search method.
|
||||||
|
|
||||||
@@ -765,6 +765,81 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
|||||||
|
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
|
Example 5: Search using wildcard filters
|
||||||
|
<CodeGroup>
|
||||||
|
```python Python
|
||||||
|
query = "What do you know about me?"
|
||||||
|
filters = {
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"user_id": "alex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_id": "*" # Matches all run_ids
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
client.search(query, version="v2", filters=filters)
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript JavaScript
|
||||||
|
const query = "What do you know about me?";
|
||||||
|
const filters = {
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"user_id": "alex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_id": "*" // Matches all run_ids
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
client.search(query, { version: "v2", filters })
|
||||||
|
.then(results => console.log(results))
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
||||||
|
-H "Authorization: Token your-api-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"query": "What do you know about me?",
|
||||||
|
"filters": {
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"user_id": "alex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_id": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
```json Output
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": "7f165f7e-b411-4afe-b7e5-35789b72c4a5",
|
||||||
|
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
|
||||||
|
"user_id": "alex",
|
||||||
|
"run_id": "session-1",
|
||||||
|
"metadata": null,
|
||||||
|
"categories": ["food_preferences"],
|
||||||
|
"immutable": false,
|
||||||
|
"expiration_date": null,
|
||||||
|
"created_at": "2024-07-20T01:30:36.275141-07:00",
|
||||||
|
"updated_at": "2024-07-20T01:30:36.275172-07:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
|
||||||
### 4.3 Get All Users
|
### 4.3 Get All Users
|
||||||
|
|
||||||
@@ -1138,7 +1213,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&keywords=to play&page
|
|||||||
|
|
||||||
#### Get all memories using custom filters
|
#### Get all memories using custom filters
|
||||||
|
|
||||||
Our advanced retrieval allows you to set custom filters when fetching memories. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and keywords. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains). For more details, see [v2 Get Memories](/api-reference/memory/v2-get-memories).
|
Our advanced retrieval allows you to set custom filters when fetching memories. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and keywords. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains, *). The wildcard character (*) matches everything for a specific field. For more details, see [v2 Get Memories](/api-reference/memory/v2-get-memories).
|
||||||
|
|
||||||
Here you need to define `version` as `v2` in the get_all method.
|
Here you need to define `version` as `v2` in the get_all method.
|
||||||
|
|
||||||
@@ -1497,6 +1572,119 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
|
|||||||
|
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
|
Example 4: Get all memories using wildcard filters
|
||||||
|
<CodeGroup>
|
||||||
|
```python Python
|
||||||
|
filters = {
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"user_id": "alex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_id": "*" # Matches all run_ids
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default (No Pagination)
|
||||||
|
client.get_all(version="v2", filters=filters)
|
||||||
|
|
||||||
|
# Pagination (You can also use the page and page_size parameters)
|
||||||
|
client.get_all(version="v2", filters=filters, page=1, page_size=50)
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript JavaScript
|
||||||
|
const filters = {
|
||||||
|
"AND": [
|
||||||
|
{
|
||||||
|
"user_id": "alex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"run_id": "*" // Matches all run_ids
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default (No Pagination)
|
||||||
|
client.getAll({ version: "v2", filters })
|
||||||
|
.then(memories => console.log(memories))
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
|
||||||
|
// Pagination (You can also use the page and page_size parameters)
|
||||||
|
client.getAll({ version: "v2", filters, page: 1, page_size: 50 })
|
||||||
|
.then(memories => console.log(memories))
|
||||||
|
.catch(error => console.error(error));
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash cURL
|
||||||
|
# Default (No Pagination)
|
||||||
|
curl -X GET "https://api.mem0.ai/v1/memories/?version=v2" \
|
||||||
|
-H "Authorization: Token your-api-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"filters": {
|
||||||
|
"AND": [
|
||||||
|
{"user_id":"alex"},
|
||||||
|
{"run_id": "*"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Pagination (You can also use the page and page_size parameters)
|
||||||
|
curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
|
||||||
|
-H "Authorization: Token your-api-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"filters": {
|
||||||
|
"AND": [
|
||||||
|
{"user_id":"alex"},
|
||||||
|
{"run_id": "*"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
```json Output (Default)
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "f38b689d-6b24-45b7-bced-17fbb4d8bac7",
|
||||||
|
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
|
||||||
|
"user_id": "alex",
|
||||||
|
"run_id": "session-1",
|
||||||
|
"metadata": null,
|
||||||
|
"categories": ["food_preferences"],
|
||||||
|
"immutable": false,
|
||||||
|
"expiration_date": null,
|
||||||
|
"created_at": "2024-07-20T01:30:36.275141-07:00",
|
||||||
|
"updated_at": "2024-07-20T01:30:36.275172-07:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
```json Output (Paginated)
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": "f38b689d-6b24-45b7-bced-17fbb4d8bac7",
|
||||||
|
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
|
||||||
|
"user_id": "alex",
|
||||||
|
"run_id": "session-1",
|
||||||
|
"metadata": null,
|
||||||
|
"categories": ["food_preferences"],
|
||||||
|
"immutable": false,
|
||||||
|
"expiration_date": null,
|
||||||
|
"created_at": "2024-07-20T01:30:36.275141-07:00",
|
||||||
|
"updated_at": "2024-07-20T01:30:36.275172-07:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
|
||||||
### 4.5 Memory History
|
### 4.5 Memory History
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mem0ai",
|
"name": "mem0ai",
|
||||||
"version": "2.1.29",
|
"version": "2.1.30",
|
||||||
"description": "The Memory Layer For Your AI Apps",
|
"description": "The Memory Layer For Your AI Apps",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
@@ -107,6 +107,8 @@
|
|||||||
"@types/sqlite3": "3.1.11",
|
"@types/sqlite3": "3.1.11",
|
||||||
"groq-sdk": "0.3.0",
|
"groq-sdk": "0.3.0",
|
||||||
"@langchain/core": "^0.3.44",
|
"@langchain/core": "^0.3.44",
|
||||||
|
"cloudflare": "^4.2.0",
|
||||||
|
"@cloudflare/workers-types": "^4.20250504.0",
|
||||||
"neo4j-driver": "^5.28.1",
|
"neo4j-driver": "^5.28.1",
|
||||||
"ollama": "^0.5.14",
|
"ollama": "^0.5.14",
|
||||||
"pg": "8.11.3",
|
"pg": "8.11.3",
|
||||||
|
|||||||
Generated
+32
@@ -10,6 +10,9 @@ importers:
|
|||||||
"@anthropic-ai/sdk":
|
"@anthropic-ai/sdk":
|
||||||
specifier: ^0.40.1
|
specifier: ^0.40.1
|
||||||
version: 0.40.1(encoding@0.1.13)
|
version: 0.40.1(encoding@0.1.13)
|
||||||
|
"@cloudflare/workers-types":
|
||||||
|
specifier: ^4.20250504.0
|
||||||
|
version: 4.20250606.0
|
||||||
"@google/genai":
|
"@google/genai":
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0(@modelcontextprotocol/sdk@1.12.1)(encoding@0.1.13)
|
version: 1.2.0(@modelcontextprotocol/sdk@1.12.1)(encoding@0.1.13)
|
||||||
@@ -37,6 +40,9 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: 1.7.7
|
specifier: 1.7.7
|
||||||
version: 1.7.7
|
version: 1.7.7
|
||||||
|
cloudflare:
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.3.0(encoding@0.1.13)
|
||||||
groq-sdk:
|
groq-sdk:
|
||||||
specifier: 0.3.0
|
specifier: 0.3.0
|
||||||
version: 0.3.0(encoding@0.1.13)
|
version: 0.3.0(encoding@0.1.13)
|
||||||
@@ -385,6 +391,12 @@ packages:
|
|||||||
integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==,
|
integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"@cloudflare/workers-types@4.20250606.0":
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-9T/Y/Mxe57UVzqgfjJKheiMplnStj/3CmCHlgoZNLU8JW2waRbXvpY3EEeliiYAJfeHZTjeAaKO2pCabxAoyCw==,
|
||||||
|
}
|
||||||
|
|
||||||
"@cspotcode/source-map-support@0.8.1":
|
"@cspotcode/source-map-support@0.8.1":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -1811,6 +1823,12 @@ packages:
|
|||||||
}
|
}
|
||||||
engines: { node: ">=12" }
|
engines: { node: ">=12" }
|
||||||
|
|
||||||
|
cloudflare@4.3.0:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-C+4Jhsl/OY4V5sykRB1yJxComDld5BkKW1xd3s0MDJ1yYamT2sFAoC2FEUQg5zipyxMaaGU4N7hZ6il+gfJxZg==,
|
||||||
|
}
|
||||||
|
|
||||||
cluster-key-slot@1.1.2:
|
cluster-key-slot@1.1.2:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -5465,6 +5483,8 @@ snapshots:
|
|||||||
|
|
||||||
"@cfworker/json-schema@4.1.1": {}
|
"@cfworker/json-schema@4.1.1": {}
|
||||||
|
|
||||||
|
"@cloudflare/workers-types@4.20250606.0": {}
|
||||||
|
|
||||||
"@cspotcode/source-map-support@0.8.1":
|
"@cspotcode/source-map-support@0.8.1":
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/trace-mapping": 0.3.9
|
"@jridgewell/trace-mapping": 0.3.9
|
||||||
@@ -6397,6 +6417,18 @@ snapshots:
|
|||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
|
|
||||||
|
cloudflare@4.3.0(encoding@0.1.13):
|
||||||
|
dependencies:
|
||||||
|
"@types/node": 18.19.76
|
||||||
|
"@types/node-fetch": 2.6.12
|
||||||
|
abort-controller: 3.0.0
|
||||||
|
agentkeepalive: 4.6.0
|
||||||
|
form-data-encoder: 1.7.2
|
||||||
|
formdata-node: 4.4.1
|
||||||
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
cluster-key-slot@1.1.2: {}
|
cluster-key-slot@1.1.2: {}
|
||||||
|
|
||||||
co@4.6.0: {}
|
co@4.6.0: {}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@types/redis": "^4.0.10",
|
"@types/redis": "^4.0.10",
|
||||||
"@types/sqlite3": "^3.1.11",
|
"@types/sqlite3": "^3.1.11",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
|
"cloudflare": "^4.2.0",
|
||||||
"dotenv": "^16.4.4",
|
"dotenv": "^16.4.4",
|
||||||
"groq-sdk": "^0.3.0",
|
"groq-sdk": "^0.3.0",
|
||||||
"openai": "^4.28.0",
|
"openai": "^4.28.0",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20250504.0",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"rimraf": "^5.0.5",
|
"rimraf": "^5.0.5",
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ export * from "./vector_stores/qdrant";
|
|||||||
export * from "./vector_stores/redis";
|
export * from "./vector_stores/redis";
|
||||||
export * from "./vector_stores/supabase";
|
export * from "./vector_stores/supabase";
|
||||||
export * from "./vector_stores/langchain";
|
export * from "./vector_stores/langchain";
|
||||||
|
export * from "./vector_stores/vectorize";
|
||||||
export * from "./utils/factory";
|
export * from "./utils/factory";
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export const MemoryConfigSchema = z.object({
|
|||||||
modelProperties: z.record(z.string(), z.any()).optional(),
|
modelProperties: z.record(z.string(), z.any()).optional(),
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
model: z.union([z.string(), z.any()]).optional(),
|
model: z.union([z.string(), z.any()]).optional(),
|
||||||
|
baseURL: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
vectorStore: z.object({
|
vectorStore: z.object({
|
||||||
@@ -135,6 +136,7 @@ export const MemoryConfigSchema = z.object({
|
|||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
model: z.union([z.string(), z.any()]).optional(),
|
model: z.union([z.string(), z.any()]).optional(),
|
||||||
modelProperties: z.record(z.string(), z.any()).optional(),
|
modelProperties: z.record(z.string(), z.any()).optional(),
|
||||||
|
baseURL: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
historyDbPath: z.string().optional(),
|
historyDbPath: z.string().optional(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Embedder } from "../embeddings/base";
|
|||||||
import { LLM } from "../llms/base";
|
import { LLM } from "../llms/base";
|
||||||
import { VectorStore } from "../vector_stores/base";
|
import { VectorStore } from "../vector_stores/base";
|
||||||
import { Qdrant } from "../vector_stores/qdrant";
|
import { Qdrant } from "../vector_stores/qdrant";
|
||||||
|
import { VectorizeDB } from "../vector_stores/vectorize";
|
||||||
import { RedisDB } from "../vector_stores/redis";
|
import { RedisDB } from "../vector_stores/redis";
|
||||||
import { OllamaLLM } from "../llms/ollama";
|
import { OllamaLLM } from "../llms/ollama";
|
||||||
import { SupabaseDB } from "../vector_stores/supabase";
|
import { SupabaseDB } from "../vector_stores/supabase";
|
||||||
@@ -90,6 +91,8 @@ export class VectorStoreFactory {
|
|||||||
return new SupabaseDB(config as any);
|
return new SupabaseDB(config as any);
|
||||||
case "langchain":
|
case "langchain":
|
||||||
return new LangchainVectorStore(config as any);
|
return new LangchainVectorStore(config as any);
|
||||||
|
case "vectorize":
|
||||||
|
return new VectorizeDB(config as any);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported vector store provider: ${provider}`);
|
throw new Error(`Unsupported vector store provider: ${provider}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,436 @@
|
|||||||
|
import Cloudflare from "cloudflare";
|
||||||
|
import type { Vectorize, VectorizeVector } from "@cloudflare/workers-types";
|
||||||
|
import { VectorStore } from "./base";
|
||||||
|
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||||
|
|
||||||
|
interface VectorizeConfig extends VectorStoreConfig {
|
||||||
|
apiKey?: string;
|
||||||
|
indexName: string;
|
||||||
|
accountId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CloudflareVector {
|
||||||
|
id: string;
|
||||||
|
values: number[];
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VectorizeDB implements VectorStore {
|
||||||
|
private client: Cloudflare | null = null;
|
||||||
|
private dimensions: number;
|
||||||
|
private indexName: string;
|
||||||
|
private accountId: string;
|
||||||
|
|
||||||
|
constructor(config: VectorizeConfig) {
|
||||||
|
this.client = new Cloudflare({ apiToken: config.apiKey });
|
||||||
|
this.dimensions = config.dimension || 1536;
|
||||||
|
this.indexName = config.indexName;
|
||||||
|
this.accountId = config.accountId;
|
||||||
|
this.initialize().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(
|
||||||
|
vectors: number[][],
|
||||||
|
ids: string[],
|
||||||
|
payloads: Record<string, any>[],
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const vectorObjects: CloudflareVector[] = vectors.map(
|
||||||
|
(vector, index) => ({
|
||||||
|
id: ids[index],
|
||||||
|
values: vector,
|
||||||
|
metadata: payloads[index] || {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ndjsonPayload = vectorObjects
|
||||||
|
.map((v) => JSON.stringify(v))
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/${this.indexName}/insert`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-ndjson",
|
||||||
|
Authorization: `Bearer ${this.client?.apiToken}`,
|
||||||
|
},
|
||||||
|
body: ndjsonPayload,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`Failed to insert vectors: ${response.status} ${errorText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error inserting vectors:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to insert vectors: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(
|
||||||
|
query: number[],
|
||||||
|
limit: number = 5,
|
||||||
|
filters?: SearchFilters,
|
||||||
|
): Promise<VectorStoreResult[]> {
|
||||||
|
try {
|
||||||
|
const result = await this.client?.vectorize.indexes.query(
|
||||||
|
this.indexName,
|
||||||
|
{
|
||||||
|
account_id: this.accountId,
|
||||||
|
vector: query,
|
||||||
|
filter: filters,
|
||||||
|
returnMetadata: "all",
|
||||||
|
topK: limit,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
(result?.matches?.map((match) => ({
|
||||||
|
id: match.id,
|
||||||
|
payload: match.metadata,
|
||||||
|
score: match.score,
|
||||||
|
})) as VectorStoreResult[]) || []
|
||||||
|
); // Return empty array if result or matches is null/undefined
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching vectors:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to search vectors: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(vectorId: string): Promise<VectorStoreResult | null> {
|
||||||
|
try {
|
||||||
|
const result = (await this.client?.vectorize.indexes.getByIds(
|
||||||
|
this.indexName,
|
||||||
|
{
|
||||||
|
account_id: this.accountId,
|
||||||
|
ids: [vectorId],
|
||||||
|
},
|
||||||
|
)) as any;
|
||||||
|
|
||||||
|
if (!result?.length) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: vectorId,
|
||||||
|
payload: result[0].metadata,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting vector:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to get vector: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
vectorId: string,
|
||||||
|
vector: number[],
|
||||||
|
payload: Record<string, any>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const data: VectorizeVector = {
|
||||||
|
id: vectorId,
|
||||||
|
values: vector,
|
||||||
|
metadata: payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/${this.indexName}/upsert`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-ndjson",
|
||||||
|
Authorization: `Bearer ${this.client?.apiToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data) + "\n", // ndjson format
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`Failed to update vector: ${response.status} ${errorText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating vector:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to update vector: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(vectorId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.client?.vectorize.indexes.deleteByIds(this.indexName, {
|
||||||
|
account_id: this.accountId,
|
||||||
|
ids: [vectorId],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting vector:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to delete vector: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCol(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.client?.vectorize.indexes.delete(this.indexName, {
|
||||||
|
account_id: this.accountId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting collection:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to delete collection: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(
|
||||||
|
filters?: SearchFilters,
|
||||||
|
limit: number = 20,
|
||||||
|
): Promise<[VectorStoreResult[], number]> {
|
||||||
|
try {
|
||||||
|
const result = await this.client?.vectorize.indexes.query(
|
||||||
|
this.indexName,
|
||||||
|
{
|
||||||
|
account_id: this.accountId,
|
||||||
|
vector: Array(this.dimensions).fill(0), // Dummy vector for listing
|
||||||
|
filter: filters,
|
||||||
|
topK: limit,
|
||||||
|
returnMetadata: "all",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const matches =
|
||||||
|
(result?.matches?.map((match) => ({
|
||||||
|
id: match.id,
|
||||||
|
payload: match.metadata,
|
||||||
|
score: match.score,
|
||||||
|
})) as VectorStoreResult[]) || [];
|
||||||
|
|
||||||
|
return [matches, matches.length];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error listing vectors:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to list vectors: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateUUID(): string {
|
||||||
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
||||||
|
/[xy]/g,
|
||||||
|
function (c) {
|
||||||
|
const r = (Math.random() * 16) | 0;
|
||||||
|
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserId(): Promise<string> {
|
||||||
|
try {
|
||||||
|
let found = false;
|
||||||
|
for await (const index of this.client!.vectorize.indexes.list({
|
||||||
|
account_id: this.accountId,
|
||||||
|
})) {
|
||||||
|
if (index.name === "memory_migrations") {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
await this.client?.vectorize.indexes.create({
|
||||||
|
account_id: this.accountId,
|
||||||
|
name: "memory_migrations",
|
||||||
|
config: {
|
||||||
|
dimensions: 1,
|
||||||
|
metric: "cosine",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try to get the userId
|
||||||
|
const result: any = await this.client?.vectorize.indexes.query(
|
||||||
|
"memory_migrations",
|
||||||
|
{
|
||||||
|
account_id: this.accountId,
|
||||||
|
vector: [0],
|
||||||
|
topK: 1,
|
||||||
|
returnMetadata: "all",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (result.matches.length > 0) {
|
||||||
|
return result.matches[0].metadata.userId as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random userId if none exists
|
||||||
|
const randomUserId =
|
||||||
|
Math.random().toString(36).substring(2, 15) +
|
||||||
|
Math.random().toString(36).substring(2, 15);
|
||||||
|
const data: VectorizeVector = {
|
||||||
|
id: this.generateUUID(),
|
||||||
|
values: [0],
|
||||||
|
metadata: { userId: randomUserId },
|
||||||
|
};
|
||||||
|
|
||||||
|
await fetch(
|
||||||
|
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/memory_migrations/upsert`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-ndjson",
|
||||||
|
Authorization: `Bearer ${this.client?.apiToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data) + "\n", // ndjson format
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return randomUserId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting user ID:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to get user ID: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserId(userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Get existing point ID
|
||||||
|
const result: any = await this.client?.vectorize.indexes.query(
|
||||||
|
"memory_migrations",
|
||||||
|
{
|
||||||
|
account_id: this.accountId,
|
||||||
|
vector: [0],
|
||||||
|
topK: 1,
|
||||||
|
returnMetadata: "all",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const pointId =
|
||||||
|
result.matches.length > 0 ? result.matches[0].id : this.generateUUID();
|
||||||
|
|
||||||
|
const data: VectorizeVector = {
|
||||||
|
id: pointId,
|
||||||
|
values: [0],
|
||||||
|
metadata: { userId },
|
||||||
|
};
|
||||||
|
await fetch(
|
||||||
|
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/memory_migrations/upsert`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-ndjson",
|
||||||
|
Authorization: `Bearer ${this.client?.apiToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data) + "\n", // ndjson format
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error setting user ID:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to set user ID: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if the index already exists
|
||||||
|
let indexFound = false;
|
||||||
|
for await (const idx of this.client!.vectorize.indexes.list({
|
||||||
|
account_id: this.accountId,
|
||||||
|
})) {
|
||||||
|
if (idx.name === this.indexName) {
|
||||||
|
indexFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the index doesn't exist, create it
|
||||||
|
if (!indexFound) {
|
||||||
|
try {
|
||||||
|
await this.client?.vectorize.indexes.create({
|
||||||
|
account_id: this.accountId,
|
||||||
|
name: this.indexName,
|
||||||
|
config: {
|
||||||
|
dimensions: this.dimensions,
|
||||||
|
metric: "cosine",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const properties = ["userId", "agentId", "runId"];
|
||||||
|
|
||||||
|
for (const propertyName of properties) {
|
||||||
|
await this.client?.vectorize.indexes.metadataIndex.create(
|
||||||
|
this.indexName,
|
||||||
|
{
|
||||||
|
account_id: this.accountId,
|
||||||
|
indexType: "string",
|
||||||
|
propertyName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for metadata index
|
||||||
|
const metadataIndexes =
|
||||||
|
await this.client?.vectorize.indexes.metadataIndex.list(
|
||||||
|
this.indexName,
|
||||||
|
{
|
||||||
|
account_id: this.accountId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const existingMetadataIndexes = new Set<string>();
|
||||||
|
for (const metadataIndex of metadataIndexes?.metadataIndexes || []) {
|
||||||
|
existingMetadataIndexes.add(metadataIndex.propertyName!);
|
||||||
|
}
|
||||||
|
const properties = ["userId", "agentId", "runId"];
|
||||||
|
for (const propertyName of properties) {
|
||||||
|
if (!existingMetadataIndexes.has(propertyName)) {
|
||||||
|
await this.client?.vectorize.indexes.metadataIndex.create(
|
||||||
|
this.indexName,
|
||||||
|
{
|
||||||
|
account_id: this.accountId,
|
||||||
|
indexType: "string",
|
||||||
|
propertyName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create memory_migrations collection if it doesn't exist
|
||||||
|
let found = false;
|
||||||
|
for await (const index of this.client!.vectorize.indexes.list({
|
||||||
|
account_id: this.accountId,
|
||||||
|
})) {
|
||||||
|
if (index.name === "memory_migrations") {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
await this.client?.vectorize.indexes.create({
|
||||||
|
account_id: this.accountId,
|
||||||
|
name: "memory_migrations",
|
||||||
|
config: {
|
||||||
|
dimensions: 1,
|
||||||
|
metric: "cosine",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-9
@@ -472,6 +472,22 @@ class MemoryClient:
|
|||||||
capture_client_event("client.get_memory_export", self, {"keys": list(kwargs.keys()), "sync_type": "sync"})
|
capture_client_event("client.get_memory_export", self, {"keys": list(kwargs.keys()), "sync_type": "sync"})
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
@api_error_handler
|
||||||
|
def get_summary(self, filters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""Get the summary of a memory export.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filters: Optional filters to apply to the summary request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict containing the export status and summary data
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.post("/v1/summary/", json=self._prepare_params({"filters": filters}))
|
||||||
|
response.raise_for_status()
|
||||||
|
capture_client_event("client.get_summary", self, {"sync_type": "sync"})
|
||||||
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
def get_project(self, fields: Optional[List[str]] = None) -> Dict[str, Any]:
|
def get_project(self, fields: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||||
"""Get instructions or categories for the current project.
|
"""Get instructions or categories for the current project.
|
||||||
@@ -526,7 +542,13 @@ class MemoryClient:
|
|||||||
if not (self.org_id and self.project_id):
|
if not (self.org_id and self.project_id):
|
||||||
raise ValueError("org_id and project_id must be set to update instructions or categories")
|
raise ValueError("org_id and project_id must be set to update instructions or categories")
|
||||||
|
|
||||||
if custom_instructions is None and custom_categories is None and retrieval_criteria is None and enable_graph is None and version is None:
|
if (
|
||||||
|
custom_instructions is None
|
||||||
|
and custom_categories is None
|
||||||
|
and retrieval_criteria is None
|
||||||
|
and enable_graph is None
|
||||||
|
and version is None
|
||||||
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Currently we only support updating custom_instructions or custom_categories or retrieval_criteria, so you must provide at least one of them"
|
"Currently we only support updating custom_instructions or custom_categories or retrieval_criteria, so you must provide at least one of them"
|
||||||
)
|
)
|
||||||
@@ -675,9 +697,7 @@ class MemoryClient:
|
|||||||
capture_client_event("client.feedback", self, data, {"sync_type": "sync"})
|
capture_client_event("client.feedback", self, data, {"sync_type": "sync"})
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def _prepare_payload(
|
def _prepare_payload(self, messages: List[Dict[str, str]], kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
self, messages: List[Dict[str, str]], kwargs: Dict[str, Any]
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""Prepare the payload for API requests.
|
"""Prepare the payload for API requests.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -795,7 +815,7 @@ class AsyncMemoryClient:
|
|||||||
|
|
||||||
return data.get("user_email")
|
return data.get("user_email")
|
||||||
|
|
||||||
except requests.HTTPStatusError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
try:
|
try:
|
||||||
error_data = e.response.json()
|
error_data = e.response.json()
|
||||||
error_message = error_data.get("detail", str(e))
|
error_message = error_data.get("detail", str(e))
|
||||||
@@ -803,9 +823,7 @@ class AsyncMemoryClient:
|
|||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
raise ValueError(f"Error: {error_message}")
|
raise ValueError(f"Error: {error_message}")
|
||||||
|
|
||||||
def _prepare_payload(
|
def _prepare_payload(self, messages: List[Dict[str, str]], kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
self, messages: List[Dict[str, str]], kwargs: Dict[str, Any]
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""Prepare the payload for API requests.
|
"""Prepare the payload for API requests.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -1115,7 +1133,13 @@ class AsyncMemoryClient:
|
|||||||
if not (self.org_id and self.project_id):
|
if not (self.org_id and self.project_id):
|
||||||
raise ValueError("org_id and project_id must be set to update instructions or categories")
|
raise ValueError("org_id and project_id must be set to update instructions or categories")
|
||||||
|
|
||||||
if custom_instructions is None and custom_categories is None and retrieval_criteria is None and enable_graph is None and version is None:
|
if (
|
||||||
|
custom_instructions is None
|
||||||
|
and custom_categories is None
|
||||||
|
and retrieval_criteria is None
|
||||||
|
and enable_graph is None
|
||||||
|
and version is None
|
||||||
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Currently we only support updating custom_instructions or custom_categories or retrieval_criteria, so you must provide at least one of them"
|
"Currently we only support updating custom_instructions or custom_categories or retrieval_criteria, so you must provide at least one of them"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
+8
-21
@@ -18,22 +18,15 @@ class SarvamLLM(LLMBase):
|
|||||||
|
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Sarvam API key is required. Set SARVAM_API_KEY environment variable "
|
"Sarvam API key is required. Set SARVAM_API_KEY environment variable " "or provide api_key in config."
|
||||||
"or provide api_key in config."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set base URL - use config value or environment or default
|
# Set base URL - use config value or environment or default
|
||||||
self.base_url = (
|
self.base_url = (
|
||||||
getattr(self.config, 'sarvam_base_url', None) or
|
getattr(self.config, "sarvam_base_url", None) or os.getenv("SARVAM_API_BASE") or "https://api.sarvam.ai/v1"
|
||||||
os.getenv("SARVAM_API_BASE") or
|
|
||||||
"https://api.sarvam.ai/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_response(
|
def generate_response(self, messages: List[Dict[str, str]], response_format=None) -> str:
|
||||||
self,
|
|
||||||
messages: List[Dict[str, str]],
|
|
||||||
response_format=None
|
|
||||||
) -> str:
|
|
||||||
"""
|
"""
|
||||||
Generate a response based on the given messages using Sarvam-M.
|
Generate a response based on the given messages using Sarvam-M.
|
||||||
|
|
||||||
@@ -47,10 +40,7 @@ class SarvamLLM(LLMBase):
|
|||||||
"""
|
"""
|
||||||
url = f"{self.base_url}/chat/completions"
|
url = f"{self.base_url}/chat/completions"
|
||||||
|
|
||||||
headers = {
|
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prepare the request payload
|
# Prepare the request payload
|
||||||
params = {
|
params = {
|
||||||
@@ -74,10 +64,7 @@ class SarvamLLM(LLMBase):
|
|||||||
params["model"] = self.config.model.get("name", "sarvam-m")
|
params["model"] = self.config.model.get("name", "sarvam-m")
|
||||||
|
|
||||||
# Add Sarvam-specific parameters
|
# Add Sarvam-specific parameters
|
||||||
sarvam_specific_params = [
|
sarvam_specific_params = ["reasoning_effort", "frequency_penalty", "presence_penalty", "seed", "stop", "n"]
|
||||||
'reasoning_effort', 'frequency_penalty', 'presence_penalty',
|
|
||||||
'seed', 'stop', 'n'
|
|
||||||
]
|
|
||||||
|
|
||||||
for param in sarvam_specific_params:
|
for param in sarvam_specific_params:
|
||||||
if param in self.config.model:
|
if param in self.config.model:
|
||||||
@@ -89,12 +76,12 @@ class SarvamLLM(LLMBase):
|
|||||||
|
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
|
||||||
if 'choices' in result and len(result['choices']) > 0:
|
if "choices" in result and len(result["choices"]) > 0:
|
||||||
return result['choices'][0]['message']['content']
|
return result["choices"][0]["message"]["content"]
|
||||||
else:
|
else:
|
||||||
raise ValueError("No response choices found in Sarvam API response")
|
raise ValueError("No response choices found in Sarvam API response")
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
raise RuntimeError(f"Sarvam API request failed: {e}")
|
raise RuntimeError(f"Sarvam API request failed: {e}")
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise ValueError(f"Unexpected response format from Sarvam API: {e}")
|
raise ValueError(f"Unexpected response format from Sarvam API: {e}")
|
||||||
|
|||||||
+119
-91
@@ -28,6 +28,7 @@ from mem0.memory.utils import (
|
|||||||
get_fact_retrieval_messages,
|
get_fact_retrieval_messages,
|
||||||
parse_messages,
|
parse_messages,
|
||||||
parse_vision_messages,
|
parse_vision_messages,
|
||||||
|
process_telemetry_filters,
|
||||||
remove_code_blocks,
|
remove_code_blocks,
|
||||||
)
|
)
|
||||||
from mem0.utils.factory import EmbedderFactory, LlmFactory, VectorStoreFactory
|
from mem0.utils.factory import EmbedderFactory, LlmFactory, VectorStoreFactory
|
||||||
@@ -45,13 +46,13 @@ def _build_filters_and_metadata(
|
|||||||
"""
|
"""
|
||||||
Constructs metadata for storage and filters for querying based on session and actor identifiers.
|
Constructs metadata for storage and filters for querying based on session and actor identifiers.
|
||||||
|
|
||||||
This helper ties every memory/query to exactly one session id (`user_id`, `agent_id`, or `run_id`) and optionally narrows queries to a specific `actor_id`. It returns two dicts:
|
This helper supports multiple session identifiers (`user_id`, `agent_id`, and/or `run_id`)
|
||||||
|
for flexible session scoping and optionally narrows queries to a specific `actor_id`. It returns two dicts:
|
||||||
|
|
||||||
1. `base_metadata_template`: Used as a template for metadata when storing new memories.
|
1. `base_metadata_template`: Used as a template for metadata when storing new memories.
|
||||||
It includes the primary session identifier(s) and any `input_metadata`.
|
It includes all provided session identifier(s) and any `input_metadata`.
|
||||||
2. `effective_query_filters`: Used for querying existing memories. It includes the
|
2. `effective_query_filters`: Used for querying existing memories. It includes all
|
||||||
primary session identifier(s), any `input_filters`, and a resolved actor
|
provided session identifier(s), any `input_filters`, and a resolved actor
|
||||||
identifier for targeted filtering if specified by any actor-related inputs.
|
identifier for targeted filtering if specified by any actor-related inputs.
|
||||||
|
|
||||||
Actor filtering precedence: explicit `actor_id` arg → `filters["actor_id"]`
|
Actor filtering precedence: explicit `actor_id` arg → `filters["actor_id"]`
|
||||||
@@ -59,11 +60,9 @@ def _build_filters_and_metadata(
|
|||||||
as the actor for storage is typically derived from message content at a later stage.
|
as the actor for storage is typically derived from message content at a later stage.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id (Optional[str]): User identifier, primarily for Classic Mode session scoping.
|
user_id (Optional[str]): User identifier, for session scoping.
|
||||||
agent_id (Optional[str]): Agent identifier, for Classic Mode session scoping or
|
agent_id (Optional[str]): Agent identifier, for session scoping.
|
||||||
as auxiliary information in Group Mode.
|
run_id (Optional[str]): Run identifier, for session scoping.
|
||||||
run_id (Optional[str]): Run identifier, for Classic Mode session scoping or
|
|
||||||
as auxiliary information in Group Mode.
|
|
||||||
actor_id (Optional[str]): Explicit actor identifier, used as a potential source for
|
actor_id (Optional[str]): Explicit actor identifier, used as a potential source for
|
||||||
actor-specific filtering. See actor resolution precedence in the main description.
|
actor-specific filtering. See actor resolution precedence in the main description.
|
||||||
input_metadata (Optional[Dict[str, Any]]): Base dictionary to be augmented with
|
input_metadata (Optional[Dict[str, Any]]): Base dictionary to be augmented with
|
||||||
@@ -74,28 +73,34 @@ def _build_filters_and_metadata(
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[Dict[str, Any], Dict[str, Any]]: A tuple containing:
|
tuple[Dict[str, Any], Dict[str, Any]]: A tuple containing:
|
||||||
- base_metadata_template (Dict[str, Any]): Metadata template for storing memories,
|
- base_metadata_template (Dict[str, Any]): Metadata template for storing memories,
|
||||||
scoped to the determined session.
|
scoped to the provided session(s).
|
||||||
- effective_query_filters (Dict[str, Any]): Filters for querying memories,
|
- effective_query_filters (Dict[str, Any]): Filters for querying memories,
|
||||||
scoped to the determined session and potentially a resolved actor.
|
scoped to the provided session(s) and potentially a resolved actor.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
base_metadata_template = deepcopy(input_metadata) if input_metadata else {}
|
base_metadata_template = deepcopy(input_metadata) if input_metadata else {}
|
||||||
effective_query_filters = deepcopy(input_filters) if input_filters else {}
|
effective_query_filters = deepcopy(input_filters) if input_filters else {}
|
||||||
|
|
||||||
# ---------- resolve session id (mandatory) ----------
|
# ---------- add all provided session ids ----------
|
||||||
session_key, session_val = None, None
|
session_ids_provided = []
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
session_key, session_val = "user_id", user_id
|
base_metadata_template["user_id"] = user_id
|
||||||
elif agent_id:
|
effective_query_filters["user_id"] = user_id
|
||||||
session_key, session_val = "agent_id", agent_id
|
session_ids_provided.append("user_id")
|
||||||
elif run_id:
|
|
||||||
session_key, session_val = "run_id", run_id
|
|
||||||
|
|
||||||
if session_key is None:
|
if agent_id:
|
||||||
raise ValueError("One of 'user_id', 'agent_id', or 'run_id' must be provided.")
|
base_metadata_template["agent_id"] = agent_id
|
||||||
|
effective_query_filters["agent_id"] = agent_id
|
||||||
|
session_ids_provided.append("agent_id")
|
||||||
|
|
||||||
base_metadata_template[session_key] = session_val
|
if run_id:
|
||||||
effective_query_filters[session_key] = session_val
|
base_metadata_template["run_id"] = run_id
|
||||||
|
effective_query_filters["run_id"] = run_id
|
||||||
|
session_ids_provided.append("run_id")
|
||||||
|
|
||||||
|
if not session_ids_provided:
|
||||||
|
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be provided.")
|
||||||
|
|
||||||
# ---------- optional actor filter ----------
|
# ---------- optional actor filter ----------
|
||||||
resolved_actor_id = actor_id or effective_query_filters.get("actor_id")
|
resolved_actor_id = actor_id or effective_query_filters.get("actor_id")
|
||||||
@@ -333,10 +338,9 @@ class Memory(MemoryBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in new_retrieved_facts: {e}")
|
logging.error(f"Error in new_retrieved_facts: {e}")
|
||||||
new_retrieved_facts = []
|
new_retrieved_facts = []
|
||||||
|
|
||||||
if not new_retrieved_facts:
|
if not new_retrieved_facts:
|
||||||
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
|
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
|
||||||
return []
|
|
||||||
|
|
||||||
retrieved_old_memory = []
|
retrieved_old_memory = []
|
||||||
new_message_embeddings = {}
|
new_message_embeddings = {}
|
||||||
@@ -364,24 +368,27 @@ class Memory(MemoryBase):
|
|||||||
temp_uuid_mapping[str(idx)] = item["id"]
|
temp_uuid_mapping[str(idx)] = item["id"]
|
||||||
retrieved_old_memory[idx]["id"] = str(idx)
|
retrieved_old_memory[idx]["id"] = str(idx)
|
||||||
|
|
||||||
function_calling_prompt = get_update_memory_messages(
|
if new_retrieved_facts:
|
||||||
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
|
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"},
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error in new memory actions response: {e}")
|
|
||||||
response = ""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = remove_code_blocks(response)
|
response: str = self.llm.generate_response(
|
||||||
new_memories_with_actions = json.loads(response)
|
messages=[{"role": "user", "content": function_calling_prompt}],
|
||||||
except Exception as e:
|
response_format={"type": "json_object"},
|
||||||
logging.error(f"Invalid JSON response: {e}")
|
)
|
||||||
|
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 = {}
|
new_memories_with_actions = {}
|
||||||
|
|
||||||
returned_memories = []
|
returned_memories = []
|
||||||
@@ -433,10 +440,11 @@ class Memory(MemoryBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error iterating new_memories_with_actions: {e}")
|
logging.error(f"Error iterating new_memories_with_actions: {e}")
|
||||||
|
|
||||||
|
keys, encoded_ids = process_telemetry_filters(filters)
|
||||||
capture_event(
|
capture_event(
|
||||||
"mem0.add",
|
"mem0.add",
|
||||||
self,
|
self,
|
||||||
{"version": self.api_version, "keys": list(filters.keys()), "sync_type": "sync"},
|
{"version": self.api_version, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"},
|
||||||
)
|
)
|
||||||
return returned_memories
|
return returned_memories
|
||||||
|
|
||||||
@@ -529,8 +537,9 @@ class Memory(MemoryBase):
|
|||||||
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
||||||
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be specified.")
|
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be specified.")
|
||||||
|
|
||||||
|
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||||
capture_event(
|
capture_event(
|
||||||
"mem0.get_all", self, {"limit": limit, "keys": list(effective_filters.keys()), "sync_type": "sync"}
|
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"}
|
||||||
)
|
)
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
@@ -564,7 +573,9 @@ class Memory(MemoryBase):
|
|||||||
def _get_all_from_vector_store(self, filters, limit):
|
def _get_all_from_vector_store(self, filters, limit):
|
||||||
memories_result = self.vector_store.list(filters=filters, limit=limit)
|
memories_result = self.vector_store.list(filters=filters, limit=limit)
|
||||||
actual_memories = (
|
actual_memories = (
|
||||||
memories_result[0] if isinstance(memories_result, (tuple, list)) 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 = [
|
promoted_payload_keys = [
|
||||||
@@ -607,6 +618,7 @@ class Memory(MemoryBase):
|
|||||||
run_id: Optional[str] = None,
|
run_id: Optional[str] = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
filters: Optional[Dict[str, Any]] = None,
|
filters: Optional[Dict[str, Any]] = None,
|
||||||
|
threshold: Optional[float] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Searches for memories based on a query
|
Searches for memories based on a query
|
||||||
@@ -617,6 +629,7 @@ class Memory(MemoryBase):
|
|||||||
run_id (str, optional): ID of the run to search for. Defaults to None.
|
run_id (str, optional): ID of the run to search for. Defaults to None.
|
||||||
limit (int, optional): Limit the number of results. Defaults to 100.
|
limit (int, optional): Limit the number of results. Defaults to 100.
|
||||||
filters (dict, optional): Filters to apply to the search. Defaults to None..
|
filters (dict, optional): Filters to apply to the search. Defaults to None..
|
||||||
|
threshold (float, optional): Minimum score for a memory to be included in the results. Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the search results, typically under a "results" key,
|
dict: A dictionary containing the search results, typically under a "results" key,
|
||||||
@@ -630,14 +643,22 @@ class Memory(MemoryBase):
|
|||||||
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
||||||
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be specified.")
|
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be specified.")
|
||||||
|
|
||||||
|
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||||
capture_event(
|
capture_event(
|
||||||
"mem0.search",
|
"mem0.search",
|
||||||
self,
|
self,
|
||||||
{"limit": limit, "version": self.api_version, "keys": list(effective_filters.keys()), "sync_type": "sync"},
|
{
|
||||||
|
"limit": limit,
|
||||||
|
"version": self.api_version,
|
||||||
|
"keys": keys,
|
||||||
|
"encoded_ids": encoded_ids,
|
||||||
|
"sync_type": "sync",
|
||||||
|
"threshold": threshold,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
future_memories = executor.submit(self._search_vector_store, query, effective_filters, limit)
|
future_memories = executor.submit(self._search_vector_store, query, effective_filters, limit, threshold)
|
||||||
future_graph_entities = (
|
future_graph_entities = (
|
||||||
executor.submit(self.graph.search, query, effective_filters, limit) if self.enable_graph else None
|
executor.submit(self.graph.search, query, effective_filters, limit) if self.enable_graph else None
|
||||||
)
|
)
|
||||||
@@ -664,7 +685,7 @@ class Memory(MemoryBase):
|
|||||||
else:
|
else:
|
||||||
return {"results": original_memories}
|
return {"results": original_memories}
|
||||||
|
|
||||||
def _search_vector_store(self, query, filters, limit):
|
def _search_vector_store(self, query, filters, limit, threshold: Optional[float] = None):
|
||||||
embeddings = self.embedding_model.embed(query, "search")
|
embeddings = self.embedding_model.embed(query, "search")
|
||||||
memories = self.vector_store.search(query=query, vectors=embeddings, limit=limit, filters=filters)
|
memories = self.vector_store.search(query=query, vectors=embeddings, limit=limit, filters=filters)
|
||||||
|
|
||||||
@@ -697,7 +718,8 @@ class Memory(MemoryBase):
|
|||||||
if additional_metadata:
|
if additional_metadata:
|
||||||
memory_item_dict["metadata"] = additional_metadata
|
memory_item_dict["metadata"] = additional_metadata
|
||||||
|
|
||||||
original_memories.append(memory_item_dict)
|
if threshold is None or mem.score >= threshold:
|
||||||
|
original_memories.append(memory_item_dict)
|
||||||
|
|
||||||
return original_memories
|
return original_memories
|
||||||
|
|
||||||
@@ -752,7 +774,8 @@ class Memory(MemoryBase):
|
|||||||
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
|
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
|
||||||
)
|
)
|
||||||
|
|
||||||
capture_event("mem0.delete_all", self, {"keys": list(filters.keys()), "sync_type": "sync"})
|
keys, encoded_ids = process_telemetry_filters(filters)
|
||||||
|
capture_event("mem0.delete_all", self, {"keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"})
|
||||||
memories = self.vector_store.list(filters=filters)[0]
|
memories = self.vector_store.list(filters=filters)[0]
|
||||||
for memory in memories:
|
for memory in memories:
|
||||||
self._delete_memory(memory.id)
|
self._delete_memory(memory.id)
|
||||||
@@ -1086,7 +1109,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
self,
|
self,
|
||||||
messages: list,
|
messages: list,
|
||||||
metadata: dict,
|
metadata: dict,
|
||||||
filters: dict,
|
effective_filters: dict,
|
||||||
infer: bool,
|
infer: bool,
|
||||||
):
|
):
|
||||||
if not infer:
|
if not infer:
|
||||||
@@ -1141,13 +1164,11 @@ class AsyncMemory(MemoryBase):
|
|||||||
response = remove_code_blocks(response)
|
response = remove_code_blocks(response)
|
||||||
new_retrieved_facts = json.loads(response)["facts"]
|
new_retrieved_facts = json.loads(response)["facts"]
|
||||||
except Exception as e:
|
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}")
|
logging.error(f"Error in new_retrieved_facts: {e}")
|
||||||
new_retrieved_facts = []
|
new_retrieved_facts = []
|
||||||
|
|
||||||
|
if not new_retrieved_facts:
|
||||||
|
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
|
||||||
|
|
||||||
retrieved_old_memory = []
|
retrieved_old_memory = []
|
||||||
new_message_embeddings = {}
|
new_message_embeddings = {}
|
||||||
@@ -1160,7 +1181,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
query=new_mem_content,
|
query=new_mem_content,
|
||||||
vectors=embeddings,
|
vectors=embeddings,
|
||||||
limit=5,
|
limit=5,
|
||||||
filters=filters, # 'filters' is query_filters_for_inference
|
filters=effective_filters, # 'filters' is query_filters_for_inference
|
||||||
)
|
)
|
||||||
return [{"id": mem.id, "text": mem.payload["data"]} for mem in existing_mems]
|
return [{"id": mem.id, "text": mem.payload["data"]} for mem in existing_mems]
|
||||||
|
|
||||||
@@ -1179,34 +1200,25 @@ class AsyncMemory(MemoryBase):
|
|||||||
temp_uuid_mapping[str(idx)] = item["id"]
|
temp_uuid_mapping[str(idx)] = item["id"]
|
||||||
retrieved_old_memory[idx]["id"] = str(idx)
|
retrieved_old_memory[idx]["id"] = str(idx)
|
||||||
|
|
||||||
function_calling_prompt = get_update_memory_messages(
|
if new_retrieved_facts:
|
||||||
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
|
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"},
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
try:
|
||||||
|
response = await asyncio.to_thread(
|
||||||
response = ""
|
self.llm.generate_response,
|
||||||
logging.error(f"Error in new memory actions response: {e}")
|
messages=[{"role": "user", "content": function_calling_prompt}],
|
||||||
response = ""
|
response_format={"type": "json_object"},
|
||||||
try:
|
)
|
||||||
response = remove_code_blocks(response)
|
except Exception as e:
|
||||||
new_memories_with_actions = json.loads(response)
|
logging.error(f"Error in new memory actions response: {e}")
|
||||||
except Exception as e:
|
response = ""
|
||||||
|
try:
|
||||||
new_memories_with_actions = {}
|
response = remove_code_blocks(response)
|
||||||
|
new_memories_with_actions = json.loads(response)
|
||||||
if not new_memories_with_actions:
|
except Exception as e:
|
||||||
logger.info("No new facts retrieved from input (async). Skipping memory update LLM call.")
|
logging.error(f"Invalid JSON response: {e}")
|
||||||
return []
|
new_memories_with_actions = {}
|
||||||
|
|
||||||
logging.error(f"Invalid JSON response: {e}")
|
|
||||||
new_memories_with_actions = {}
|
|
||||||
|
|
||||||
|
|
||||||
returned_memories = []
|
returned_memories = []
|
||||||
try:
|
try:
|
||||||
@@ -1267,8 +1279,11 @@ class AsyncMemory(MemoryBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in memory processing loop (async): {e}")
|
logging.error(f"Error in memory processing loop (async): {e}")
|
||||||
|
|
||||||
|
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||||
capture_event(
|
capture_event(
|
||||||
"mem0.add", self, {"version": self.api_version, "keys": list(filters.keys()), "sync_type": "async"}
|
"mem0.add",
|
||||||
|
self,
|
||||||
|
{"version": self.api_version, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"},
|
||||||
)
|
)
|
||||||
return returned_memories
|
return returned_memories
|
||||||
|
|
||||||
@@ -1364,8 +1379,9 @@ class AsyncMemory(MemoryBase):
|
|||||||
"at least one of 'user_id', 'agent_id', or 'run_id' must be specified for get_all."
|
"at least one of 'user_id', 'agent_id', or 'run_id' must be specified for get_all."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||||
capture_event(
|
capture_event(
|
||||||
"mem0.get_all", self, {"limit": limit, "keys": list(effective_filters.keys()), "sync_type": "async"}
|
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"}
|
||||||
)
|
)
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
@@ -1442,6 +1458,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
run_id: Optional[str] = None,
|
run_id: Optional[str] = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
filters: Optional[Dict[str, Any]] = None,
|
filters: Optional[Dict[str, Any]] = None,
|
||||||
|
threshold: Optional[float] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Searches for memories based on a query
|
Searches for memories based on a query
|
||||||
@@ -1451,7 +1468,8 @@ class AsyncMemory(MemoryBase):
|
|||||||
agent_id (str, optional): ID of the agent to search for. Defaults to None.
|
agent_id (str, optional): ID of the agent to search for. Defaults to None.
|
||||||
run_id (str, optional): ID of the run to search for. Defaults to None.
|
run_id (str, optional): ID of the run to search for. Defaults to None.
|
||||||
limit (int, optional): Limit the number of results. Defaults to 100.
|
limit (int, optional): Limit the number of results. Defaults to 100.
|
||||||
filters (dict, optional): Filters to apply to the search. Defaults to None..
|
filters (dict, optional): Filters to apply to the search. Defaults to None.
|
||||||
|
threshold (float, optional): Minimum score for a memory to be included in the results. Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the search results, typically under a "results" key,
|
dict: A dictionary containing the search results, typically under a "results" key,
|
||||||
@@ -1466,13 +1484,21 @@ class AsyncMemory(MemoryBase):
|
|||||||
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
||||||
raise ValueError("at least one of 'user_id', 'agent_id', or 'run_id' must be specified ")
|
raise ValueError("at least one of 'user_id', 'agent_id', or 'run_id' must be specified ")
|
||||||
|
|
||||||
|
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||||
capture_event(
|
capture_event(
|
||||||
"mem0.search",
|
"mem0.search",
|
||||||
self,
|
self,
|
||||||
{"limit": limit, "version": self.api_version, "keys": list(effective_filters.keys()), "sync_type": "async"},
|
{
|
||||||
|
"limit": limit,
|
||||||
|
"version": self.api_version,
|
||||||
|
"keys": keys,
|
||||||
|
"encoded_ids": encoded_ids,
|
||||||
|
"sync_type": "async",
|
||||||
|
"threshold": threshold,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
vector_store_task = asyncio.create_task(self._search_vector_store(query, effective_filters, limit))
|
vector_store_task = asyncio.create_task(self._search_vector_store(query, effective_filters, limit, threshold))
|
||||||
|
|
||||||
graph_task = None
|
graph_task = None
|
||||||
if self.enable_graph:
|
if self.enable_graph:
|
||||||
@@ -1502,7 +1528,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
else:
|
else:
|
||||||
return {"results": original_memories}
|
return {"results": original_memories}
|
||||||
|
|
||||||
async def _search_vector_store(self, query, filters, limit):
|
async def _search_vector_store(self, query, filters, limit, threshold: Optional[float] = None):
|
||||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, query, "search")
|
embeddings = await asyncio.to_thread(self.embedding_model.embed, query, "search")
|
||||||
memories = await asyncio.to_thread(
|
memories = await asyncio.to_thread(
|
||||||
self.vector_store.search, query=query, vectors=embeddings, limit=limit, filters=filters
|
self.vector_store.search, query=query, vectors=embeddings, limit=limit, filters=filters
|
||||||
@@ -1537,7 +1563,8 @@ class AsyncMemory(MemoryBase):
|
|||||||
if additional_metadata:
|
if additional_metadata:
|
||||||
memory_item_dict["metadata"] = additional_metadata
|
memory_item_dict["metadata"] = additional_metadata
|
||||||
|
|
||||||
original_memories.append(memory_item_dict)
|
if threshold is None or mem.score >= threshold:
|
||||||
|
original_memories.append(memory_item_dict)
|
||||||
|
|
||||||
return original_memories
|
return original_memories
|
||||||
|
|
||||||
@@ -1593,7 +1620,8 @@ class AsyncMemory(MemoryBase):
|
|||||||
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
|
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
|
||||||
)
|
)
|
||||||
|
|
||||||
capture_event("mem0.delete_all", self, {"keys": list(filters.keys()), "sync_type": "async"})
|
keys, encoded_ids = process_telemetry_filters(filters)
|
||||||
|
capture_event("mem0.delete_all", self, {"keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"})
|
||||||
memories = await asyncio.to_thread(self.vector_store.list, filters=filters)
|
memories = await asyncio.to_thread(self.vector_store.list, filters=filters)
|
||||||
|
|
||||||
delete_tasks = []
|
delete_tasks = []
|
||||||
|
|||||||
+129
-67
@@ -21,59 +21,107 @@ class SQLiteManager:
|
|||||||
rename it, create the new schema, copy the intersecting data, then
|
rename it, create the new schema, copy the intersecting data, then
|
||||||
drop the old table.
|
drop the old table.
|
||||||
"""
|
"""
|
||||||
with self._lock, self.connection:
|
with self._lock:
|
||||||
cur = self.connection.cursor()
|
try:
|
||||||
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='history'")
|
# Start a transaction
|
||||||
if cur.fetchone() is None:
|
self.connection.execute("BEGIN")
|
||||||
return # nothing to migrate
|
cur = self.connection.cursor()
|
||||||
|
|
||||||
cur.execute("PRAGMA table_info(history)")
|
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='history'")
|
||||||
old_cols = {row[1] for row in cur.fetchall()}
|
if cur.fetchone() is None:
|
||||||
|
self.connection.execute("COMMIT")
|
||||||
|
return # nothing to migrate
|
||||||
|
|
||||||
expected_cols = {
|
cur.execute("PRAGMA table_info(history)")
|
||||||
"id",
|
old_cols = {row[1] for row in cur.fetchall()}
|
||||||
"memory_id",
|
|
||||||
"old_memory",
|
|
||||||
"new_memory",
|
|
||||||
"event",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"is_deleted",
|
|
||||||
"actor_id",
|
|
||||||
"role",
|
|
||||||
}
|
|
||||||
|
|
||||||
if old_cols == expected_cols:
|
expected_cols = {
|
||||||
return
|
"id",
|
||||||
|
"memory_id",
|
||||||
|
"old_memory",
|
||||||
|
"new_memory",
|
||||||
|
"event",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"is_deleted",
|
||||||
|
"actor_id",
|
||||||
|
"role",
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Migrating history table to new schema (no convo columns).")
|
if old_cols == expected_cols:
|
||||||
cur.execute("ALTER TABLE history RENAME TO history_old")
|
self.connection.execute("COMMIT")
|
||||||
|
return
|
||||||
|
|
||||||
self._create_history_table()
|
logger.info("Migrating history table to new schema (no convo columns).")
|
||||||
|
|
||||||
intersecting = list(expected_cols & old_cols)
|
# Clean up any existing history_old table from previous failed migration
|
||||||
cols_csv = ", ".join(intersecting)
|
cur.execute("DROP TABLE IF EXISTS history_old")
|
||||||
cur.execute(f"INSERT INTO history ({cols_csv}) SELECT {cols_csv} FROM history_old")
|
|
||||||
cur.execute("DROP TABLE history_old")
|
# Rename the current history table
|
||||||
|
cur.execute("ALTER TABLE history RENAME TO history_old")
|
||||||
|
|
||||||
|
# Create the new history table with updated schema
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE history (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
memory_id TEXT,
|
||||||
|
old_memory TEXT,
|
||||||
|
new_memory TEXT,
|
||||||
|
event TEXT,
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME,
|
||||||
|
is_deleted INTEGER,
|
||||||
|
actor_id TEXT,
|
||||||
|
role TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy data from old table to new table
|
||||||
|
intersecting = list(expected_cols & old_cols)
|
||||||
|
if intersecting:
|
||||||
|
cols_csv = ", ".join(intersecting)
|
||||||
|
cur.execute(f"INSERT INTO history ({cols_csv}) SELECT {cols_csv} FROM history_old")
|
||||||
|
|
||||||
|
# Drop the old table
|
||||||
|
cur.execute("DROP TABLE history_old")
|
||||||
|
|
||||||
|
# Commit the transaction
|
||||||
|
self.connection.execute("COMMIT")
|
||||||
|
logger.info("History table migration completed successfully.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Rollback the transaction on any error
|
||||||
|
self.connection.execute("ROLLBACK")
|
||||||
|
logger.error(f"History table migration failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
def _create_history_table(self) -> None:
|
def _create_history_table(self) -> None:
|
||||||
with self._lock, self.connection:
|
with self._lock:
|
||||||
self.connection.execute(
|
try:
|
||||||
|
self.connection.execute("BEGIN")
|
||||||
|
self.connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS history (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
memory_id TEXT,
|
||||||
|
old_memory TEXT,
|
||||||
|
new_memory TEXT,
|
||||||
|
event TEXT,
|
||||||
|
created_at DATETIME,
|
||||||
|
updated_at DATETIME,
|
||||||
|
is_deleted INTEGER,
|
||||||
|
actor_id TEXT,
|
||||||
|
role TEXT
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS history (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
memory_id TEXT,
|
|
||||||
old_memory TEXT,
|
|
||||||
new_memory TEXT,
|
|
||||||
event TEXT,
|
|
||||||
created_at DATETIME,
|
|
||||||
updated_at DATETIME,
|
|
||||||
is_deleted INTEGER,
|
|
||||||
actor_id TEXT,
|
|
||||||
role TEXT
|
|
||||||
)
|
)
|
||||||
"""
|
self.connection.execute("COMMIT")
|
||||||
)
|
except Exception as e:
|
||||||
|
self.connection.execute("ROLLBACK")
|
||||||
|
logger.error(f"Failed to create history table: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
def add_history(
|
def add_history(
|
||||||
self,
|
self,
|
||||||
@@ -88,28 +136,35 @@ class SQLiteManager:
|
|||||||
actor_id: Optional[str] = None,
|
actor_id: Optional[str] = None,
|
||||||
role: Optional[str] = None,
|
role: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
with self._lock, self.connection:
|
with self._lock:
|
||||||
self.connection.execute(
|
try:
|
||||||
"""
|
self.connection.execute("BEGIN")
|
||||||
INSERT INTO history (
|
self.connection.execute(
|
||||||
id, memory_id, old_memory, new_memory, event,
|
"""
|
||||||
created_at, updated_at, is_deleted, actor_id, role
|
INSERT INTO history (
|
||||||
|
id, memory_id, old_memory, new_memory, event,
|
||||||
|
created_at, updated_at, is_deleted, actor_id, role
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
str(uuid.uuid4()),
|
||||||
|
memory_id,
|
||||||
|
old_memory,
|
||||||
|
new_memory,
|
||||||
|
event,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
is_deleted,
|
||||||
|
actor_id,
|
||||||
|
role,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
self.connection.execute("COMMIT")
|
||||||
""",
|
except Exception as e:
|
||||||
(
|
self.connection.execute("ROLLBACK")
|
||||||
str(uuid.uuid4()),
|
logger.error(f"Failed to add history record: {e}")
|
||||||
memory_id,
|
raise
|
||||||
old_memory,
|
|
||||||
new_memory,
|
|
||||||
event,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
is_deleted,
|
|
||||||
actor_id,
|
|
||||||
role,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_history(self, memory_id: str) -> List[Dict[str, Any]]:
|
def get_history(self, memory_id: str) -> List[Dict[str, Any]]:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -143,9 +198,16 @@ class SQLiteManager:
|
|||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Drop and recreate the history table."""
|
"""Drop and recreate the history table."""
|
||||||
with self._lock, self.connection:
|
with self._lock:
|
||||||
self.connection.execute("DROP TABLE IF EXISTS history")
|
try:
|
||||||
self._create_history_table()
|
self.connection.execute("BEGIN")
|
||||||
|
self.connection.execute("DROP TABLE IF EXISTS history")
|
||||||
|
self.connection.execute("COMMIT")
|
||||||
|
self._create_history_table()
|
||||||
|
except Exception as e:
|
||||||
|
self.connection.execute("ROLLBACK")
|
||||||
|
logger.error(f"Failed to reset history table: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
if self.connection:
|
if self.connection:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from mem0.configs.prompts import FACT_RETRIEVAL_PROMPT
|
from mem0.configs.prompts import FACT_RETRIEVAL_PROMPT
|
||||||
|
|
||||||
@@ -98,3 +99,21 @@ def parse_vision_messages(messages, llm=None, vision_details="auto"):
|
|||||||
returned_messages.append(msg)
|
returned_messages.append(msg)
|
||||||
|
|
||||||
return returned_messages
|
return returned_messages
|
||||||
|
|
||||||
|
|
||||||
|
def process_telemetry_filters(filters):
|
||||||
|
"""
|
||||||
|
Process the telemetry filters
|
||||||
|
"""
|
||||||
|
if filters is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
encoded_ids = {}
|
||||||
|
if "user_id" in filters:
|
||||||
|
encoded_ids["user_id"] = hashlib.md5(filters["user_id"].encode()).hexdigest()
|
||||||
|
if "agent_id" in filters:
|
||||||
|
encoded_ids["agent_id"] = hashlib.md5(filters["agent_id"].encode()).hexdigest()
|
||||||
|
if "run_id" in filters:
|
||||||
|
encoded_ids["run_id"] = hashlib.md5(filters["run_id"].encode()).hexdigest()
|
||||||
|
|
||||||
|
return list(filters.keys()), encoded_ids
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class VectorStoreFactory:
|
|||||||
"upstash_vector": "mem0.vector_stores.upstash_vector.UpstashVector",
|
"upstash_vector": "mem0.vector_stores.upstash_vector.UpstashVector",
|
||||||
"azure_ai_search": "mem0.vector_stores.azure_ai_search.AzureAISearch",
|
"azure_ai_search": "mem0.vector_stores.azure_ai_search.AzureAISearch",
|
||||||
"pinecone": "mem0.vector_stores.pinecone.PineconeDB",
|
"pinecone": "mem0.vector_stores.pinecone.PineconeDB",
|
||||||
|
"mongodb": "mem0.vector_stores.mongodb.MongoDB",
|
||||||
"redis": "mem0.vector_stores.redis.RedisDB",
|
"redis": "mem0.vector_stores.redis.RedisDB",
|
||||||
"elasticsearch": "mem0.vector_stores.elasticsearch.ElasticsearchDB",
|
"elasticsearch": "mem0.vector_stores.elasticsearch.ElasticsearchDB",
|
||||||
"vertex_ai_vector_search": "mem0.vector_stores.vertex_ai_vector_search.GoogleMatchingEngine",
|
"vertex_ai_vector_search": "mem0.vector_stores.vertex_ai_vector_search.GoogleMatchingEngine",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class VectorStoreConfig(BaseModel):
|
|||||||
"chroma": "ChromaDbConfig",
|
"chroma": "ChromaDbConfig",
|
||||||
"pgvector": "PGVectorConfig",
|
"pgvector": "PGVectorConfig",
|
||||||
"pinecone": "PineconeConfig",
|
"pinecone": "PineconeConfig",
|
||||||
|
"mongodb": "MongoDBConfig",
|
||||||
"milvus": "MilvusDBConfig",
|
"milvus": "MilvusDBConfig",
|
||||||
"upstash_vector": "UpstashVectorConfig",
|
"upstash_vector": "UpstashVectorConfig",
|
||||||
"azure_ai_search": "AzureAISearchConfig",
|
"azure_ai_search": "AzureAISearchConfig",
|
||||||
|
|||||||
@@ -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.")
|
||||||
@@ -83,19 +83,19 @@ class OpenSearchDB(VectorStoreBase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.warning(f"Creating index {name}, it might take 1-2 minutes...")
|
||||||
if not self.client.indices.exists(index=name):
|
if not self.client.indices.exists(index=name):
|
||||||
self.client.indices.create(index=name, body=index_settings)
|
self.client.indices.create(index=name, body=index_settings)
|
||||||
logger.info(f"Created index {name}")
|
|
||||||
|
|
||||||
# Wait for index to be ready
|
# Wait for index to be ready
|
||||||
max_retries = 60 # 60 seconds timeout
|
max_retries = 180 # 3 minutes timeout
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
while retry_count < max_retries:
|
while retry_count < max_retries:
|
||||||
try:
|
try:
|
||||||
# Check if index is ready by attempting a simple search
|
# Check if index is ready by attempting a simple search
|
||||||
self.client.search(index=name, body={"query": {"match_all": {}}})
|
self.client.search(index=name, body={"query": {"match_all": {}}})
|
||||||
logger.info(f"Index {name} is ready")
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
logger.info(f"Index {name} is ready")
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
|
|||||||
@@ -37,6 +37,45 @@ curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh |
|
|||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
|
### 1. Set Up Environment Variables
|
||||||
|
|
||||||
|
Before running the project, you need to configure environment variables for both the API and the UI.
|
||||||
|
|
||||||
|
You can do this in one of the following ways:
|
||||||
|
|
||||||
|
- **Manually**:
|
||||||
|
Create a `.env` file in each of the following directories:
|
||||||
|
- `/api/.env`
|
||||||
|
- `/ui/.env`
|
||||||
|
|
||||||
|
- **Using `.env.example` files**:
|
||||||
|
Copy and rename the example files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp api/.env.example api/.env
|
||||||
|
cp ui/.env.example ui/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Using Makefile** (if supported):
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make env
|
||||||
|
```
|
||||||
|
- #### Example `/api/.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
OPENAI_API_KEY=sk-xxx
|
||||||
|
USER=<user-id> # The User Id you want to associate the memories with
|
||||||
|
```
|
||||||
|
- #### Example `/ui/.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8765
|
||||||
|
NEXT_PUBLIC_USER_ID=<user-id> # Same as the user id for environment variable in api
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build and Run the Project
|
||||||
You can run the project using the following two commands:
|
You can run the project using the following two commands:
|
||||||
```bash
|
```bash
|
||||||
make build # builds the mcp server and ui
|
make build # builds the mcp server and ui
|
||||||
@@ -47,6 +86,17 @@ After running these commands, you will have:
|
|||||||
- OpenMemory MCP server running at: http://localhost:8765 (API documentation available at http://localhost:8765/docs)
|
- OpenMemory MCP server running at: http://localhost:8765 (API documentation available at http://localhost:8765/docs)
|
||||||
- OpenMemory UI running at: http://localhost:3000
|
- OpenMemory UI running at: http://localhost:3000
|
||||||
|
|
||||||
|
#### UI not working on `localhost:3000`?
|
||||||
|
|
||||||
|
If the UI does not start properly on [http://localhost:3000](http://localhost:3000), try running it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
- `api/` - Backend APIs + MCP server
|
- `api/` - Backend APIs + MCP server
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
OPENAI_API_KEY=sk-xxx
|
OPENAI_API_KEY=sk-xxx
|
||||||
|
USER=user
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""remove_global_unique_constraint_on_app_name_add_composite_unique
|
||||||
|
|
||||||
|
Revision ID: afd00efbd06b
|
||||||
|
Revises: add_config_table
|
||||||
|
Create Date: 2025-06-04 01:59:41.637440
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'afd00efbd06b'
|
||||||
|
down_revision: Union[str, None] = 'add_config_table'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index('ix_apps_name', table_name='apps')
|
||||||
|
op.create_index(op.f('ix_apps_name'), 'apps', ['name'], unique=False)
|
||||||
|
op.create_index('idx_app_owner_name', 'apps', ['owner_id', 'name'], unique=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index('idx_app_owner_name', table_name='apps')
|
||||||
|
op.drop_index(op.f('ix_apps_name'), table_name='apps')
|
||||||
|
op.create_index('ix_apps_name', 'apps', ['name'], unique=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import enum
|
import enum
|
||||||
import uuid
|
import uuid
|
||||||
import datetime
|
import datetime
|
||||||
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Column, String, Boolean, ForeignKey, Enum, Table,
|
Column, String, Boolean, ForeignKey, Enum, Table,
|
||||||
DateTime, JSON, Integer, UUID, Index, event
|
DateTime, JSON, Integer, UUID, Index, event
|
||||||
@@ -43,7 +44,7 @@ class App(Base):
|
|||||||
__tablename__ = "apps"
|
__tablename__ = "apps"
|
||||||
id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
|
id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
|
||||||
owner_id = Column(UUID, ForeignKey("users.id"), nullable=False, index=True)
|
owner_id = Column(UUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||||
name = Column(String, unique=True, nullable=False, index=True)
|
name = Column(String, nullable=False, index=True)
|
||||||
description = Column(String)
|
description = Column(String)
|
||||||
metadata_ = Column('metadata', JSON, default=dict)
|
metadata_ = Column('metadata', JSON, default=dict)
|
||||||
is_active = Column(Boolean, default=True, index=True)
|
is_active = Column(Boolean, default=True, index=True)
|
||||||
@@ -55,6 +56,10 @@ class App(Base):
|
|||||||
owner = relationship("User", back_populates="apps")
|
owner = relationship("User", back_populates="apps")
|
||||||
memories = relationship("Memory", back_populates="app")
|
memories = relationship("Memory", back_populates="app")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
sa.UniqueConstraint('owner_id', 'name', name='idx_app_owner_name'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Config(Base):
|
class Config(Base):
|
||||||
__tablename__ = "configs"
|
__tablename__ = "configs"
|
||||||
|
|||||||
@@ -212,7 +212,8 @@ async def create_memory(
|
|||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
# Get or create app
|
# Get or create app
|
||||||
app_obj = db.query(App).filter(App.name == request.app).first()
|
app_obj = db.query(App).filter(App.name == request.app,
|
||||||
|
App.owner_id == user.id).first()
|
||||||
if not app_obj:
|
if not app_obj:
|
||||||
app_obj = App(name=request.app, owner_id=user.id)
|
app_obj = App(name=request.app, owner_id=user.id)
|
||||||
db.add(app_obj)
|
db.add(app_obj)
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from openai import OpenAI
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from openai import OpenAI
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||||
from app.utils.prompts import MEMORY_CATEGORIZATION_PROMPT
|
from app.utils.prompts import MEMORY_CATEGORIZATION_PROMPT
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
openai_client = OpenAI()
|
openai_client = OpenAI()
|
||||||
|
|
||||||
|
|
||||||
@@ -19,19 +17,28 @@ class MemoryCategories(BaseModel):
|
|||||||
|
|
||||||
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15))
|
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15))
|
||||||
def get_categories_for_memory(memory: str) -> List[str]:
|
def get_categories_for_memory(memory: str) -> List[str]:
|
||||||
"""Get categories for a memory."""
|
|
||||||
try:
|
try:
|
||||||
response = openai_client.responses.parse(
|
messages = [
|
||||||
|
{"role": "system", "content": MEMORY_CATEGORIZATION_PROMPT},
|
||||||
|
{"role": "user", "content": memory}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Let OpenAI handle the pydantic parsing directly
|
||||||
|
completion = openai_client.chat.completions.with_response_format(
|
||||||
|
response_format=MemoryCategories
|
||||||
|
).create(
|
||||||
model="gpt-4o-mini",
|
model="gpt-4o-mini",
|
||||||
instructions=MEMORY_CATEGORIZATION_PROMPT,
|
messages=messages,
|
||||||
input=memory,
|
temperature=0
|
||||||
temperature=0,
|
|
||||||
text_format=MemoryCategories,
|
|
||||||
)
|
)
|
||||||
response_json =json.loads(response.output[0].content[0].text)
|
|
||||||
categories = response_json['categories']
|
parsed: MemoryCategories = completion.choices[0].message.parsed
|
||||||
categories = [cat.strip().lower() for cat in categories]
|
return [cat.strip().lower() for cat in parsed.categories]
|
||||||
# TODO: Validate categories later may be
|
|
||||||
return categories
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
logging.error(f"[ERROR] Failed to get categories: {e}")
|
||||||
|
try:
|
||||||
|
logging.debug(f"[DEBUG] Raw response: {completion.choices[0].message.content}")
|
||||||
|
except Exception as debug_e:
|
||||||
|
logging.debug(f"[DEBUG] Could not extract raw response: {debug_e}")
|
||||||
|
raise
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface ProfileState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ProfileState = {
|
const initialState: ProfileState = {
|
||||||
userId: process.env.NEXT_PUBLIC_USER_ID || 'deshraj',
|
userId: process.env.NEXT_PUBLIC_USER_ID || 'user',
|
||||||
totalMemories: 0,
|
totalMemories: 0,
|
||||||
totalApps: 0,
|
totalApps: 0,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
@@ -36,7 +36,7 @@ const profileSlice = createSlice({
|
|||||||
resetProfileState: (state) => {
|
resetProfileState: (state) => {
|
||||||
state.status = 'idle';
|
state.status = 'idle';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
state.userId = process.env.NEXT_PUBLIC_USER_ID || 'deshraj';
|
state.userId = process.env.NEXT_PUBLIC_USER_ID || 'user';
|
||||||
},
|
},
|
||||||
setTotalMemories: (state, action: PayloadAction<number>) => {
|
setTotalMemories: (state, action: PayloadAction<number>) => {
|
||||||
state.totalMemories = action.payload;
|
state.totalMemories = action.payload;
|
||||||
|
|||||||
Generated
+1
-1
@@ -2200,4 +2200,4 @@ graph = ["langchain-neo4j", "neo4j", "rank-bm25"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.9,<4.0"
|
python-versions = ">=3.9,<4.0"
|
||||||
content-hash = "07f2aee9c596c2d2470df085b92551b7b7e3c19cabe61ae5bee7505395601417"
|
content-hash = "07f2aee9c596c2d2470df085b92551b7b7e3c19cabe61ae5bee7505395601417"
|
||||||
+57
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mem0ai"
|
name = "mem0ai"
|
||||||
version = "0.1.104"
|
version = "0.1.108"
|
||||||
description = "Long-term memory for AI Agents"
|
description = "Long-term memory for AI Agents"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Mem0", email = "founders@mem0.ai" }
|
{ name = "Mem0", email = "founders@mem0.ai" }
|
||||||
@@ -26,6 +26,32 @@ graph = [
|
|||||||
"neo4j>=5.23.1",
|
"neo4j>=5.23.1",
|
||||||
"rank-bm25>=0.2.2",
|
"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",
|
||||||
|
]
|
||||||
|
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 = [
|
test = [
|
||||||
"pytest>=8.2.2",
|
"pytest>=8.2.2",
|
||||||
"pytest-mock>=3.14.0",
|
"pytest-mock>=3.14.0",
|
||||||
@@ -53,6 +79,36 @@ only-include = ["mem0"]
|
|||||||
[tool.hatch.build.targets.wheel.shared-data]
|
[tool.hatch.build.targets.wheel.shared-data]
|
||||||
"README.md" = "README.md"
|
"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]
|
[tool.hatch.envs.default.scripts]
|
||||||
format = [
|
format = [
|
||||||
"ruff format",
|
"ruff format",
|
||||||
|
|||||||
@@ -40,10 +40,12 @@ class TestAddToVectorStoreErrors:
|
|||||||
|
|
||||||
return memory
|
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"""
|
"""Test empty response from LLM during fact extraction"""
|
||||||
# Setup
|
# Setup
|
||||||
mock_memory.llm.generate_response.return_value = ""
|
mock_memory.llm.generate_response.return_value = ""
|
||||||
|
mock_capture_event = mocker.MagicMock()
|
||||||
|
mocker.patch("mem0.memory.main.capture_event", mock_capture_event)
|
||||||
|
|
||||||
# Execute
|
# Execute
|
||||||
with caplog.at_level(logging.ERROR):
|
with caplog.at_level(logging.ERROR):
|
||||||
@@ -52,9 +54,10 @@ class TestAddToVectorStoreErrors:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Verify
|
# 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 result == [] # Should return empty list when no memories processed
|
||||||
assert "Error in new_retrieved_facts" in caplog.text
|
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):
|
def test_empty_llm_response_memory_actions(self, mock_memory, caplog):
|
||||||
"""Test empty response from LLM during memory actions"""
|
"""Test empty response from LLM during memory actions"""
|
||||||
@@ -94,25 +97,31 @@ class TestAsyncAddToVectorStoreErrors:
|
|||||||
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
||||||
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
||||||
mock_async_memory.llm.generate_response.return_value = ""
|
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):
|
with caplog.at_level(logging.ERROR):
|
||||||
result = await mock_async_memory._add_to_vector_store(
|
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 result == []
|
||||||
assert "Error in new_retrieved_facts" in caplog.text
|
assert "Error in new_retrieved_facts" in caplog.text
|
||||||
|
assert mock_capture_event.call_count == 1
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_async_empty_llm_response_memory_actions(self, mock_async_memory, caplog, mocker):
|
async def test_async_empty_llm_response_memory_actions(self, mock_async_memory, caplog, mocker):
|
||||||
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
"""Test empty response in AsyncMemory._add_to_vector_store"""
|
||||||
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
|
||||||
mock_async_memory.llm.generate_response.side_effect = ['{"facts": ["test fact"]}', ""]
|
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):
|
with caplog.at_level(logging.ERROR):
|
||||||
result = await mock_async_memory._add_to_vector_store(
|
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 result == []
|
||||||
assert "Invalid JSON response" in caplog.text
|
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():
|
def memory_instance():
|
||||||
with (
|
with (
|
||||||
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
|
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.utils.factory.LlmFactory") as mock_llm,
|
||||||
patch("mem0.memory.telemetry.capture_event"),
|
patch("mem0.memory.telemetry.capture_event"),
|
||||||
patch("mem0.memory.graph_memory.MemoryGraph"),
|
patch("mem0.memory.graph_memory.MemoryGraph"),
|
||||||
):
|
):
|
||||||
mock_embedder.create.return_value = Mock()
|
mock_embedder.create.return_value = Mock()
|
||||||
mock_vector_store.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()
|
mock_llm.create.return_value = Mock()
|
||||||
|
|
||||||
config = MemoryConfig(version="v1.1")
|
config = MemoryConfig(version="v1.1")
|
||||||
@@ -37,13 +38,14 @@ def memory_instance():
|
|||||||
def memory_custom_instance():
|
def memory_custom_instance():
|
||||||
with (
|
with (
|
||||||
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
|
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.utils.factory.LlmFactory") as mock_llm,
|
||||||
patch("mem0.memory.telemetry.capture_event"),
|
patch("mem0.memory.telemetry.capture_event"),
|
||||||
patch("mem0.memory.graph_memory.MemoryGraph"),
|
patch("mem0.memory.graph_memory.MemoryGraph"),
|
||||||
):
|
):
|
||||||
mock_embedder.create.return_value = Mock()
|
mock_embedder.create.return_value = Mock()
|
||||||
mock_vector_store.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()
|
mock_llm.create.return_value = Mock()
|
||||||
|
|
||||||
config = MemoryConfig(
|
config = MemoryConfig(
|
||||||
@@ -250,7 +252,11 @@ def test_get_all(memory_instance, version, enable_graph, expected_result):
|
|||||||
|
|
||||||
def test_custom_prompts(memory_custom_instance):
|
def test_custom_prompts(memory_custom_instance):
|
||||||
messages = [{"role": "user", "content": "Test message"}]
|
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 = 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("mem0.memory.main.parse_messages", return_value="Test message") as mock_parse_messages:
|
||||||
with patch(
|
with patch(
|
||||||
@@ -273,7 +279,7 @@ def test_custom_prompts(memory_custom_instance):
|
|||||||
## custom update memory prompt
|
## custom update memory prompt
|
||||||
##
|
##
|
||||||
mock_get_update_memory_messages.assert_called_once_with(
|
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(
|
memory_custom_instance.llm.generate_response.assert_any_call(
|
||||||
|
|||||||
@@ -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"}
|
||||||
Reference in New Issue
Block a user