Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e77a10a8da | ||
|
|
e4307ae420 | ||
|
|
7c89d00079 | ||
|
|
563eaae5ee | ||
|
|
6733f78f81 | ||
|
|
c11637bd2f | ||
|
|
ff30cb8ddd | ||
|
|
2e853c3d22 | ||
|
|
3cc7013fde | ||
|
|
8e6a08aa83 | ||
|
|
8b9a8e5825 | ||
|
|
afc630272d | ||
|
|
e33008e3a4 | ||
|
|
7b516328a8 | ||
|
|
6d5889d98f | ||
|
|
ee66e0c954 | ||
|
|
1aed611539 | ||
|
|
6c2b131d6e | ||
|
|
2ffe9922f3 | ||
|
|
540ada489b | ||
|
|
65cffa0369 | ||
|
|
9f937943ba | ||
|
|
51a68bf7c5 | ||
|
|
92541d8955 | ||
|
|
0e0be18ecc | ||
|
|
66d3f9b93c | ||
|
|
b8f40f728f | ||
|
|
00a2ea9ff0 | ||
|
|
9545836469 | ||
|
|
3acd9e20da | ||
|
|
d48ecd52ef | ||
|
|
2fbea7705b | ||
|
|
d7a26bd0c3 | ||
|
|
e25dc4b504 | ||
|
|
dab3349990 | ||
|
|
6db87e8d07 | ||
|
|
faf811ee2d | ||
|
|
ee80a43810 | ||
|
|
4be426f762 | ||
|
|
ba9c61938b | ||
|
|
65f826e064 | ||
|
|
b43363cdf3 | ||
|
|
89e786a88e | ||
|
|
2d5062bd40 | ||
|
|
b89628322d |
@@ -12,8 +12,8 @@ install:
|
||||
|
||||
install_all:
|
||||
poetry install
|
||||
poetry run pip install groq together boto3 litellm ollama chromadb sentence_transformers vertexai \
|
||||
google-generativeai elasticsearch opensearch-py vecs
|
||||
poetry run pip install groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
||||
google-generativeai elasticsearch opensearch-py vecs pinecone pinecone-text
|
||||
|
||||
# Format code with ruff
|
||||
format:
|
||||
|
||||
@@ -84,6 +84,7 @@ Here's a comprehensive list of all parameters that can be used across different
|
||||
| `memory_add_embedding_type` | The type of embedding to use for the add memory action | VertexAI |
|
||||
| `memory_update_embedding_type` | The type of embedding to use for the update memory action | VertexAI |
|
||||
| `memory_search_embedding_type` | The type of embedding to use for the search memory action | VertexAI |
|
||||
| `lmstudio_base_url` | Base URL for LM Studio API | LM Studio |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
| Parameter | Description | Provider |
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
You can use embedding models from LM Studio to run Mem0 locally.
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "your_api_key" # For LLM
|
||||
|
||||
config = {
|
||||
"embedder": {
|
||||
"provider": "lmstudio",
|
||||
"config": {
|
||||
"model": "nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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="john")
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Here are the parameters available for configuring Ollama embedder:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `model` | The name of the OpenAI model to use | `nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf` |
|
||||
| `embedding_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `lmstudio_base_url` | Base URL for LM Studio connection | `http://localhost:1234/v1` |
|
||||
@@ -22,6 +22,7 @@ See the list of supported embedders below.
|
||||
<Card title="Gemini" href="/components/embedders/models/gemini"></Card>
|
||||
<Card title="Vertex AI" href="/components/embedders/models/vertexai"></Card>
|
||||
<Card title="Together" href="/components/embedders/models/together"></Card>
|
||||
<Card title="LM Studio" href="/components/embedders/models/lmstudio"></Card>
|
||||
</CardGroup>
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -108,6 +108,7 @@ Here's a comprehensive list of all parameters that can be used across different
|
||||
| `azure_kwargs` | Azure LLM args for initialization | AzureOpenAI |
|
||||
| `deepseek_base_url` | Base URL for DeepSeek API | DeepSeek |
|
||||
| `xai_base_url` | Base URL for XAI API | XAI |
|
||||
| `lmstudio_base_url` | Base URL for LM Studio API | LM Studio |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
| Parameter | Description | Provider |
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: LM Studio
|
||||
---
|
||||
|
||||
To use LM Studio with Mem0, you'll need to have LM Studio running locally with its server enabled. LM Studio provides a way to run local LLMs with an OpenAI-compatible API.
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "your-api-key" # used for embedding model
|
||||
|
||||
config = {
|
||||
"llm": {
|
||||
"provider": "lmstudio",
|
||||
"config": {
|
||||
"model": "lmstudio-community/Meta-Llama-3.1-70B-Instruct-GGUF/Meta-Llama-3.1-70B-Instruct-IQ2_M.gguf",
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 2000,
|
||||
"lmstudio_base_url": "http://localhost:1234/v1", # default LM Studio API URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"})
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Running Completely Locally
|
||||
|
||||
You can also use LM Studio for both LLM and embedding to run Mem0 entirely locally:
|
||||
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
# No external API keys needed!
|
||||
config = {
|
||||
"llm": {
|
||||
"provider": "lmstudio"
|
||||
},
|
||||
"embedder": {
|
||||
"provider": "lmstudio"
|
||||
}
|
||||
}
|
||||
|
||||
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="alice123", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
<Note>
|
||||
When using LM Studio for both LLM and embedding, make sure you have:
|
||||
1. An LLM model loaded for generating responses
|
||||
2. An embedding model loaded for vector embeddings
|
||||
3. The server enabled with the correct endpoints accessible
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
To use LM Studio, you need to:
|
||||
1. Download and install [LM Studio](https://lmstudio.ai/)
|
||||
2. Start a local server from the "Server" tab
|
||||
3. Set the appropriate `lmstudio_base_url` in your configuration (default is usually http://localhost:1234/v1)
|
||||
</Note>
|
||||
|
||||
## Config
|
||||
|
||||
All available parameters for the `lmstudio` config are present in [Master List of All Params in Config](../config).
|
||||
@@ -32,6 +32,7 @@ To view all supported llms, visit the [Supported LLMs](./models).
|
||||
<Card title="Gemini" href="/components/llms/models/gemini" />
|
||||
<Card title="DeepSeek" href="/components/llms/models/deepseek" />
|
||||
<Card title="xAI" href="/components/llms/models/xAI" />
|
||||
<Card title="LM Studio" href="/components/llms/models/lmstudio" />
|
||||
</CardGroup>
|
||||
|
||||
## Structured vs Unstructured Outputs
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[Azure AI Search](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search/) (formerly known as "Azure Cognitive Search") provides secure information retrieval at scale over user-owned content in traditional and generative AI search applications.
|
||||
[Azure AI Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search/) (formerly known as "Azure Cognitive Search") provides secure information retrieval at scale over user-owned content in traditional and generative AI search applications.
|
||||
|
||||
### Usage
|
||||
## Usage
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx" #this key is used for embedding purpose
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx" # This key is used for embedding purpose
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
@@ -15,8 +15,7 @@ config = {
|
||||
"service_name": "ai-search-test",
|
||||
"api_key": "*****",
|
||||
"collection_name": "mem0",
|
||||
"embedding_model_dims": 1536 ,
|
||||
"use_compression": False
|
||||
"embedding_model_dims": 1536
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,20 +24,72 @@ 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": "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
|
||||
## Using binary compression for large vector collections
|
||||
|
||||
Let's see the available parameters for the `qdrant` config:
|
||||
service_name (str): Azure Cognitive Search service name.
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `service_name` | Azure AI Search service name | `None` |
|
||||
| `api_key` | API key of the Azure AI Search service | `None` |
|
||||
| `collection_name` | The name of the collection/index to store the vectors, it will be created automatically if not exist | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `use_compression` | Use scalar quantization vector compression | False |
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "azure_ai_search",
|
||||
"config": {
|
||||
"service_name": "ai-search-test",
|
||||
"api_key": "*****",
|
||||
"collection_name": "mem0",
|
||||
"embedding_model_dims": 1536,
|
||||
"compression_type": "binary",
|
||||
"use_float16": True # Use half precision for storage efficiency
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using hybrid search
|
||||
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "azure_ai_search",
|
||||
"config": {
|
||||
"service_name": "ai-search-test",
|
||||
"api_key": "*****",
|
||||
"collection_name": "mem0",
|
||||
"embedding_model_dims": 1536,
|
||||
"hybrid_search": True,
|
||||
"vector_filter_mode": "postFilter"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
| Parameter | Description | Default Value | Options |
|
||||
| --- | --- | --- | --- |
|
||||
| `service_name` | Azure AI Search service name | Required | - |
|
||||
| `api_key` | API key of the Azure AI Search service | Required | - |
|
||||
| `collection_name` | The name of the collection/index to store vectors | `mem0` | Any valid index name |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` | Any integer value |
|
||||
| `compression_type` | Type of vector compression to use | `none` | `none`, `scalar`, `binary` |
|
||||
| `use_float16` | Store vectors in half precision (Edm.Half) | `False` | `True`, `False` |
|
||||
| `vector_filter_mode` | Vector filter mode to use | `preFilter` | `postFilter`, `preFilter` |
|
||||
| `hybrid_search` | Use hybrid search | `False` | `True`, `False` |
|
||||
|
||||
## Notes on Configuration Options
|
||||
|
||||
- **compression_type**:
|
||||
- `none`: No compression, uses full vector precision
|
||||
- `scalar`: Scalar quantization with reasonable balance of speed and accuracy
|
||||
- `binary`: Binary quantization for maximum compression with some accuracy trade-off
|
||||
|
||||
- **vector_filter_mode**:
|
||||
- `preFilter`: Applies filters before vector search (faster)
|
||||
- `postFilter`: Applies filters after vector search (may provide better relevance)
|
||||
|
||||
- **use_float16**: Using half precision (float16) reduces storage requirements but may slightly impact accuracy. Useful for very large vector collections.
|
||||
|
||||
- **Filterable Fields**: The implementation automatically extracts `user_id`, `run_id`, and `agent_id` fields from payloads for filtering.
|
||||
@@ -54,6 +54,7 @@ Let's see the available parameters for the `elasticsearch` config:
|
||||
| `password` | Password for basic authentication | `None` |
|
||||
| `verify_certs` | Whether to verify SSL certificates | `True` |
|
||||
| `auto_create_index` | Whether to automatically create the index | `True` |
|
||||
| `custom_search_query` | Function returning a custom search query | `None` |
|
||||
|
||||
### Features
|
||||
|
||||
@@ -62,3 +63,46 @@ Let's see the available parameters for the `elasticsearch` config:
|
||||
- Multiple authentication methods (Basic Auth, API Key)
|
||||
- Automatic index creation with optimized mappings for vector search
|
||||
- Memory isolation through payload filtering
|
||||
- Custom search query function to customize the search query
|
||||
|
||||
### Custom Search Query
|
||||
|
||||
The `custom_search_query` parameter allows you to customize the search query when `Memory.search` is called.
|
||||
|
||||
__Example__
|
||||
```python
|
||||
import os
|
||||
from typing import List, Optional, Dict
|
||||
from mem0 import Memory
|
||||
|
||||
def custom_search_query(query: List[float], limit: int, filters: Optional[Dict]) -> Dict:
|
||||
return {
|
||||
"knn": {
|
||||
"field": "vector",
|
||||
"query_vector": query,
|
||||
"k": limit,
|
||||
"num_candidates": limit * 2
|
||||
}
|
||||
}
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "elasticsearch",
|
||||
"config": {
|
||||
"collection_name": "mem0",
|
||||
"host": "localhost",
|
||||
"port": 9200,
|
||||
"embedding_model_dims": 1536,
|
||||
"custom_search_query": custom_search_query
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
It should be a function that takes the following parameters:
|
||||
- `query`: a query vector used in `Memory.search`
|
||||
- `limit`: a number of results used in `Memory.search`
|
||||
- `filters`: a dictionary of key-value pairs used in `Memory.search`. You can add custom pairs for the custom search query.
|
||||
|
||||
The function should return a query body for the Elasticsearch search API.
|
||||
@@ -0,0 +1,92 @@
|
||||
[Pinecone](https://www.pinecone.io/) is a fully managed vector database designed for machine learning applications, offering high performance vector search with low latency at scale. It's particularly well-suited for semantic search, recommendation systems, and other AI-powered applications.
|
||||
|
||||
> **Note**: Before configuring Pinecone, you need to select an embedding model (e.g., OpenAI, Cohere, or custom models) and ensure the `embedding_model_dims` in your config matches your chosen model's dimensions. For example, OpenAI's text-embedding-ada-002 uses 1536 dimensions.
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
||||
os.environ["PINECONE_API_KEY"] = "your-api-key"
|
||||
|
||||
# Example using serverless configuration
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "pinecone",
|
||||
"config": {
|
||||
"collection_name": "testing",
|
||||
"embedding_model_dims": 1536, # Matches OpenAI's text-embedding-3-small
|
||||
"serverless_config": {
|
||||
"cloud": "aws", # Choose between 'aws' or 'gcp' or 'azure'
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"metric": "cosine"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about a thriller 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 Pinecone:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `collection_name` | Name of the index/collection | Required |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model (must match your chosen embedding model) | Required |
|
||||
| `client` | Existing Pinecone client instance | `None` |
|
||||
| `api_key` | API key for Pinecone | Environment variable: `PINECONE_API_KEY` |
|
||||
| `environment` | Pinecone environment | `None` |
|
||||
| `serverless_config` | Configuration for serverless deployment (AWS or GCP or Azure) | `None` |
|
||||
| `pod_config` | Configuration for pod-based deployment | `None` |
|
||||
| `hybrid_search` | Whether to enable hybrid search | `False` |
|
||||
| `metric` | Distance metric for vector similarity | `"cosine"` |
|
||||
| `batch_size` | Batch size for operations | `100` |
|
||||
|
||||
> **Important**: You must choose either `serverless_config` or `pod_config` for your deployment, but not both.
|
||||
|
||||
#### Serverless Config Example
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "pinecone",
|
||||
"config": {
|
||||
"collection_name": "memory_index",
|
||||
"embedding_model_dims": 1536, # For OpenAI's text-embedding-3-small
|
||||
"serverless_config": {
|
||||
"cloud": "aws", # or "gcp" or "azure"
|
||||
"region": "us-east-1" # Choose appropriate region
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Pod Config Example
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "pinecone",
|
||||
"config": {
|
||||
"collection_name": "memory_index",
|
||||
"embedding_model_dims": 1536, # For OpenAI's text-embedding-ada-002
|
||||
"pod_config": {
|
||||
"environment": "gcp-starter",
|
||||
"replicas": 1,
|
||||
"pod_type": "starter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
[Weaviate](https://weaviate.io/) is an open-source vector search engine. It allows efficient storage and retrieval of high-dimensional vector embeddings, enabling powerful search and retrieval capabilities.
|
||||
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install weaviate weaviate-client
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```python Python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "weaviate",
|
||||
"config": {
|
||||
"collection_name": "test",
|
||||
"cluster_url": "http://localhost:8080",
|
||||
"auth_client_secret": None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about a thriller movie? They can be quite engaging."},
|
||||
{"role": "user", "content": "I’m not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Let's see the available parameters for the `weaviate` config:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `collection_name` | The name of the collection to store the vectors | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `cluster_url` | URL for the Weaviate server | `None` |
|
||||
| `auth_client_secret` | API key for Weaviate authentication | `None` |
|
||||
@@ -19,12 +19,14 @@ See the list of supported vector databases below.
|
||||
<Card title="Chroma" href="/components/vectordbs/dbs/chroma"></Card>
|
||||
<Card title="Pgvector" href="/components/vectordbs/dbs/pgvector"></Card>
|
||||
<Card title="Milvus" href="/components/vectordbs/dbs/milvus"></Card>
|
||||
<Card title="Pinecone" href="/components/vectordbs/dbs/pinecone"></Card>
|
||||
<Card title="Azure AI Search" href="/components/vectordbs/dbs/azure_ai_search"></Card>
|
||||
<Card title="Redis" href="/components/vectordbs/dbs/redis"></Card>
|
||||
<Card title="Elasticsearch" href="/components/vectordbs/dbs/elasticsearch"></Card>
|
||||
<Card title="OpenSearch" href="/components/vectordbs/dbs/opensearch"></Card>
|
||||
<Card title="Supabase" href="/components/vectordbs/dbs/supabase"></Card>
|
||||
<Card title="Vertex AI Vector Search" href="/components/vectordbs/dbs/vertex_ai_vector_search"></Card>
|
||||
<Card title="Weaviate" href="/components/vectordbs/dbs/weaviate"></Card>
|
||||
</CardGroup>
|
||||
|
||||
## Usage
|
||||
|
||||
+16
-4
@@ -47,6 +47,7 @@
|
||||
"pages": [
|
||||
"features/platform-overview",
|
||||
"features/advanced-retrieval",
|
||||
"features/contextual-add",
|
||||
"features/multimodal-support",
|
||||
"features/selective-memory",
|
||||
"features/custom-categories",
|
||||
@@ -54,7 +55,9 @@
|
||||
"features/direct-import",
|
||||
"features/async-client",
|
||||
"features/memory-export",
|
||||
"features/webhooks"
|
||||
"features/webhooks",
|
||||
"features/graph-memory",
|
||||
"features/feedback-mechanism"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -71,7 +74,8 @@
|
||||
"icon": "wrench",
|
||||
"pages": [
|
||||
"features/openai_compatibility",
|
||||
"features/custom-prompts",
|
||||
"features/custom-fact-extraction-prompt",
|
||||
"features/custom-update-memory-prompt",
|
||||
"open-source/multimodal-support",
|
||||
"open-source/features/rest-api"
|
||||
]
|
||||
@@ -125,12 +129,14 @@
|
||||
"components/vectordbs/dbs/chroma",
|
||||
"components/vectordbs/dbs/pgvector",
|
||||
"components/vectordbs/dbs/milvus",
|
||||
"components/vectordbs/dbs/pinecone",
|
||||
"components/vectordbs/dbs/azure_ai_search",
|
||||
"components/vectordbs/dbs/redis",
|
||||
"components/vectordbs/dbs/elasticsearch",
|
||||
"components/vectordbs/dbs/opensearch",
|
||||
"components/vectordbs/dbs/supabase",
|
||||
"components/vectordbs/dbs/vertex_ai_vector_search"
|
||||
"components/vectordbs/dbs/vertex_ai_vector_search",
|
||||
"components/vectordbs/dbs/weaviate"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -185,7 +191,10 @@
|
||||
"examples/chrome-extension",
|
||||
"examples/document-writing",
|
||||
"examples/multimodal-demo",
|
||||
"examples/personalized-deep-research"
|
||||
"examples/personalized-deep-research",
|
||||
"examples/mem0-agentic-tool",
|
||||
"examples/openai-inbuilt-tools",
|
||||
"examples/mem0-openai-voice-demo"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -333,6 +342,9 @@
|
||||
"posthog": {
|
||||
"apiKey": "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX",
|
||||
"apiHost": "https://mango.mem0.ai"
|
||||
},
|
||||
"intercom": {
|
||||
"appId": "jjv2r0tt"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
title: Mem0 as an Agentic Tool
|
||||
---
|
||||
|
||||
Integrate Mem0's memory capabilities with OpenAI's Agents SDK to create AI agents with persistent memory.
|
||||
You can create agents that remember past conversations and use that context to provide better responses.
|
||||
|
||||
## Installation
|
||||
|
||||
First, install the required packages:
|
||||
```bash
|
||||
pip install mem0ai pydantic openai-agents
|
||||
```
|
||||
|
||||
You'll also need a custom agents framework for this implementation.
|
||||
|
||||
## Setting Up Environment Variables
|
||||
|
||||
Store your Mem0 API key as an environment variable:
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="your_mem0_api_key"
|
||||
```
|
||||
|
||||
Or in your Python script:
|
||||
|
||||
```python
|
||||
import os
|
||||
os.environ["MEM0_API_KEY"] = "your_mem0_api_key"
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
The integration consists of three main components:
|
||||
|
||||
1. **Context Manager**: Defines user context for memory operations
|
||||
2. **Memory Tools**: Functions to add, search, and retrieve memories
|
||||
3. **Memory Agent**: An agent configured to use these memory tools
|
||||
|
||||
## Step-by-Step Implementation
|
||||
|
||||
### 1. Import Dependencies
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import asyncio
|
||||
from pydantic import BaseModel
|
||||
try:
|
||||
from mem0 import AsyncMemoryClient
|
||||
except ImportError:
|
||||
raise ImportError("mem0 is not installed. Please install it using 'pip install mem0ai'.")
|
||||
from agents import (
|
||||
Agent,
|
||||
ItemHelpers,
|
||||
MessageOutputItem,
|
||||
RunContextWrapper,
|
||||
Runner,
|
||||
ToolCallItem,
|
||||
ToolCallOutputItem,
|
||||
TResponseInputItem,
|
||||
function_tool,
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Define Memory Context
|
||||
|
||||
```python
|
||||
class Mem0Context(BaseModel):
|
||||
user_id: str | None = None
|
||||
```
|
||||
|
||||
### 3. Initialize the Mem0 Client
|
||||
|
||||
```python
|
||||
client = AsyncMemoryClient(api_key=os.getenv("MEM0_API_KEY"))
|
||||
```
|
||||
|
||||
### 4. Create Memory Tools
|
||||
|
||||
#### Add to Memory
|
||||
|
||||
```python
|
||||
@function_tool
|
||||
async def add_to_memory(
|
||||
context: RunContextWrapper[Mem0Context],
|
||||
content: str,
|
||||
) -> str:
|
||||
"""
|
||||
Add a message to Mem0
|
||||
Args:
|
||||
content: The content to store in memory.
|
||||
"""
|
||||
messages = [{"role": "user", "content": content}]
|
||||
user_id = context.context.user_id or "default_user"
|
||||
await client.add(messages, user_id=user_id)
|
||||
return f"Stored message: {content}"
|
||||
```
|
||||
|
||||
#### Search Memory
|
||||
|
||||
```python
|
||||
@function_tool
|
||||
async def search_memory(
|
||||
context: RunContextWrapper[Mem0Context],
|
||||
query: str,
|
||||
) -> str:
|
||||
"""
|
||||
Search for memories in Mem0
|
||||
Args:
|
||||
query: The search query.
|
||||
"""
|
||||
user_id = context.context.user_id or "default_user"
|
||||
memories = await client.search(query, user_id=user_id, output_format="v1.1")
|
||||
results = '\n'.join([result["memory"] for result in memories["results"]])
|
||||
return str(results)
|
||||
```
|
||||
|
||||
#### Get All Memories
|
||||
|
||||
```python
|
||||
@function_tool
|
||||
async def get_all_memory(
|
||||
context: RunContextWrapper[Mem0Context],
|
||||
) -> str:
|
||||
"""Retrieve all memories from Mem0"""
|
||||
user_id = context.context.user_id or "default_user"
|
||||
memories = await client.get_all(user_id=user_id, output_format="v1.1")
|
||||
results = '\n'.join([result["memory"] for result in memories["results"]])
|
||||
return str(results)
|
||||
```
|
||||
|
||||
### 5. Configure the Memory Agent
|
||||
|
||||
```python
|
||||
memory_agent = Agent[Mem0Context](
|
||||
name="Memory Assistant",
|
||||
instructions="""You are a helpful assistant with memory capabilities. You can:
|
||||
1. Store new information using add_to_memory
|
||||
2. Search existing information using search_memory
|
||||
3. Retrieve all stored information using get_all_memory
|
||||
When users ask questions:
|
||||
- If they want to store information, use add_to_memory
|
||||
- If they're searching for specific information, use search_memory
|
||||
- If they want to see everything stored, use get_all_memory""",
|
||||
tools=[add_to_memory, search_memory, get_all_memory],
|
||||
)
|
||||
```
|
||||
|
||||
### 6. Implement the Main Runtime Loop
|
||||
|
||||
```python
|
||||
async def main():
|
||||
current_agent: Agent[Mem0Context] = memory_agent
|
||||
input_items: list[TResponseInputItem] = []
|
||||
context = Mem0Context()
|
||||
while True:
|
||||
user_input = input("Enter your message (or 'quit' to exit): ")
|
||||
if user_input.lower() == 'quit':
|
||||
break
|
||||
input_items.append({"content": user_input, "role": "user"})
|
||||
result = await Runner.run(current_agent, input_items, context=context)
|
||||
for new_item in result.new_items:
|
||||
agent_name = new_item.agent.name
|
||||
if isinstance(new_item, MessageOutputItem):
|
||||
print(f"{agent_name}: {ItemHelpers.text_message_output(new_item)}")
|
||||
elif isinstance(new_item, ToolCallItem):
|
||||
print(f"{agent_name}: Calling a tool")
|
||||
elif isinstance(new_item, ToolCallOutputItem):
|
||||
print(f"{agent_name}: Tool call output: {new_item.output}")
|
||||
else:
|
||||
print(f"{agent_name}: Skipping item: {new_item.__class__.__name__}")
|
||||
input_items = result.to_input_list()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Storing Information
|
||||
|
||||
```
|
||||
User: Remember that my favorite color is blue
|
||||
Agent: Calling a tool
|
||||
Agent: Tool call output: Stored message: my favorite color is blue
|
||||
Agent: I've stored that your favorite color is blue in my memory. I'll remember that for future conversations.
|
||||
```
|
||||
|
||||
### Searching Memory
|
||||
|
||||
```
|
||||
User: What's my favorite color?
|
||||
Agent: Calling a tool
|
||||
Agent: Tool call output: my favorite color is blue
|
||||
Agent: Your favorite color is blue, based on what you've told me earlier.
|
||||
```
|
||||
|
||||
### Retrieving All Memories
|
||||
|
||||
```
|
||||
User: What do you know about me?
|
||||
Agent: Calling a tool
|
||||
Agent: Tool call output: favorite color is blue
|
||||
my birthday is on March 15
|
||||
Agent: Based on our previous conversations, I know that:
|
||||
1. Your favorite color is blue
|
||||
2. Your birthday is on March 15
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom User IDs
|
||||
|
||||
You can specify different user IDs to maintain separate memory stores for multiple users:
|
||||
|
||||
```python
|
||||
context = Mem0Context(user_id="user123")
|
||||
```
|
||||
|
||||
|
||||
## Resources
|
||||
|
||||
- [Mem0 Documentation](https://docs.mem0.ai)
|
||||
- [Mem0 Dashboard](https://app.mem0.ai/dashboard)
|
||||
- [API Reference](https://docs.mem0.ai/api-reference)
|
||||
@@ -0,0 +1,538 @@
|
||||
---
|
||||
title: 'Mem0 with OpenAI Agents SDK for Voice'
|
||||
description: 'Integrate memory capabilities into your voice agents using Mem0 and OpenAI Agents SDK'
|
||||
---
|
||||
|
||||
# Building Voice Agents with Memory using Mem0 and OpenAI Agents SDK
|
||||
|
||||
This guide demonstrates how to combine OpenAI's Agents SDK for voice applications with Mem0's memory capabilities to create a voice assistant that remembers user preferences and past interactions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, make sure you have:
|
||||
|
||||
1. Installed OpenAI Agents SDK with voice dependencies:
|
||||
```bash
|
||||
pip install 'openai-agents[voice]'
|
||||
```
|
||||
|
||||
2. Installed Mem0 SDK:
|
||||
```bash
|
||||
pip install mem0ai
|
||||
```
|
||||
|
||||
3. Installed other required dependencies:
|
||||
```bash
|
||||
pip install numpy sounddevice pydantic
|
||||
```
|
||||
|
||||
4. Set up your API keys:
|
||||
- OpenAI API key for the Agents SDK
|
||||
- Mem0 API key from the Mem0 Platform
|
||||
|
||||
## Code Breakdown
|
||||
|
||||
Let's break down the key components of this implementation:
|
||||
|
||||
### 1. Setting Up Dependencies and Environment
|
||||
|
||||
```python
|
||||
# OpenAI Agents SDK imports
|
||||
from agents import (
|
||||
Agent,
|
||||
function_tool
|
||||
)
|
||||
from agents.voice import (
|
||||
AudioInput,
|
||||
SingleAgentVoiceWorkflow,
|
||||
VoicePipeline
|
||||
)
|
||||
from agents.extensions.handoff_prompt import prompt_with_handoff_instructions
|
||||
|
||||
# Mem0 imports
|
||||
from mem0 import AsyncMemoryClient
|
||||
|
||||
# Set up API keys (replace with your actual keys)
|
||||
os.environ["OPENAI_API_KEY"] = "your-openai-api-key"
|
||||
os.environ["MEM0_API_KEY"] = "your-mem0-api-key"
|
||||
|
||||
# Define a global user ID for simplicity
|
||||
USER_ID = "voice_user"
|
||||
|
||||
# Initialize Mem0 client
|
||||
mem0_client = AsyncMemoryClient()
|
||||
```
|
||||
|
||||
This section handles:
|
||||
- Importing required modules from OpenAI Agents SDK and Mem0
|
||||
- Setting up environment variables for API keys
|
||||
- Defining a simple user identification system (using a global variable)
|
||||
- Initializing the Mem0 client that will handle memory operations
|
||||
|
||||
### 2. Memory Tools with Function Decorators
|
||||
|
||||
The `@function_tool` decorator transforms Python functions into callable tools for the OpenAI agent. Here are the key memory tools:
|
||||
|
||||
#### Storing User Memories
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
# Set up logging at the top of your file
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
force=True
|
||||
)
|
||||
logger = logging.getLogger("memory_voice_agent")
|
||||
|
||||
# Then use logger in your function tools
|
||||
@function_tool
|
||||
async def save_memories(
|
||||
memory: str
|
||||
) -> str:
|
||||
"""Store a user memory in memory."""
|
||||
# This will be visible in your console
|
||||
logger.debug(f"Saving memory: {memory} for user {USER_ID}")
|
||||
|
||||
# Store the preference in Mem0
|
||||
memory_content = f"User memory - {memory}"
|
||||
await mem0_client.add(
|
||||
memory_content,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
|
||||
return f"I've saved your memory: {memory}"
|
||||
```
|
||||
|
||||
This function:
|
||||
- Takes a memory string
|
||||
- Creates a formatted memory string
|
||||
- Stores it in Mem0 using the `add()` method
|
||||
- Includes metadata to categorize the memory for easier retrieval
|
||||
- Returns a confirmation message that the agent will speak
|
||||
|
||||
#### Finding Relevant Memories
|
||||
|
||||
```python
|
||||
@function_tool
|
||||
async def search_memories(
|
||||
query: str
|
||||
) -> str:
|
||||
"""
|
||||
Find memories relevant to the current conversation.
|
||||
Args:
|
||||
query: The search query to find relevant memories
|
||||
"""
|
||||
print(f"Finding memories related to: {query}")
|
||||
results = await mem0_client.search(
|
||||
query,
|
||||
user_id=USER_ID,
|
||||
limit=5,
|
||||
threshold=0.7, # Higher threshold for more relevant results
|
||||
output_format="v1.1"
|
||||
)
|
||||
|
||||
# Format and return the results
|
||||
if not results.get('results', []):
|
||||
return "I don't have any relevant memories about this topic."
|
||||
|
||||
memories = [f"• {result['memory']}" for result in results.get('results', [])]
|
||||
return "Here's what I remember that might be relevant:\n" + "\n".join(memories)
|
||||
```
|
||||
|
||||
This tool:
|
||||
- Takes a search query string
|
||||
- Passes it to Mem0's semantic search to find related memories
|
||||
- Sets a threshold for relevance to ensure quality results
|
||||
- Returns a formatted list of relevant memories or a default message
|
||||
|
||||
### 3. Creating the Voice Agent
|
||||
|
||||
```python
|
||||
def create_memory_voice_agent():
|
||||
# Create the agent with memory-enabled tools
|
||||
agent = Agent(
|
||||
name="Memory Assistant",
|
||||
instructions=prompt_with_handoff_instructions(
|
||||
"""You're speaking to a human, so be polite and concise.
|
||||
Always respond in clear, natural English.
|
||||
You have the ability to remember information about the user.
|
||||
Use the save_memories tool when the user shares an important information worth remembering.
|
||||
Use the search_memories tool when you need context from past conversations or user asks you to recall something.
|
||||
""",
|
||||
),
|
||||
model="gpt-4o",
|
||||
tools=[save_memories, search_memories],
|
||||
)
|
||||
|
||||
return agent
|
||||
```
|
||||
|
||||
This function:
|
||||
- Creates an OpenAI Agent with specific instructions
|
||||
- Configures it to use gpt-4o (you can use other models)
|
||||
- Registers the memory-related tools with the agent
|
||||
- Uses `prompt_with_handoff_instructions` to include standard voice agent behaviors
|
||||
|
||||
### 4. Microphone Recording Functionality
|
||||
|
||||
```python
|
||||
async def record_from_microphone(duration=5, samplerate=24000):
|
||||
"""Record audio from the microphone for a specified duration."""
|
||||
print(f"Recording for {duration} seconds...")
|
||||
|
||||
# Create a buffer to store the recorded audio
|
||||
frames = []
|
||||
|
||||
# Callback function to store audio data
|
||||
def callback(indata, frames_count, time_info, status):
|
||||
frames.append(indata.copy())
|
||||
|
||||
# Start recording
|
||||
with sd.InputStream(samplerate=samplerate, channels=1, callback=callback, dtype=np.int16):
|
||||
await asyncio.sleep(duration)
|
||||
|
||||
# Combine all frames into a single numpy array
|
||||
audio_data = np.concatenate(frames)
|
||||
return audio_data
|
||||
```
|
||||
|
||||
This function:
|
||||
- Creates a simple asynchronous microphone recording function
|
||||
- Uses the sounddevice library to capture audio input
|
||||
- Stores frames in a buffer during recording
|
||||
- Combines frames into a single numpy array when complete
|
||||
- Returns the audio data for processing
|
||||
|
||||
### 5. Main Loop and Voice Processing
|
||||
|
||||
```python
|
||||
async def main():
|
||||
# Create the agent
|
||||
agent = create_memory_voice_agent()
|
||||
|
||||
# Set up the voice pipeline
|
||||
pipeline = VoicePipeline(
|
||||
workflow=SingleAgentVoiceWorkflow(agent)
|
||||
)
|
||||
|
||||
# Configure TTS settings
|
||||
pipeline.config.tts_settings.voice = "alloy"
|
||||
pipeline.config.tts_settings.speed = 1.0
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Get user input
|
||||
print("\nPress Enter to start recording (or 'q' to quit)...")
|
||||
user_input = input()
|
||||
if user_input.lower() == 'q':
|
||||
break
|
||||
|
||||
# Record and process audio
|
||||
audio_data = await record_from_microphone(duration=5)
|
||||
audio_input = AudioInput(buffer=audio_data)
|
||||
result = await pipeline.run(audio_input)
|
||||
|
||||
# Play response and handle events
|
||||
player = sd.OutputStream(samplerate=24000, channels=1, dtype=np.int16)
|
||||
player.start()
|
||||
|
||||
agent_response = ""
|
||||
print("\nAgent response:")
|
||||
|
||||
async for event in result.stream():
|
||||
if event.type == "voice_stream_event_audio":
|
||||
player.write(event.data)
|
||||
elif event.type == "voice_stream_event_content":
|
||||
content = event.data
|
||||
agent_response += content
|
||||
print(content, end="", flush=True)
|
||||
|
||||
# Save the agent's response to memory
|
||||
if agent_response:
|
||||
try:
|
||||
await mem0_client.add(
|
||||
f"Agent response: {agent_response}",
|
||||
user_id=USER_ID,
|
||||
metadata={"type": "agent_response"}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to store memory: {e}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting...")
|
||||
```
|
||||
|
||||
This main function orchestrates the entire process:
|
||||
1. Creates the memory-enabled voice agent
|
||||
2. Sets up the voice pipeline with TTS settings
|
||||
3. Implements an interactive loop for recording and processing voice input
|
||||
4. Handles streaming of response events (both audio and text)
|
||||
5. Automatically saves the agent's responses to memory
|
||||
6. Includes proper error handling and exit mechanisms
|
||||
|
||||
## Create a Memory-Enabled Voice Agent
|
||||
|
||||
Now that we've explained each component, here's the complete implementation that combines OpenAI Agents SDK for voice with Mem0's memory capabilities:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Any
|
||||
import numpy as np
|
||||
import sounddevice as sd
|
||||
from pydantic import BaseModel
|
||||
|
||||
# OpenAI Agents SDK imports
|
||||
from agents import (
|
||||
Agent,
|
||||
function_tool
|
||||
)
|
||||
from agents.voice import (
|
||||
AudioInput,
|
||||
SingleAgentVoiceWorkflow,
|
||||
VoicePipeline
|
||||
)
|
||||
from agents.extensions.handoff_prompt import prompt_with_handoff_instructions
|
||||
|
||||
# Mem0 imports
|
||||
from mem0 import AsyncMemoryClient
|
||||
|
||||
# Set up API keys (replace with your actual keys)
|
||||
os.environ["OPENAI_API_KEY"] = "your-openai-api-key"
|
||||
os.environ["MEM0_API_KEY"] = "your-mem0-api-key"
|
||||
|
||||
# Define a global user ID for simplicity
|
||||
USER_ID = "voice_user"
|
||||
|
||||
# Initialize Mem0 client
|
||||
mem0_client = AsyncMemoryClient()
|
||||
|
||||
# Create tools that utilize Mem0's memory
|
||||
@function_tool
|
||||
async def save_memories(
|
||||
memory: str
|
||||
) -> str:
|
||||
"""
|
||||
Store a user memory in memory.
|
||||
Args:
|
||||
memory: The memory to save
|
||||
"""
|
||||
print(f"Saving memory: {memory} for user {USER_ID}")
|
||||
|
||||
# Store the preference in Mem0
|
||||
memory_content = f"User memory - {memory}"
|
||||
await mem0_client.add(
|
||||
memory_content,
|
||||
user_id=USER_ID,
|
||||
)
|
||||
|
||||
return f"I've saved your memory: {memory}"
|
||||
|
||||
@function_tool
|
||||
async def search_memories(
|
||||
query: str
|
||||
) -> str:
|
||||
"""
|
||||
Find memories relevant to the current conversation.
|
||||
Args:
|
||||
query: The search query to find relevant memories
|
||||
"""
|
||||
print(f"Finding memories related to: {query}")
|
||||
results = await mem0_client.search(
|
||||
query,
|
||||
user_id=USER_ID,
|
||||
limit=5,
|
||||
threshold=0.7, # Higher threshold for more relevant results
|
||||
output_format="v1.1"
|
||||
)
|
||||
|
||||
# Format and return the results
|
||||
if not results.get('results', []):
|
||||
return "I don't have any relevant memories about this topic."
|
||||
|
||||
memories = [f"• {result['memory']}" for result in results.get('results', [])]
|
||||
return "Here's what I remember that might be relevant:\n" + "\n".join(memories)
|
||||
|
||||
# Create the agent with memory-enabled tools
|
||||
def create_memory_voice_agent():
|
||||
# Create the agent with memory-enabled tools
|
||||
agent = Agent(
|
||||
name="Memory Assistant",
|
||||
instructions=prompt_with_handoff_instructions(
|
||||
"""You're speaking to a human, so be polite and concise.
|
||||
Always respond in clear, natural English.
|
||||
You have the ability to remember information about the user.
|
||||
Use the save_memories tool when the user shares an important information worth remembering.
|
||||
Use the search_memories tool when you need context from past conversations or user asks you to recall something.
|
||||
""",
|
||||
),
|
||||
model="gpt-4o",
|
||||
tools=[save_memories, search_memories],
|
||||
)
|
||||
|
||||
return agent
|
||||
|
||||
async def record_from_microphone(duration=5, samplerate=24000):
|
||||
"""Record audio from the microphone for a specified duration."""
|
||||
print(f"Recording for {duration} seconds...")
|
||||
|
||||
# Create a buffer to store the recorded audio
|
||||
frames = []
|
||||
|
||||
# Callback function to store audio data
|
||||
def callback(indata, frames_count, time_info, status):
|
||||
frames.append(indata.copy())
|
||||
|
||||
# Start recording
|
||||
with sd.InputStream(samplerate=samplerate, channels=1, callback=callback, dtype=np.int16):
|
||||
await asyncio.sleep(duration)
|
||||
|
||||
# Combine all frames into a single numpy array
|
||||
audio_data = np.concatenate(frames)
|
||||
return audio_data
|
||||
|
||||
async def main():
|
||||
print("Starting Memory Voice Agent")
|
||||
|
||||
# Create the agent and context
|
||||
agent = create_memory_voice_agent()
|
||||
|
||||
# Set up the voice pipeline
|
||||
pipeline = VoicePipeline(
|
||||
workflow=SingleAgentVoiceWorkflow(agent)
|
||||
)
|
||||
|
||||
# Configure TTS settings
|
||||
pipeline.config.tts_settings.voice = "alloy"
|
||||
pipeline.config.tts_settings.speed = 1.0
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Get user input
|
||||
print("\nPress Enter to start recording (or 'q' to quit)...")
|
||||
user_input = input()
|
||||
if user_input.lower() == 'q':
|
||||
break
|
||||
|
||||
# Record and process audio
|
||||
audio_data = await record_from_microphone(duration=5)
|
||||
audio_input = AudioInput(buffer=audio_data)
|
||||
|
||||
print("Processing your request...")
|
||||
|
||||
# Process the audio input
|
||||
result = await pipeline.run(audio_input)
|
||||
|
||||
# Create an audio player
|
||||
player = sd.OutputStream(samplerate=24000, channels=1, dtype=np.int16)
|
||||
player.start()
|
||||
|
||||
# Store the agent's response for adding to memory
|
||||
agent_response = ""
|
||||
|
||||
print("\nAgent response:")
|
||||
# Play the audio stream as it comes in
|
||||
async for event in result.stream():
|
||||
if event.type == "voice_stream_event_audio":
|
||||
player.write(event.data)
|
||||
elif event.type == "voice_stream_event_content":
|
||||
# Accumulate and print the text response
|
||||
content = event.data
|
||||
agent_response += content
|
||||
print(content, end="", flush=True)
|
||||
|
||||
print("\n")
|
||||
|
||||
# Example of saving the conversation to Mem0 after completion
|
||||
if agent_response:
|
||||
try:
|
||||
await mem0_client.add(
|
||||
f"Agent response: {agent_response}",
|
||||
user_id=USER_ID,
|
||||
metadata={"type": "agent_response"}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to store memory: {e}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Key Features of This Implementation
|
||||
|
||||
This implementation offers several key features:
|
||||
|
||||
1. **Simplified User Management**: Uses a global `USER_ID` variable for simplicity, but can be extended to manage multiple users.
|
||||
|
||||
2. **Real Microphone Input**: Includes a `record_from_microphone()` function that captures actual voice input from your microphone.
|
||||
|
||||
3. **Interactive Voice Loop**: Implements a continuous interaction loop, allowing for multiple back-and-forth exchanges.
|
||||
|
||||
4. **Memory Management Tools**:
|
||||
- `save_memories`: Stores user memories in Mem0
|
||||
- `search_memories`: Searches for relevant past information
|
||||
|
||||
5. **Voice Configuration**: Demonstrates how to configure TTS settings for the voice response.
|
||||
|
||||
## Running the Example
|
||||
|
||||
To run this example:
|
||||
|
||||
1. Replace the placeholder API keys with your actual keys
|
||||
2. Make sure your microphone is properly connected
|
||||
3. Run the script with Python 3.8 or newer
|
||||
4. Press Enter to start recording, then speak your request
|
||||
5. Press 'q' to quit the application
|
||||
|
||||
The agent will listen to your request, process it through the OpenAI model, utilize Mem0 for memory operations as needed, and respond both through text output and voice speech.
|
||||
|
||||
## Best Practices for Voice Agents with Memory
|
||||
|
||||
1. **Optimizing Memory for Voice**: Keep memories concise and relevant for voice responses.
|
||||
|
||||
2. **Forgetting Mechanism**: Implement a way to delete or expire memories that are no longer relevant.
|
||||
|
||||
3. **Context Preservation**: Store enough context with each memory to make retrieval effective.
|
||||
|
||||
4. **Error Handling**: Implement robust error handling for memory operations, as voice interactions should continue smoothly even if memory operations fail.
|
||||
|
||||
## Conclusion
|
||||
|
||||
By combining OpenAI's Agents SDK with Mem0's memory capabilities, you can create voice agents that maintain persistent memory of user preferences and past interactions. This significantly enhances the user experience by making conversations more natural and personalized.
|
||||
|
||||
As you build your voice application, experiment with different memory strategies and filtering approaches to find the optimal balance between comprehensive memory and efficient retrieval for your specific use case.
|
||||
|
||||
## Debugging Function Tools
|
||||
|
||||
When working with the OpenAI Agents SDK, you might notice that regular `print()` statements inside `@function_tool` decorated functions don't appear in your console output. This is because the Agents SDK captures and redirects standard output when executing these functions.
|
||||
|
||||
To effectively debug your function tools, use Python's `logging` module instead:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
# Set up logging at the top of your file
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
force=True
|
||||
)
|
||||
logger = logging.getLogger("memory_voice_agent")
|
||||
|
||||
# Then use logger in your function tools
|
||||
@function_tool
|
||||
async def save_memories(
|
||||
memory: str
|
||||
) -> str:
|
||||
"""Store a user memory in memory."""
|
||||
# This will be visible in your console
|
||||
logger.debug(f"Saving memory: {memory} for user {USER_ID}")
|
||||
|
||||
# Rest of your function...
|
||||
```
|
||||
@@ -0,0 +1,312 @@
|
||||
---
|
||||
title: OpenAI Inbuilt Tools
|
||||
---
|
||||
|
||||
Integrate Mem0’s memory capabilities with OpenAI’s Inbuilt Tools to create AI agents with persistent memory.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install mem0ai openai zod
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Save your Mem0 and OpenAI API keys in a `.env` file:
|
||||
|
||||
```
|
||||
MEM0_API_KEY=your_mem0_api_key
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
```
|
||||
|
||||
Get your Mem0 API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys).
|
||||
|
||||
### Configuration
|
||||
|
||||
```javascript
|
||||
const mem0Config = {
|
||||
apiKey: process.env.MEM0_API_KEY,
|
||||
user_id: "sample-user",
|
||||
};
|
||||
|
||||
const openAIClient = new OpenAI();
|
||||
const mem0Client = new MemoryClient(mem0Config);
|
||||
```
|
||||
|
||||
### Adding Memories
|
||||
|
||||
Store user preferences, past interactions, or any relevant information:
|
||||
<CodeGroup>
|
||||
```javascript JavaScript
|
||||
async function addUserPreferences() {
|
||||
const mem0Client = new MemoryClient(mem0Config);
|
||||
|
||||
const userPreferences = "I Love BMW, Audi and Porsche. I Hate Mercedes. I love Red cars and Maroon cars. I have a budget of 120K to 150K USD. I like Audi the most.";
|
||||
|
||||
await mem0Client.add([{
|
||||
role: "user",
|
||||
content: userPreferences,
|
||||
}], mem0Config);
|
||||
}
|
||||
|
||||
await addUserPreferences();
|
||||
```
|
||||
|
||||
```json Output (Memories)
|
||||
[
|
||||
{
|
||||
"id": "ff9f3367-9e83-415d-b9c5-dc8befd9a4b4",
|
||||
"data": { "memory": "Loves BMW, Audi, and Porsche" },
|
||||
"event": "ADD"
|
||||
},
|
||||
{
|
||||
"id": "04172ce6-3d7b-45a3-b4a1-ee9798593cb4",
|
||||
"data": { "memory": "Hates Mercedes" },
|
||||
"event": "ADD"
|
||||
},
|
||||
{
|
||||
"id": "db363a5d-d258-4953-9e4c-777c120de34d",
|
||||
"data": { "memory": "Loves red cars and maroon cars" },
|
||||
"event": "ADD"
|
||||
},
|
||||
{
|
||||
"id": "5519aaad-a2ac-4c0d-81d7-0d55c6ecdba8",
|
||||
"data": { "memory": "Has a budget of 120K to 150K USD" },
|
||||
"event": "ADD"
|
||||
},
|
||||
{
|
||||
"id": "523b7693-7344-4563-922f-5db08edc8634",
|
||||
"data": { "memory": "Likes Audi the most" },
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
```
|
||||
</CodeGroup>
|
||||
### Retrieving Memories
|
||||
|
||||
Search for relevant memories based on the current user input:
|
||||
|
||||
```javascript
|
||||
const relevantMemories = await mem0Client.search(userInput, mem0Config);
|
||||
```
|
||||
|
||||
### Structured Responses with Zod
|
||||
|
||||
Define structured response schemas to get consistent output formats:
|
||||
|
||||
```javascript
|
||||
// Define the schema for a car recommendation
|
||||
const CarSchema = z.object({
|
||||
car_name: z.string(),
|
||||
car_price: z.string(),
|
||||
car_url: z.string(),
|
||||
car_image: z.string(),
|
||||
car_description: z.string(),
|
||||
});
|
||||
|
||||
// Schema for a list of car recommendations
|
||||
const Cars = z.object({
|
||||
cars: z.array(CarSchema),
|
||||
});
|
||||
|
||||
// Create a function tool based on the schema
|
||||
const carRecommendationTool = zodResponsesFunction({
|
||||
name: "carRecommendations",
|
||||
parameters: Cars
|
||||
});
|
||||
|
||||
// Use the tool in your OpenAI request
|
||||
const response = await openAIClient.responses.create({
|
||||
model: "gpt-4o",
|
||||
tools: [{ type: "web_search_preview" }, carRecommendationTool],
|
||||
input: `${getMemoryString(relevantMemories)}\n${userInput}`,
|
||||
});
|
||||
```
|
||||
|
||||
### Using Web Search
|
||||
|
||||
Combine memory with web search for up-to-date recommendations:
|
||||
|
||||
```javascript
|
||||
const response = await openAIClient.responses.create({
|
||||
model: "gpt-4o",
|
||||
tools: [{ type: "web_search_preview" }, carRecommendationTool],
|
||||
input: `${getMemoryString(relevantMemories)}\n${userInput}`,
|
||||
});
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Car Recommendation System
|
||||
|
||||
```javascript
|
||||
import MemoryClient from "mem0ai";
|
||||
import { OpenAI } from "openai";
|
||||
import { zodResponsesFunction } from "openai/helpers/zod";
|
||||
import { z } from "zod";
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const mem0Config = {
|
||||
apiKey: process.env.MEM0_API_KEY,
|
||||
user_id: "sample-user",
|
||||
};
|
||||
|
||||
async function run() {
|
||||
// Responses without memories
|
||||
console.log("\n\nRESPONSES WITHOUT MEMORIES\n\n");
|
||||
await main();
|
||||
|
||||
// Adding sample memories
|
||||
await addSampleMemories();
|
||||
|
||||
// Responses with memories
|
||||
console.log("\n\nRESPONSES WITH MEMORIES\n\n");
|
||||
await main(true);
|
||||
}
|
||||
|
||||
// OpenAI Response Schema
|
||||
const CarSchema = z.object({
|
||||
car_name: z.string(),
|
||||
car_price: z.string(),
|
||||
car_url: z.string(),
|
||||
car_image: z.string(),
|
||||
car_description: z.string(),
|
||||
});
|
||||
|
||||
const Cars = z.object({
|
||||
cars: z.array(CarSchema),
|
||||
});
|
||||
|
||||
async function main(memory = false) {
|
||||
const openAIClient = new OpenAI();
|
||||
const mem0Client = new MemoryClient(mem0Config);
|
||||
|
||||
const input = "Suggest me some cars that I can buy today.";
|
||||
|
||||
const tool = zodResponsesFunction({ name: "carRecommendations", parameters: Cars });
|
||||
|
||||
// Store the user input as a memory
|
||||
await mem0Client.add([{
|
||||
role: "user",
|
||||
content: input,
|
||||
}], mem0Config);
|
||||
|
||||
// Search for relevant memories
|
||||
let relevantMemories = []
|
||||
if (memory) {
|
||||
relevantMemories = await mem0Client.search(input, mem0Config);
|
||||
}
|
||||
|
||||
const response = await openAIClient.responses.create({
|
||||
model: "gpt-4o",
|
||||
tools: [{ type: "web_search_preview" }, tool],
|
||||
input: `${getMemoryString(relevantMemories)}\n${input}`,
|
||||
});
|
||||
|
||||
console.log(response.output);
|
||||
}
|
||||
|
||||
async function addSampleMemories() {
|
||||
const mem0Client = new MemoryClient(mem0Config);
|
||||
|
||||
const myInterests = "I Love BMW, Audi and Porsche. I Hate Mercedes. I love Red cars and Maroon cars. I have a budget of 120K to 150K USD. I like Audi the most.";
|
||||
|
||||
await mem0Client.add([{
|
||||
role: "user",
|
||||
content: myInterests,
|
||||
}], mem0Config);
|
||||
}
|
||||
|
||||
const getMemoryString = (memories) => {
|
||||
const MEMORY_STRING_PREFIX = "These are the memories I have stored. Give more weightage to the question by users and try to answer that first. You have to modify your answer based on the memories I have provided. If the memories are irrelevant you can ignore them. Also don't reply to this section of the prompt, or the memories, they are only for your reference. The MEMORIES of the USER are: \n\n";
|
||||
const memoryString = memories.map((mem) => `${mem.memory}`).join("\n") ?? "";
|
||||
return memoryString.length > 0 ? `${MEMORY_STRING_PREFIX}${memoryString}` : "";
|
||||
};
|
||||
|
||||
run().catch(console.error);
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
<CodeGroup>
|
||||
```json Without Memories
|
||||
{
|
||||
"cars": [
|
||||
{
|
||||
"car_name": "Toyota Camry",
|
||||
"car_price": "$25,000",
|
||||
"car_url": "https://www.toyota.com/camry/",
|
||||
"car_image": "https://link-to-toyota-camry-image.com",
|
||||
"car_description": "Reliable mid-size sedan with great fuel efficiency."
|
||||
},
|
||||
{
|
||||
"car_name": "Honda Accord",
|
||||
"car_price": "$26,000",
|
||||
"car_url": "https://www.honda.com/accord/",
|
||||
"car_image": "https://link-to-honda-accord-image.com",
|
||||
"car_description": "Comfortable and spacious with advanced safety features."
|
||||
},
|
||||
{
|
||||
"car_name": "Ford Mustang",
|
||||
"car_price": "$28,000",
|
||||
"car_url": "https://www.ford.com/mustang/",
|
||||
"car_image": "https://link-to-ford-mustang-image.com",
|
||||
"car_description": "Iconic sports car with powerful engine options."
|
||||
},
|
||||
{
|
||||
"car_name": "Tesla Model 3",
|
||||
"car_price": "$38,000",
|
||||
"car_url": "https://www.tesla.com/model3",
|
||||
"car_image": "https://link-to-tesla-model3-image.com",
|
||||
"car_description": "Electric vehicle with advanced technology and long range."
|
||||
},
|
||||
{
|
||||
"car_name": "Chevrolet Equinox",
|
||||
"car_price": "$24,000",
|
||||
"car_url": "https://www.chevrolet.com/equinox/",
|
||||
"car_image": "https://link-to-chevron-equinox-image.com",
|
||||
"car_description": "Compact SUV with a spacious interior and user-friendly technology."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json With Memories
|
||||
{
|
||||
"cars": [
|
||||
{
|
||||
"car_name": "Audi RS7",
|
||||
"car_price": "$118,500",
|
||||
"car_url": "https://www.audiusa.com/us/web/en/models/rs7/2023/overview.html",
|
||||
"car_image": "https://www.audiusa.com/content/dam/nemo/us/models/rs7/my23/gallery/1920x1080_AOZ_A717_191004.jpg",
|
||||
"car_description": "The Audi RS7 is a high-performance hatchback with a sleek design, powerful 591-hp twin-turbo V8, and luxurious interior. It's available in various colors including red."
|
||||
},
|
||||
{
|
||||
"car_name": "Porsche Panamera GTS",
|
||||
"car_price": "$129,300",
|
||||
"car_url": "https://www.porsche.com/usa/models/panamera/panamera-models/panamera-gts/",
|
||||
"car_image": "https://files.porsche.com/filestore/image/multimedia/noneporsche-panamera-gts-sample-m02-high/normal/8a6327c3-6c7f-4c6f-a9a8-fb9f58b21795;sP;twebp/porsche-normal.webp",
|
||||
"car_description": "The Porsche Panamera GTS is a luxury sports sedan with a 473-hp V8 engine, exquisite handling, and available in stunning red. Balances sportiness and comfort."
|
||||
},
|
||||
{
|
||||
"car_name": "BMW M5",
|
||||
"car_price": "$105,500",
|
||||
"car_url": "https://www.bmwusa.com/vehicles/m-models/m5/sedan/overview.html",
|
||||
"car_image": "https://www.bmwusa.com/content/dam/bmwusa/M/m5/2023/bmw-my23-m5-sapphire-black-twilight-purple-exterior-02.jpg",
|
||||
"car_description": "The BMW M5 is a powerhouse sedan with a 600-hp V8 engine, known for its great handling and luxury. It comes in several distinctive colors including maroon."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Resources
|
||||
|
||||
- [Mem0 Documentation](https://docs.mem0.ai)
|
||||
- [Mem0 Dashboard](https://app.mem0.ai/dashboard)
|
||||
- [API Reference](https://docs.mem0.ai/api-reference)
|
||||
- [OpenAI Documentation](https://platform.openai.com/docs)
|
||||
@@ -58,4 +58,16 @@ Explore how **Mem0** can power real-world applications and bring personalized, i
|
||||
<Card title="Personalized Research Agent" icon="robot" href="/examples/personalized-deep-research">
|
||||
Build a **Deep Research AI** that remembers your research goals and compiles insights from vast information sources.
|
||||
</Card>
|
||||
|
||||
<Card title="Mem0 as an Agentic Tool" icon="robot" href="/examples/mem0-agentic-tool">
|
||||
Integrate Mem0's memory capabilities with OpenAI's Agents SDK to create AI agents with persistent memory.
|
||||
</Card>
|
||||
|
||||
<Card title="OpenAI Inbuilt Tools" icon="robot" href="/examples/openai-inbuilt-tools">
|
||||
Use Mem0's memory capabilities with OpenAI's Inbuilt Tools to create AI agents with persistent memory.
|
||||
</Card>
|
||||
|
||||
<Card title="Mem0 OpenAI Voice Demo" icon="robot" href="/examples/mem0-openai-voice-demo">
|
||||
Use Mem0's memory capabilities with OpenAI's Inbuilt Tools to create AI agents with persistent memory.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: Contextual Add (ADD v2)
|
||||
icon: "square-plus"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
Mem0 now supports an contextual add version (v2). To use it, set `version="v2"` during the add call. The default version is v1, which is deprecated now. We recommend migrating to `v2` for new applications.
|
||||
|
||||
## Key Differences Between v1 and v2
|
||||
|
||||
### Version 1 (Legacy)
|
||||
In v1 (default), users needed to pass either the entire conversation history or past k messages with each new message to generate properly contextualized memories. This approach required:
|
||||
|
||||
- Manually tracking and sending previous messages using a sliding window approach
|
||||
- Increased payload sizes as conversations grew longer, requiring careful window size management
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
# First interaction
|
||||
messages1 = [
|
||||
{"role": "user", "content": "Hi, I'm Alex and I live in San Francisco."},
|
||||
{"role": "assistant", "content": "Hello Alex! Nice to meet you. San Francisco is a beautiful city."}
|
||||
]
|
||||
client.add(messages1, user_id="alex")
|
||||
|
||||
# Second interaction - must include previous messages for context
|
||||
messages2 = [
|
||||
{"role": "user", "content": "Hi, I'm Alex and I live in San Francisco."},
|
||||
{"role": "assistant", "content": "Hello Alex! Nice to meet you. San Francisco is a beautiful city."},
|
||||
{"role": "user", "content": "I like to eat sushi, and yesterday I went to Sunnyvale to eat sushi with my friends."},
|
||||
{"role": "assistant", "content": "Sushi is really a tasty choice. What did you do this weekend?"}
|
||||
]
|
||||
client.add(messages2, user_id="alex")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// First interaction
|
||||
const messages1 = [
|
||||
{"role": "user", "content": "Hi, I'm Alex and I live in San Francisco."},
|
||||
{"role": "assistant", "content": "Hello Alex! Nice to meet you. San Francisco is a beautiful city."}
|
||||
];
|
||||
client.add(messages1, { user_id: "alex" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// Second interaction - must include previous messages for context
|
||||
const messages2 = [
|
||||
{"role": "user", "content": "Hi, I'm Alex and I live in San Francisco."},
|
||||
{"role": "assistant", "content": "Hello Alex! Nice to meet you. San Francisco is a beautiful city."},
|
||||
{"role": "user", "content": "I like to eat sushi, and yesterday I went to Sunnyvale to eat sushi with my friends."},
|
||||
{"role": "assistant", "content": "Sushi is really a tasty choice. What did you do this weekend?"}
|
||||
];
|
||||
client.add(messages2, { user_id: "alex" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### Version 2 (Recommended)
|
||||
In v2, Mem0 automatically manages conversation context. Users only need to send new messages, and the system will:
|
||||
|
||||
- Automatically retrieve relevant conversation history
|
||||
- Generate properly contextualized memories
|
||||
- Reduce payload sizes and simplify integration
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
# First interaction
|
||||
messages1 = [
|
||||
{"role": "user", "content": "Hi, I'm Alex and I live in San Francisco."},
|
||||
{"role": "assistant", "content": "Hello Alex! Nice to meet you. San Francisco is a beautiful city."}
|
||||
]
|
||||
client.add(messages1, user_id="alex", version="v2")
|
||||
|
||||
# Second interaction - only need to send new messages
|
||||
messages2 = [
|
||||
{"role": "user", "content": "I like to eat sushi, and yesterday I went to Sunnyvale to eat sushi with my friends."},
|
||||
{"role": "assistant", "content": "Sushi is really a tasty choice. What did you do this weekend?"}
|
||||
]
|
||||
client.add(messages2, user_id="alex", version="v2")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// First interaction
|
||||
const messages1 = [
|
||||
{"role": "user", "content": "Hi, I'm Alex and I live in San Francisco."},
|
||||
{"role": "assistant", "content": "Hello Alex! Nice to meet you. San Francisco is a beautiful city."}
|
||||
];
|
||||
client.add(messages1, { user_id: "alex", version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// Second interaction - only need to send new messages
|
||||
const messages2 = [
|
||||
{"role": "user", "content": "I like to eat sushi, and yesterday I went to Sunnyvale to eat sushi with my friends."},
|
||||
{"role": "assistant", "content": "Sushi is really a tasty choice. What did you do this weekend?"}
|
||||
];
|
||||
client.add(messages2, { user_id: "alex", version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Benefits of Using v2
|
||||
|
||||
1. **Simplified Integration**: No need to track and manage conversation history
|
||||
2. **Reduced Payload Size**: Only send new messages, not the entire conversation
|
||||
3. **Improved Memory Quality**: Automatic context retrieval ensures better memory generation
|
||||
|
||||
## Understanding ID Parameters in v2
|
||||
|
||||
When using contextual add v2, you have different options for how to organize and retrieve memories:
|
||||
|
||||
### Using Only `user_id`
|
||||
|
||||
When you provide only a `user_id`:
|
||||
|
||||
- Memories are associated with this user's long-term memory store
|
||||
- The system will automatically retrieve relevant context from all of the user's previous conversations
|
||||
- These memories persist indefinitely across all of the user's sessions
|
||||
- Ideal for maintaining persistent user information (preferences, personal details, etc.)
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
# Adding to long-term user memory
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm allergic to peanuts and shellfish."},
|
||||
{"role": "assistant", "content": "I've noted your allergies to peanuts and shellfish."}
|
||||
]
|
||||
client.add(messages, user_id="alex", version="v2")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Adding to long-term user memory
|
||||
const messages = [
|
||||
{"role": "user", "content": "I'm allergic to peanuts and shellfish."},
|
||||
{"role": "assistant", "content": "I've noted your allergies to peanuts and shellfish."}
|
||||
];
|
||||
client.add(messages, { user_id: "alex", version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### Using `user_id` with `run_id`
|
||||
|
||||
When you provide both `user_id` and `run_id`:
|
||||
|
||||
- Memories are associated with a specific conversation session or interaction
|
||||
- The system will retrieve context primarily from this specific session
|
||||
- These memories are still tied to the user but are organized by the specific session
|
||||
- Ideal for maintaining context within a specific conversation flow or task
|
||||
- Helps prevent context from different conversations from interfering with each other
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
# Adding to a specific conversation session
|
||||
messages = [
|
||||
{"role": "user", "content": "For this trip to Paris, I want to focus on art museums."},
|
||||
{"role": "assistant", "content": "Great! I'll help you plan your Paris trip with a focus on art museums."}
|
||||
]
|
||||
client.add(messages, user_id="alex", run_id="paris-trip-2024", version="v2")
|
||||
|
||||
# Later in the same conversation session
|
||||
messages2 = [
|
||||
{"role": "user", "content": "I'd like to visit the Louvre on Monday."},
|
||||
{"role": "assistant", "content": "The Louvre is a great choice for Monday. Would you like information about opening hours?"}
|
||||
]
|
||||
client.add(messages2, user_id="alex", run_id="paris-trip-2024", version="v2")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Adding to a specific conversation session
|
||||
const messages = [
|
||||
{"role": "user", "content": "For this trip to Paris, I want to focus on art museums."},
|
||||
{"role": "assistant", "content": "Great! I'll help you plan your Paris trip with a focus on art museums."}
|
||||
];
|
||||
client.add(messages, { user_id: "alex", run_id: "paris-trip-2024", version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// Later in the same conversation session
|
||||
const messages2 = [
|
||||
{"role": "user", "content": "I'd like to visit the Louvre on Monday."},
|
||||
{"role": "assistant", "content": "The Louvre is a great choice for Monday. Would you like information about opening hours?"}
|
||||
];
|
||||
client.add(messages2, { user_id: "alex", run_id: "paris-trip-2024", version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Using `run_id` helps you organize memories into logical sessions or tasks, making it easier to maintain context for specific interactions while still associating everything with the user's overall profile.
|
||||
|
||||
If you have any questions, please feel free to reach out to us using one of the following methods:
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
+11
-11
@@ -1,25 +1,25 @@
|
||||
---
|
||||
title: Custom Prompts
|
||||
description: 'Enhance your product experience by adding custom prompts tailored to your needs'
|
||||
title: Custom Fact Extraction Prompt
|
||||
description: 'Enhance your product experience by adding custom fact extraction prompt tailored to your needs'
|
||||
icon: "pencil"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
## Introduction to Custom Prompts
|
||||
## Introduction to Custom Fact Extraction Prompt
|
||||
|
||||
Custom prompts allow you to tailor the behavior of your Mem0 instance to specific use cases or domains.
|
||||
By defining a custom prompt, you can control how information is extracted, processed, and stored in your memory system.
|
||||
Custom fact extraction prompt allow you to tailor the behavior of your Mem0 instance to specific use cases or domains.
|
||||
By defining it, you can control how information is extracted from the user's message.
|
||||
|
||||
To create an effective custom prompt:
|
||||
To create an effective custom fact extraction prompt:
|
||||
1. Be specific about the information to extract.
|
||||
2. Provide few-shot examples to guide the LLM.
|
||||
3. Ensure examples follow the format shown below.
|
||||
|
||||
Example of a custom prompt:
|
||||
Example of a custom fact extraction prompt:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
custom_prompt = """
|
||||
custom_fact_extraction_prompt = """
|
||||
Please only extract entities containing customer support information, order details, and user information.
|
||||
Here are some few shot examples:
|
||||
|
||||
@@ -67,7 +67,7 @@ Return the facts and customer information in a json format as shown above.
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Here we initialize the custom prompt in the config:
|
||||
Here we initialize the custom fact extraction prompt in the config:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
@@ -82,7 +82,7 @@ config = {
|
||||
"max_tokens": 2000,
|
||||
}
|
||||
},
|
||||
"custom_prompt": custom_prompt,
|
||||
"custom_fact_extraction_prompt": custom_fact_extraction_prompt,
|
||||
"version": "v1.1"
|
||||
}
|
||||
|
||||
@@ -166,4 +166,4 @@ await memory.add('I like going to hikes', { userId: "user123" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The custom prompt will process both the user and assistant messages to extract relevant information according to the defined format.
|
||||
The custom fact extraction prompt will process both the user and assistant messages to extract relevant information according to the defined format.
|
||||
@@ -0,0 +1,239 @@
|
||||
---
|
||||
title: Custom Update Memory Prompt
|
||||
icon: "pencil"
|
||||
iconType: "solid"
|
||||
---
|
||||
Update memory prompt is a prompt used to determine the action to be performed on the memory.
|
||||
By customizing this prompt, you can control how the memory is updated.
|
||||
|
||||
|
||||
## Introduction
|
||||
Mem0 memory system compares the newly retrieved facts with the existing memory and determines the action to be performed on the memory.
|
||||
The kinds of actions are:
|
||||
- Add
|
||||
- Add the newly retrieved facts to the memory.
|
||||
- Update
|
||||
- Update the existing memory with the newly retrieved facts.
|
||||
- Delete
|
||||
- Delete the existing memory.
|
||||
- No Change
|
||||
- Do not make any changes to the memory.
|
||||
|
||||
### Example
|
||||
Example of a custom update memory prompt:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
UPDATE_MEMORY_PROMPT = """You are a smart memory manager which controls the memory of a system.
|
||||
You can perform four operations: (1) add into the memory, (2) update the memory, (3) delete from the memory, and (4) no change.
|
||||
|
||||
Based on the above four operations, the memory will change.
|
||||
|
||||
Compare newly retrieved facts with the existing memory. For each new fact, decide whether to:
|
||||
- ADD: Add it to the memory as a new element
|
||||
- UPDATE: Update an existing memory element
|
||||
- DELETE: Delete an existing memory element
|
||||
- NONE: Make no change (if the fact is already present or irrelevant)
|
||||
|
||||
There are specific guidelines to select which operation to perform:
|
||||
|
||||
1. **Add**: If the retrieved facts contain new information not present in the memory, then you have to add it by generating a new ID in the id field.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "User is a software engineer"
|
||||
}
|
||||
]
|
||||
- Retrieved facts: ["Name is John"]
|
||||
- New Memory:
|
||||
{
|
||||
"memory" : [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "User is a software engineer",
|
||||
"event" : "NONE"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Name is John",
|
||||
"event" : "ADD"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
2. **Update**: If the retrieved facts contain information that is already present in the memory but the information is totally different, then you have to update it.
|
||||
If the retrieved fact contains information that conveys the same thing as the elements present in the memory, then you have to keep the fact which has the most information.
|
||||
Example (a) -- if the memory contains "User likes to play cricket" and the retrieved fact is "Loves to play cricket with friends", then update the memory with the retrieved facts.
|
||||
Example (b) -- if the memory contains "Likes cheese pizza" and the retrieved fact is "Loves cheese pizza", then you do not need to update it because they convey the same information.
|
||||
If the direction is to update the memory, then you have to update it.
|
||||
Please keep in mind while updating you have to keep the same ID.
|
||||
Please note to return the IDs in the output from the input IDs only and do not generate any new ID.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "I really like cheese pizza"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "User is a software engineer"
|
||||
},
|
||||
{
|
||||
"id" : "2",
|
||||
"text" : "User likes to play cricket"
|
||||
}
|
||||
]
|
||||
- Retrieved facts: ["Loves chicken pizza", "Loves to play cricket with friends"]
|
||||
- New Memory:
|
||||
{
|
||||
"memory" : [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Loves cheese and chicken pizza",
|
||||
"event" : "UPDATE",
|
||||
"old_memory" : "I really like cheese pizza"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "User is a software engineer",
|
||||
"event" : "NONE"
|
||||
},
|
||||
{
|
||||
"id" : "2",
|
||||
"text" : "Loves to play cricket with friends",
|
||||
"event" : "UPDATE",
|
||||
"old_memory" : "User likes to play cricket"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
3. **Delete**: If the retrieved facts contain information that contradicts the information present in the memory, then you have to delete it. Or if the direction is to delete the memory, then you have to delete it.
|
||||
Please note to return the IDs in the output from the input IDs only and do not generate any new ID.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Name is John"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza"
|
||||
}
|
||||
]
|
||||
- Retrieved facts: ["Dislikes cheese pizza"]
|
||||
- New Memory:
|
||||
{
|
||||
"memory" : [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Name is John",
|
||||
"event" : "NONE"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza",
|
||||
"event" : "DELETE"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
4. **No Change**: If the retrieved facts contain information that is already present in the memory, then you do not need to make any changes.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Name is John"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza"
|
||||
}
|
||||
]
|
||||
- Retrieved facts: ["Name is John"]
|
||||
- New Memory:
|
||||
{
|
||||
"memory" : [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Name is John",
|
||||
"event" : "NONE"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza",
|
||||
"event" : "NONE"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Output format
|
||||
The prompt needs to guide the output to follow the structure as shown below:
|
||||
<CodeGroup>
|
||||
```json Add
|
||||
{
|
||||
"memory": [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "This information is new",
|
||||
"event" : "ADD"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json Update
|
||||
{
|
||||
"memory": [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "This information replaces the old information",
|
||||
"event" : "UPDATE",
|
||||
"old_memory" : "Old information"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json Delete
|
||||
{
|
||||
"memory": [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "This information will be deleted",
|
||||
"event" : "DELETE"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json No Change
|
||||
{
|
||||
"memory": [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "No changes for this information",
|
||||
"event" : "NONE"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
## custom update memory prompt vs custom prompt
|
||||
|
||||
| Feature | `custom_update_memory_prompt` | `custom_prompt` |
|
||||
|---------|-------------------------------|-----------------|
|
||||
| Use case | Determine the action to be performed on the memory | Extract the facts from messages |
|
||||
| Reference | Retrieved facts from messages and old memory | Messages |
|
||||
| Output | Action to be performed on the memory | Extracted facts |
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Feedback Mechanism
|
||||
icon: "thumbs-up"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
Mem0's **Feedback Mechanism** allows you to provide feedback on the memories generated by your application. This feedback is used to improve the accuracy of the memories and the search results.
|
||||
|
||||
## How it works
|
||||
|
||||
The feedback mechanism is a simple API that allows you to provide feedback on the memories generated by your application. The feedback is stored in the database and is used to improve the accuracy of the memories and the search results. Over time, Mem0 continuously learns from this feedback, refining its memory generation and search capabilities for better performance.
|
||||
|
||||
## Give Feedback
|
||||
|
||||
You can give feedback on a memory by calling the `feedback` method on the Mem0 client.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
from mem0 import Mem0
|
||||
|
||||
client = Mem0(api_key="your_api_key")
|
||||
|
||||
client.feedback(memory_id="your-memory-id", feedback="NEGATIVE", feedback_reason="I don't like this memory because it is not relevant.")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
import MemoryClient from 'mem0ai';
|
||||
|
||||
const client = new MemoryClient({ apiKey: 'your-api-key'});
|
||||
|
||||
client.feedback({
|
||||
memory_id: "your-memory-id",
|
||||
feedback: "NEGATIVE",
|
||||
feedback_reason: "I don't like this memory because it is not relevant."
|
||||
})
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Feedback Types
|
||||
|
||||
The `feedback` parameter can be one of the following values:
|
||||
|
||||
- `POSITIVE`: The memory is useful.
|
||||
- `NEGATIVE`: The memory is not useful.
|
||||
- `VERY_NEGATIVE`: The memory is not useful at all.
|
||||
|
||||
## Parameters
|
||||
|
||||
The `feedback` method takes the following parameters:
|
||||
|
||||
- `memory_id`: The ID of the memory to give feedback on.
|
||||
- `feedback`: The feedback to give on the memory. (Optional)
|
||||
- `feedback_reason`: The reason for the feedback. (Optional)
|
||||
|
||||
The `feedback_reason` parameter is optional and can be used to provide a reason for the feedback.
|
||||
|
||||
<Note>
|
||||
You can pass `None` or `null` to the `feedback` and `feedback_reason` parameters to remove the feedback for a memory.
|
||||
</Note>
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
---
|
||||
title: Graph Memory
|
||||
icon: "circle-nodes"
|
||||
iconType: "solid"
|
||||
description: "Enable graph-based memory retrieval for more contextually relevant results"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Graph Memory enhances memory pipeline by creating relationships between entities in your data. It builds a network of interconnected information for more contextually relevant search results.
|
||||
|
||||
This feature allows your AI applications to understand connections between entities, providing richer context for responses. It's ideal for applications needing relationship tracking and nuanced information retrieval across related memories.
|
||||
|
||||
## How Graph Memory Works
|
||||
|
||||
The Graph Memory feature analyzes how each entity connects and relates to each other. When enabled:
|
||||
|
||||
1. Mem0 automatically builds a graph representation of entities
|
||||
2. Retrieval considers graph relationships between entities
|
||||
3. Results include entities that may be contextually important even if they're not direct semantic matches
|
||||
|
||||
## Using Graph Memory
|
||||
|
||||
To use Graph Memory, you need to enable it in your API calls by setting the `enable_graph=True` parameter. You'll also need to specify `output_format="v1.1"` to receive the enriched response format.
|
||||
|
||||
### Adding Memories with Graph Memory
|
||||
|
||||
When adding new memories, enable Graph Memory to automatically build relationships with existing memories:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(
|
||||
api_key="your-api-key",
|
||||
org_id="your-org-id",
|
||||
project_id="your-project-id"
|
||||
)
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "My name is Joseph"},
|
||||
{"role": "assistant", "content": "Hello Joseph, it's nice to meet you!"},
|
||||
{"role": "user", "content": "I'm from Seattle and I work as a software engineer"}
|
||||
]
|
||||
|
||||
# Enable graph memory when adding
|
||||
client.add(
|
||||
messages,
|
||||
user_id="joseph",
|
||||
version="v1",
|
||||
enable_graph=True,
|
||||
output_format="v1.1"
|
||||
)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
import { MemoryClient } from "mem0";
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: "your-api-key",
|
||||
orgId: "your-org-id",
|
||||
projectId: "your-project-id"
|
||||
});
|
||||
|
||||
const messages = [
|
||||
{ role: "user", content: "My name is Joseph" },
|
||||
{ role: "assistant", content: "Hello Joseph, it's nice to meet you!" },
|
||||
{ role: "user", content: "I'm from Seattle and I work as a software engineer" }
|
||||
];
|
||||
|
||||
// Enable graph memory when adding
|
||||
await client.add({
|
||||
messages,
|
||||
userId: "joseph",
|
||||
version: "v1",
|
||||
enableGraph: true,
|
||||
outputFormat: "v1.1"
|
||||
});
|
||||
```
|
||||
|
||||
```json Output
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"memory": "Name is Joseph",
|
||||
"event": "ADD",
|
||||
"id": "4a5a417a-fa10-43b5-8c53-a77c45e80438"
|
||||
},
|
||||
{
|
||||
"memory": "Is from Seattle",
|
||||
"event": "ADD",
|
||||
"id": "8d268d0f-5452-4714-b27d-ae46f676a49d"
|
||||
},
|
||||
{
|
||||
"memory": "Is a software engineer",
|
||||
"event": "ADD",
|
||||
"id": "5f0a184e-ddea-4fe6-9b92-692d6a901df8"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The graph memory would look like this:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/graph-platform.png" alt="Graph Memory Visualization showing relationships between entities" />
|
||||
</Frame>
|
||||
|
||||
<Caption>Graph Memory creates a network of relationships between entities, enabling more contextual retrieval</Caption>
|
||||
|
||||
|
||||
<Note>
|
||||
Response for the graph memory's `add` operation will not be available directly in the response.
|
||||
As adding graph memories is an asynchronous operation due to heavy processing,
|
||||
you can use the `get_all()` endpoint to retrieve the memory with the graph metadata.
|
||||
</Note>
|
||||
|
||||
|
||||
### Searching with Graph Memory
|
||||
|
||||
When searching memories, Graph Memory helps retrieve entities that are contextually important even if they're not direct semantic matches.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
# Search with graph memory enabled
|
||||
results = client.search(
|
||||
"what is my name?",
|
||||
user_id="joseph",
|
||||
enable_graph=True,
|
||||
output_format="v1.1"
|
||||
)
|
||||
|
||||
print(results)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Search with graph memory enabled
|
||||
const results = await client.search({
|
||||
query: "what is my name?",
|
||||
userId: "joseph",
|
||||
enableGraph: true,
|
||||
outputFormat: "v1.1"
|
||||
});
|
||||
|
||||
console.log(results);
|
||||
```
|
||||
|
||||
```json Output
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "4a5a417a-fa10-43b5-8c53-a77c45e80438",
|
||||
"memory": "Name is Joseph",
|
||||
"user_id": "joseph",
|
||||
"metadata": null,
|
||||
"categories": ["personal_details"],
|
||||
"immutable": false,
|
||||
"created_at": "2025-03-19T09:09:00.146390-07:00",
|
||||
"updated_at": "2025-03-19T09:09:00.146404-07:00",
|
||||
"score": 0.3621795393335552
|
||||
},
|
||||
{
|
||||
"id": "8d268d0f-5452-4714-b27d-ae46f676a49d",
|
||||
"memory": "Is from Seattle",
|
||||
"user_id": "joseph",
|
||||
"metadata": null,
|
||||
"categories": ["personal_details"],
|
||||
"immutable": false,
|
||||
"created_at": "2025-03-19T09:09:00.170680-07:00",
|
||||
"updated_at": "2025-03-19T09:09:00.170692-07:00",
|
||||
"score": 0.31212713194651254
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"source": "joseph",
|
||||
"source_type": "person",
|
||||
"relationship": "name",
|
||||
"target": "joseph",
|
||||
"target_type": "person",
|
||||
"score": 0.39
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### Retrieving All Memories with Graph Memory
|
||||
|
||||
When retrieving all memories, Graph Memory provides additional relationship context:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
# Get all memories with graph context
|
||||
memories = client.get_all(
|
||||
user_id="joseph",
|
||||
enable_graph=True,
|
||||
output_format="v1.1"
|
||||
)
|
||||
|
||||
print(memories)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Get all memories with graph context
|
||||
const memories = await client.getAll({
|
||||
userId: "joseph",
|
||||
enableGraph: true,
|
||||
outputFormat: "v1.1"
|
||||
});
|
||||
|
||||
console.log(memories);
|
||||
```
|
||||
|
||||
```json Output
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "5f0a184e-ddea-4fe6-9b92-692d6a901df8",
|
||||
"memory": "Is a software engineer",
|
||||
"user_id": "joseph",
|
||||
"metadata": null,
|
||||
"categories": ["professional_details"],
|
||||
"immutable": false,
|
||||
"created_at": "2025-03-19T09:09:00.194116-07:00",
|
||||
"updated_at": "2025-03-19T09:09:00.194128-07:00",
|
||||
},
|
||||
{
|
||||
"id": "8d268d0f-5452-4714-b27d-ae46f676a49d",
|
||||
"memory": "Is from Seattle",
|
||||
"user_id": "joseph",
|
||||
"metadata": null,
|
||||
"categories": ["personal_details"],
|
||||
"immutable": false,
|
||||
"created_at": "2025-03-19T09:09:00.170680-07:00",
|
||||
"updated_at": "2025-03-19T09:09:00.170692-07:00",
|
||||
},
|
||||
{
|
||||
"id": "4a5a417a-fa10-43b5-8c53-a77c45e80438",
|
||||
"memory": "Name is Joseph",
|
||||
"user_id": "joseph",
|
||||
"metadata": null,
|
||||
"categories": ["personal_details"],
|
||||
"immutable": false,
|
||||
"created_at": "2025-03-19T09:09:00.146390-07:00",
|
||||
"updated_at": "2025-03-19T09:09:00.146404-07:00",
|
||||
}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"source": "joseph",
|
||||
"source_type": "person",
|
||||
"relationship": "name",
|
||||
"target": "joseph",
|
||||
"target_type": "person"
|
||||
},
|
||||
{
|
||||
"source": "joseph",
|
||||
"source_type": "person",
|
||||
"relationship": "city",
|
||||
"target": "seattle",
|
||||
"target_type": "city"
|
||||
},
|
||||
{
|
||||
"source": "joseph",
|
||||
"source_type": "person",
|
||||
"relationship": "job",
|
||||
"target": "software engineer",
|
||||
"target_type": "job"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Enable Graph Memory for applications where understanding context and relationships between memories is important
|
||||
- Graph Memory works best with a rich history of related conversations
|
||||
- Consider Graph Memory for long-running assistants that need to track evolving information
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
Graph Memory requires additional processing and may increase response times slightly for very large memory stores. However, for most use cases, the improved retrieval quality outweighs the minimal performance impact.
|
||||
|
||||
If you have any questions, please feel free to reach out to us using one of the following methods:
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
|
||||
@@ -71,13 +71,32 @@ Here's an example schema for extracting professional profile information:
|
||||
|
||||
### Submit Export Job
|
||||
|
||||
You can optionally provide additional instructions to guide how memories are processed and structured during export using the `export_instructions` parameter.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
# Basic export request
|
||||
response = client.create_memory_export(
|
||||
schema=json_schema,
|
||||
user_id="user123"
|
||||
user_id="alice"
|
||||
)
|
||||
|
||||
# Export with custom instructions
|
||||
export_instructions = """
|
||||
1. Create a comprehensive profile with detailed information in each category
|
||||
2. Only mark fields as "None" when absolutely no relevant information exists
|
||||
3. Base all information directly on the user's memories
|
||||
4. When contradictions exist, prioritize the most recent information
|
||||
5. Clearly distinguish between factual statements and inferences
|
||||
"""
|
||||
|
||||
response = client.create_memory_export(
|
||||
schema=json_schema,
|
||||
user_id="alice",
|
||||
export_instructions=export_instructions
|
||||
)
|
||||
|
||||
print(response)
|
||||
```
|
||||
|
||||
@@ -87,7 +106,8 @@ curl -X POST "https://api.mem0.ai/v1/memories/export/" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"schema": {json_schema},
|
||||
"user_id": "user123"
|
||||
"user_id": "alice",
|
||||
"export_instructions": "1. Create a comprehensive profile with detailed information\n2. Only mark fields as \"None\" when absolutely no relevant information exists"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -107,12 +127,12 @@ Once the export job is complete, you can retrieve the structured data:
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
response = client.get_memory_export(user_id="user123")
|
||||
response = client.get_memory_export(user_id="alice")
|
||||
print(response)
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/export/?user_id=user123" \
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/export/?user_id=alice" \
|
||||
-H "Authorization: Token your-api-key"
|
||||
```
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ Learn about the key features and capabilities that make Mem0 a powerful platform
|
||||
<Card title="Advanced Retrieval" icon="magnifying-glass" href="/features/advanced-retrieval">
|
||||
Superior search results using state-of-the-art algorithms, including keyword search, reranking, and filtering capabilities.
|
||||
</Card>
|
||||
<Card title="Contextual Add" icon="square-plus" href="/features/contextual-add">
|
||||
Only send your latest conversation history - we automatically retrieve the rest and generate properly contextualized memories.
|
||||
</Card>
|
||||
<Card title="Multimodal Support" icon="photo-film" href="/features/multimodal-support">
|
||||
Process and analyze various types of content including images.
|
||||
</Card>
|
||||
@@ -33,6 +36,9 @@ Learn about the key features and capabilities that make Mem0 a powerful platform
|
||||
<Card title="Memory Export" icon="file-export" href="/features/memory-export">
|
||||
Export memories in structured formats using customizable Pydantic schemas.
|
||||
</Card>
|
||||
<Card title="Graph Memory" icon="graph" href="/features/graph-memory">
|
||||
Add memories in the form of nodes and edges in a graph database and search for related memories.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Getting Help
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -95,7 +95,7 @@ add_input = {
|
||||
{"role": "user", "content": "Hi, I'm Alex. I'm a vegetarian and I'm allergic to nuts."},
|
||||
{"role": "assistant", "content": "Hello Alex! I've noted that you're a vegetarian and have a nut allergy."}
|
||||
],
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"output_format": "v1.1",
|
||||
"metadata": {"food": "vegan"}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ search_input = {
|
||||
"filters": {
|
||||
"AND": [
|
||||
{"created_at": {"gte": "2024-07-20", "lte": "2024-12-10"}},
|
||||
{"user_id": "alex123"}
|
||||
{"user_id": "alex"}
|
||||
]
|
||||
},
|
||||
"version": "v2"
|
||||
@@ -186,7 +186,7 @@ result = search_tool.invoke(search_input)
|
||||
{
|
||||
"id": "1a75e827-7eca-45ea-8c5c-cfd43299f061",
|
||||
"memory": "Name is Alex",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "d0fccc8fa47f7a149ee95750c37bb0ca",
|
||||
"metadata": {
|
||||
"food": "vegan"
|
||||
@@ -255,7 +255,7 @@ get_all_input = {
|
||||
"version": "v2",
|
||||
"filters": {
|
||||
"AND": [
|
||||
{"user_id": "alex123"},
|
||||
{"user_id": "alex"},
|
||||
{"created_at": {"gte": "2024-07-01", "lte": "2024-12-31"}}
|
||||
]
|
||||
},
|
||||
@@ -274,7 +274,7 @@ get_all_result = get_all_tool.invoke(get_all_input)
|
||||
{
|
||||
"id": "1a75e827-7eca-45ea-8c5c-cfd43299f061",
|
||||
"memory": "Name is Alex",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "d0fccc8fa47f7a149ee95750c37bb0ca",
|
||||
"metadata": {
|
||||
"food": "vegan"
|
||||
@@ -288,7 +288,7 @@ get_all_result = get_all_tool.invoke(get_all_input)
|
||||
{
|
||||
"id": "91509588-0b39-408a-8df3-84b3bce8c521",
|
||||
"memory": "Is a vegetarian",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "ce6b1c84586772ab9995a9477032df99",
|
||||
"metadata": {
|
||||
"food": "vegan"
|
||||
@@ -303,7 +303,7 @@ get_all_result = get_all_tool.invoke(get_all_input)
|
||||
{
|
||||
"id": "8d74f7a0-6107-4589-bd6f-210f6bf4fbbb",
|
||||
"memory": "Is allergic to nuts",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "7873cd0e5a29c513253d9fad038e758b",
|
||||
"metadata": {
|
||||
"food": "vegan"
|
||||
|
||||
@@ -22,7 +22,11 @@ pip install mem0ai
|
||||
<Tabs>
|
||||
<Tab title="Basic">
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "your-api-key"
|
||||
|
||||
m = Memory()
|
||||
```
|
||||
</Tab>
|
||||
@@ -42,8 +46,11 @@ docker run -p 6333:6333 -p 6334:6334 \
|
||||
Then, instantiate memory with qdrant server:
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "your-api-key"
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "qdrant",
|
||||
@@ -61,8 +68,11 @@ m = Memory.from_config(config)
|
||||
<Tab title="Advanced (Graph Memory)">
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "your-api-key"
|
||||
|
||||
config = {
|
||||
"graph_store": {
|
||||
"provider": "neo4j",
|
||||
@@ -85,14 +95,18 @@ m = Memory.from_config(config_dict=config)
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
const messages = [
|
||||
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."}
|
||||
]
|
||||
|
||||
# Store inferred memories (default behavior)
|
||||
result = m.add(messages, user_id="alice", metadata={"category": "movie_recommendations"})
|
||||
|
||||
# Store raw messages without inference
|
||||
# result = m.add(messages, user_id="alice", metadata={"category": "movie_recommendations"}, infer=False)
|
||||
```
|
||||
|
||||
```json Output
|
||||
@@ -368,7 +382,8 @@ Mem0 offers extensive configuration options to customize its behavior according
|
||||
|------------------|--------------------------------------|----------------------------|
|
||||
| `history_db_path` | Path to the history database | "{mem0_dir}/history.db" |
|
||||
| `version` | API version | "v1.1" |
|
||||
| `custom_prompt` | Custom prompt for memory processing | None |
|
||||
| `custom_fact_extraction_prompt` | Custom prompt for memory processing | None |
|
||||
| `custom_update_memory_prompt` | Custom prompt for update memory | None |
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Complete Configuration Example">
|
||||
@@ -405,7 +420,8 @@ config = {
|
||||
},
|
||||
"history_db_path": "/path/to/history.db",
|
||||
"version": "v1.1",
|
||||
"custom_prompt": "Optional custom prompt for memory processing"
|
||||
"custom_fact_extraction_prompt": "Optional custom prompt for fact extraction for memory",
|
||||
"custom_update_memory_prompt": "Optional custom prompt for update memory"
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
+47
-7
@@ -783,6 +783,13 @@
|
||||
"title": "Immutable",
|
||||
"default": false
|
||||
},
|
||||
"expiration_date": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the memory will expire. The default expiration date is 30 days from the date of creation. Format: YYYY-MM-DD",
|
||||
"title": "Expiration date",
|
||||
"nullable": true
|
||||
},
|
||||
"organization": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -932,27 +939,27 @@
|
||||
"x-code-samples": [
|
||||
{
|
||||
"lang": "Python",
|
||||
"source": "# To use the Python SDK, install the package:\n# pip install mem0ai\n\nfrom mem0 import MemoryClient\n\nclient = MemoryClient(api_key=\"your_api_key\", org_id=\"your_org_id\", project_id=\"your_project_id\")\n\nmessages = [\n {\"role\": \"user\", \"content\": \"<user-message>\"},\n {\"role\": \"assistant\", \"content\": \"<assistant-response>\"}\n]\n\nclient.add(messages, user_id=\"<user-id>\")"
|
||||
"source": "# To use the Python SDK, install the package:\n# pip install mem0ai\n\nfrom mem0 import MemoryClient\n\nclient = MemoryClient(api_key=\"your_api_key\", org_id=\"your_org_id\", project_id=\"your_project_id\")\n\nmessages = [\n {\"role\": \"user\", \"content\": \"<user-message>\"},\n {\"role\": \"assistant\", \"content\": \"<assistant-response>\"}\n]\n\nclient.add(messages, user_id=\"<user-id>\", version=\"v2\")"
|
||||
},
|
||||
{
|
||||
"lang": "JavaScript",
|
||||
"source": "// To use the JavaScript SDK, install the package:\n// npm i mem0ai\n\nimport MemoryClient from 'mem0ai';\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\nconst messages = [\n { role: \"user\", content: \"Hi, I'm Alex. I'm a vegetarian and I'm allergic to nuts.\" },\n { role: \"assistant\", content: \"Hello Alex! I've noted that you're a vegetarian and have a nut allergy. I'll keep this in mind for any food-related recommendations or discussions.\" }\n];\n\nclient.add(messages, { user_id: \"<user_id>\" })\n .then(result => console.log(result))\n .catch(error => console.error(error));"
|
||||
"source": "// To use the JavaScript SDK, install the package:\n// npm i mem0ai\n\nimport MemoryClient from 'mem0ai';\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\nconst messages = [\n { role: \"user\", content: \"Hi, I'm Alex. I'm a vegetarian and I'm allergic to nuts.\" },\n { role: \"assistant\", content: \"Hello Alex! I've noted that you're a vegetarian and have a nut allergy. I'll keep this in mind for any food-related recommendations or discussions.\" }\n];\n\nclient.add(messages, { user_id: \"<user_id>\", version: \"v2\" })\n .then(result => console.log(result))\n .catch(error => console.error(error));"
|
||||
},
|
||||
{
|
||||
"lang": "cURL",
|
||||
"source": "curl --request POST \\\n --url https://api.mem0.ai/v1/memories/ \\\n --header 'Authorization: Token <api-key>' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"messages\": [\n {}\n ],\n \"agent_id\": \"<string>\",\n \"user_id\": \"<string>\",\n \"app_id\": \"<string>\",\n \"run_id\": \"<string>\",\n \"metadata\": {},\n \"includes\": \"<string>\",\n \"excludes\": \"<string>\",\n \"infer\": true,\n \"custom_categories\": {}, \n \"org_id\": \"<string>\",\n \"project_id\": \"<string>\"\n}'"
|
||||
"source": "curl --request POST \\\n --url https://api.mem0.ai/v1/memories/ \\\n --header 'Authorization: Token <api-key>' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"messages\": [\n {}\n ],\n \"agent_id\": \"<string>\",\n \"user_id\": \"<string>\",\n \"app_id\": \"<string>\",\n \"run_id\": \"<string>\",\n \"metadata\": {},\n \"includes\": \"<string>\",\n \"excludes\": \"<string>\",\n \"infer\": true,\n \"custom_categories\": {}, \n \"org_id\": \"<string>\",\n \"project_id\": \"<string>\",\n \"version\": \"v2\"\n}'"
|
||||
},
|
||||
{
|
||||
"lang": "Go",
|
||||
"source": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"net/http\"\n\t\"io/ioutil\"\n)\n\nfunc main() {\n\n\turl := \"https://api.mem0.ai/v1/memories/\"\n\n\tpayload := strings.NewReader(\"{\n \\\"messages\\\": [\n {}\n ],\n \\\"agent_id\\\": \\\"<string>\\\",\n \\\"user_id\\\": \\\"<string>\\\",\n \\\"app_id\\\": \\\"<string>\\\",\n \\\"run_id\\\": \\\"<string>\\\",\n \\\"metadata\\\": {},\n \\\"includes\\\": \\\"<string>\\\",\n \\\"excludes\\\": \\\"<string>\\\",\n \\\"infer\\\": true,\n \\\"custom_categories\\\": {},\n \\\"org_id\\\": \\\"<string>\\\",\n \\\"project_id\\\": \\\"<string>\"\n}\")\n\n\treq, _ := http.NewRequest(\"POST\", url, payload)\n\n\treq.Header.Add(\"Authorization\", \"Token <api-key>\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\tres, _ := http.DefaultClient.Do(req)\n\n\tdefer res.Body.Close()\n\tbody, _ := ioutil.ReadAll(res.Body)\n\n\tfmt.Println(res)\n\tfmt.Println(string(body))\n\n}"
|
||||
"source": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"net/http\"\n\t\"io/ioutil\"\n)\n\nfunc main() {\n\n\turl := \"https://api.mem0.ai/v1/memories/\"\n\n\tpayload := strings.NewReader(\"{\n \\\"messages\\\": [\n {}\n ],\n \\\"agent_id\\\": \\\"<string>\\\",\n \\\"user_id\\\": \\\"<string>\\\",\n \\\"app_id\\\": \\\"<string>\\\",\n \\\"run_id\\\": \\\"<string>\\\",\n \\\"metadata\\\": {},\n \\\"includes\\\": \\\"<string>\\\",\n \\\"excludes\\\": \\\"<string>\\\",\n \\\"infer\\\": true,\n \\\"custom_categories\\\": {},\n \\\"org_id\\\": \\\"<string>\\\",\n \\\"project_id\\\": \\\"<string>\",\n \\\"version\\\": \"v2\"\n}\")\n\n\treq, _ := http.NewRequest(\"POST\", url, payload)\n\n\treq.Header.Add(\"Authorization\", \"Token <api-key>\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\n\tres, _ := http.DefaultClient.Do(req)\n\n\tdefer res.Body.Close()\n\tbody, _ := ioutil.ReadAll(res.Body)\n\n\tfmt.Println(res)\n\tfmt.Println(string(body))\n\n}"
|
||||
},
|
||||
{
|
||||
"lang": "PHP",
|
||||
"source": "<?php\n\n$curl = curl_init();\n\ncurl_setopt_array($curl, [\n CURLOPT_URL => \"https://api.mem0.ai/v1/memories/\",\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_ENCODING => \"\",\n CURLOPT_MAXREDIRS => 10,\n CURLOPT_TIMEOUT => 30,\n CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\n CURLOPT_CUSTOMREQUEST => \"POST\",\n CURLOPT_POSTFIELDS => \"{\n \\\"messages\\\": [\n {}\n ],\n \\\"agent_id\\\": \\\"<string>\\\",\n \\\"user_id\\\": \\\"<string>\\\",\n \\\"app_id\\\": \\\"<string>\\\",\n \\\"run_id\\\": \\\"<string>\\\",\n \\\"metadata\\\": {},\n \\\"includes\\\": \\\"<string>\\\",\n \\\"excludes\\\": \\\"<string>\\\",\n \\\"infer\\\": true,\n \\\"custom_categories\\\": {}, \n \\\"org_id\\\": \\\"<string>\\\",\n \\\"project_id\\\": \\\"<string>\"\n}\",\n CURLOPT_HTTPHEADER => [\n \"Authorization: Token <api-key>\",\n \"Content-Type: application/json\"\n ],\n]);\n\n$response = curl_exec($curl);\n$err = curl_error($curl);\n\ncurl_close($curl);\n\nif ($err) {\n echo \"cURL Error #:\" . $err;\n} else {\n echo $response;\n}"
|
||||
"source": "<?php\n\n$curl = curl_init();\n\ncurl_setopt_array($curl, [\n CURLOPT_URL => \"https://api.mem0.ai/v1/memories/\",\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_ENCODING => \"\",\n CURLOPT_MAXREDIRS => 10,\n CURLOPT_TIMEOUT => 30,\n CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\n CURLOPT_CUSTOMREQUEST => \"POST\",\n CURLOPT_POSTFIELDS => \"{\n \\\"messages\\\": [\n {}\n ],\n \\\"agent_id\\\": \\\"<string>\\\",\n \\\"user_id\\\": \\\"<string>\\\",\n \\\"app_id\\\": \\\"<string>\\\",\n \\\"run_id\\\": \\\"<string>\\\",\n \\\"metadata\\\": {},\n \\\"includes\\\": \\\"<string>\\\",\n \\\"excludes\\\": \\\"<string>\\\",\n \\\"infer\\\": true,\n \\\"custom_categories\\\": {}, \n \\\"org_id\\\": \\\"<string>\\\",\n \\\"project_id\\\": \\\"<string>\",\n \\\"version\\\": \"v2\"\n}\",\n CURLOPT_HTTPHEADER => [\n \"Authorization: Token <api-key>\",\n \"Content-Type: application/json\"\n ],\n]);\n\n$response = curl_exec($curl);\n$err = curl_error($curl);\n\ncurl_close($curl);\n\nif ($err) {\n echo \"cURL Error #:\" . $err;\n} else {\n echo $response;\n}"
|
||||
},
|
||||
{
|
||||
"lang": "Java",
|
||||
"source": "HttpResponse<String> response = Unirest.post(\"https://api.mem0.ai/v1/memories/\")\n .header(\"Authorization\", \"Token <api-key>\")\n .header(\"Content-Type\", \"application/json\")\n .body(\"{\n \\\"messages\\\": [\n {}\n ],\n \\\"agent_id\\\": \\\"<string>\\\",\n \\\"user_id\\\": \\\"<string>\\\",\n \\\"app_id\\\": \\\"<string>\\\",\n \\\"run_id\\\": \\\"<string>\\\",\n \\\"metadata\\\": {},\n \\\"includes\\\": \\\"<string>\\\",\n \\\"excludes\\\": \\\"<string>\\\",\n \\\"infer\\\": true,\n \\\"custom_categories\\\": {}, \n \\\"org_id\\\": \\\"<string>\\\",\n \\\"project_id\\\": \\\"<string>\"\n}\")\n .asString();"
|
||||
"source": "HttpResponse<String> response = Unirest.post(\"https://api.mem0.ai/v1/memories/\")\n .header(\"Authorization\", \"Token <api-key>\")\n .header(\"Content-Type\", \"application/json\")\n .body(\"{\n \\\"messages\\\": [\n {}\n ],\n \\\"agent_id\\\": \\\"<string>\\\",\n \\\"user_id\\\": \\\"<string>\\\",\n \\\"app_id\\\": \\\"<string>\\\",\n \\\"run_id\\\": \\\"<string>\\\",\n \\\"metadata\\\": {},\n \\\"includes\\\": \\\"<string>\\\",\n \\\"excludes\\\": \\\"<string>\\\",\n \\\"infer\\\": true,\n \\\"custom_categories\\\": {}, \n \\\"org_id\\\": \\\"<string>\\\",\n \\\"project_id\\\": \\\"<string>\",\n \\\"version\\\": \"v2\"\n}\")\n .asString();"
|
||||
}
|
||||
],
|
||||
"x-codegen-request-body-name": "data"
|
||||
@@ -1183,6 +1190,13 @@
|
||||
"title": "Immutable",
|
||||
"default": false
|
||||
},
|
||||
"expiration_date": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the memory will expire. The default expiration date is 30 days from the date of creation. Format: YYYY-MM-DD",
|
||||
"title": "Expiration date",
|
||||
"nullable": true
|
||||
},
|
||||
"organization": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1324,6 +1338,13 @@
|
||||
"title": "Immutable",
|
||||
"default": false
|
||||
},
|
||||
"expiration_date": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the memory will expire. The default expiration date is 30 days from the date of creation. Format: YYYY-MM-DD",
|
||||
"title": "Expiration date",
|
||||
"nullable": true
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
@@ -1451,6 +1472,13 @@
|
||||
"title": "Immutable",
|
||||
"default": false
|
||||
},
|
||||
"expiration_date": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the memory will expire. The default expiration date is 30 days from the date of creation. Format: YYYY-MM-DD",
|
||||
"title": "Expiration date",
|
||||
"nullable": true
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
@@ -1761,7 +1789,7 @@
|
||||
},
|
||||
{
|
||||
"lang": "JavaScript",
|
||||
"source": "const options = {\n method: 'PUT',\n headers: {\n 'Authorization': 'Token <api-key>',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n text: 'Your updated memory text here'\n })\n};\n\nfetch('https://api.mem0.ai/v1/memories/{memory_id}/', options)\n .then(response => response.json())\n .then(response => console.log(response))\n .catch(err => console.error(err));"
|
||||
"source": "// To use the JavaScript SDK, install the package:\n// npm i mem0ai\n\nimport MemoryClient from 'mem0ai';\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\n// Update a specific memory\nconst memory_id=<memory_id>\nconst message=\"Your updated memory message here\"\nclient.update(memory_id, message)\n .then(result => console.log(result))\n .catch(error => console.error(error));"
|
||||
},
|
||||
{
|
||||
"lang": "cURL",
|
||||
@@ -4798,6 +4826,12 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"expiration_date": {
|
||||
"description": "The date and time when the memory will expire. The default expiration date is 30 days from the date of creation. Format: YYYY-MM-DD",
|
||||
"title": "Expiration date",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"org_id": {
|
||||
"description": "The unique identifier of the organization associated with this memory.",
|
||||
"title": "Organization id",
|
||||
@@ -4809,6 +4843,12 @@
|
||||
"title": "Project id",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"version": {
|
||||
"description": "The version of the memory to use. The default version is v1, which is deprecated. We recommend using v2 for new applications.",
|
||||
"title": "Version",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+167
-76
@@ -87,10 +87,10 @@ messages = [
|
||||
]
|
||||
|
||||
# The default output_format is v1.0
|
||||
client.add(messages, user_id="alex", output_format="v1.0")
|
||||
client.add(messages, user_id="alex", output_format="v1.0", version="v2")
|
||||
|
||||
# To use the latest output_format, set the output_format parameter to "v1.1"
|
||||
client.add(messages, user_id="alex", output_format="v1.1", metadata={"food": "vegan"})
|
||||
client.add(messages, user_id="alex", output_format="v1.1", metadata={"food": "vegan"}, version="v2")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
@@ -98,7 +98,7 @@ const messages = [
|
||||
{"role": "user", "content": "Hi, I'm Alex. I'm a vegetarian and I'm allergic to nuts."},
|
||||
{"role": "assistant", "content": "Hello Alex! I've noted that you're a vegetarian and have a nut allergy. I'll keep this in mind for any food-related recommendations or discussions."}
|
||||
];
|
||||
client.add(messages, { user_id: "alex", output_format: "v1.1", metadata: { food: "vegan" } })
|
||||
client.add(messages, { user_id: "alex", output_format: "v1.1", metadata: { food: "vegan" }, version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
@@ -116,7 +116,8 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \
|
||||
"output_format": "v1.1",
|
||||
"metadata": {
|
||||
"food": "vegan"
|
||||
}
|
||||
},
|
||||
"version": "v2"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -191,10 +192,10 @@ messages = [
|
||||
]
|
||||
|
||||
# The default output_format is v1.0
|
||||
client.add(messages, user_id="alex123", run_id="trip-planning-2024", output_format="v1.0")
|
||||
client.add(messages, user_id="alex", run_id="trip-planning-2024", output_format="v1.0", version="v2")
|
||||
|
||||
# To use the latest output_format, set the output_format parameter to "v1.1"
|
||||
client.add(messages, user_id="alex123", run_id="trip-planning-2024", output_format="v1.1")
|
||||
client.add(messages, user_id="alex", run_id="trip-planning-2024", output_format="v1.1", version="v2")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
@@ -204,7 +205,7 @@ const messages = [
|
||||
{"role": "user", "content": "Yes, please! Especially in Tokyo."},
|
||||
{"role": "assistant", "content": "Great! I'll remember that you're interested in vegetarian restaurants in Tokyo for your upcoming trip. I'll prepare a list for you in our next interaction."}
|
||||
];
|
||||
client.add(messages, { user_id: "alex123", run_id: "trip-planning-2024", output_format: "v1.1" })
|
||||
client.add(messages, { user_id: "alex", run_id: "trip-planning-2024", output_format: "v1.1", version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
@@ -220,9 +221,10 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \
|
||||
{"role": "user", "content": "Yes, please! Especially in Tokyo."},
|
||||
{"role": "assistant", "content": "Great! I'll remember that you're interested in vegetarian restaurants in Tokyo for your upcoming trip. I'll prepare a list for you in our next interaction."}
|
||||
],
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"run_id": "trip-planning-2024",
|
||||
"output_format": "v1.1"
|
||||
"output_format": "v1.1",
|
||||
"version": "v2"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -274,10 +276,10 @@ messages = [
|
||||
]
|
||||
|
||||
# The default output_format is v1.0
|
||||
client.add(messages, agent_id="ai-tutor", output_format="v1.0")
|
||||
client.add(messages, agent_id="ai-tutor", output_format="v1.0", version="v2")
|
||||
|
||||
# To use the latest output_format, set the output_format parameter to "v1.1"
|
||||
client.add(messages, agent_id="ai-tutor", output_format="v1.1")
|
||||
client.add(messages, agent_id="ai-tutor", output_format="v1.1", version="v2")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
@@ -285,7 +287,7 @@ const messages = [
|
||||
{"role": "system", "content": "You are an AI tutor with a personality. Give yourself a name for the user."},
|
||||
{"role": "assistant", "content": "Understood. I'm an AI tutor with a personality. My name is Alice."}
|
||||
];
|
||||
client.add(messages, { agent_id: "ai-tutor", output_format: "v1.1" })
|
||||
client.add(messages, { agent_id: "ai-tutor", output_format: "v1.1", version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
@@ -300,7 +302,8 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \
|
||||
{"role": "assistant", "content": "Understood. I'm an AI tutor with a personality. My name is Alice."}
|
||||
],
|
||||
"agent_id": "ai-tutor",
|
||||
"output_format": "v1.1"
|
||||
"output_format": "v1.1",
|
||||
"version": "v2"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -368,7 +371,7 @@ messages = [
|
||||
{"role": "assistant", "content": "That's great! I'm going to Dubai next month."},
|
||||
]
|
||||
|
||||
client.add(messages=messages, user_id="user1", agent_id="agent1")
|
||||
client.add(messages=messages, user_id="user1", agent_id="agent1", version="v2")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
@@ -377,7 +380,7 @@ const messages = [
|
||||
{"role": "assistant", "content": "That's great! I'm going to Dubai next month."},
|
||||
]
|
||||
|
||||
client.add(messages, { user_id: "user1", agent_id: "agent1" })
|
||||
client.add(messages, { user_id: "user1", agent_id: "agent1", version: "v2" })
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
@@ -392,7 +395,8 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \
|
||||
{"role": "assistant", "content": "That's great! I'm going to Dubai next month."},
|
||||
],
|
||||
"user_id": "user1",
|
||||
"agent_id": "agent1"
|
||||
"agent_id": "agent1",
|
||||
"version": "v2"
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -412,6 +416,89 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
#### Setting Memory Expiration Date
|
||||
|
||||
You can set an expiration date for memories, after which they will no longer be retrieved in searches. This is useful for creating temporary memories or memories that are only relevant for a specific time period.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
import datetime
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I'll be in San Francisco until August 31st."
|
||||
}
|
||||
]
|
||||
|
||||
# Set an expiration date for this memory
|
||||
client.add(messages=messages, user_id="alex", expiration_date=str(datetime.datetime.now().date() + datetime.timedelta(days=30)))
|
||||
|
||||
# You can also use an explicit date string
|
||||
client.add(messages=messages, user_id="alex", expiration_date="2023-08-31")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
const messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I'll be in San Francisco until August 31st."
|
||||
}
|
||||
];
|
||||
|
||||
// Set an expiration date 30 days from now
|
||||
const expirationDate = new Date();
|
||||
expirationDate.setDate(expirationDate.getDate() + 30);
|
||||
client.add(messages, {
|
||||
user_id: "alex",
|
||||
expiration_date: expirationDate.toISOString().split('T')[0]
|
||||
})
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// You can also use an explicit date string
|
||||
client.add(messages, {
|
||||
user_id: "alex",
|
||||
expiration_date: "2023-08-31"
|
||||
})
|
||||
.then(response => console.log(response))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "https://api.mem0.ai/v1/memories/" \
|
||||
-H "Authorization: Token your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I'll be in San Francisco until August 31st."
|
||||
}
|
||||
],
|
||||
"user_id": "alex",
|
||||
"expiration_date": "2023-08-31"
|
||||
}'
|
||||
```
|
||||
|
||||
```json Output
|
||||
[
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-4g7h-8i9j-k0l1m2n3o4p5",
|
||||
"data": {
|
||||
"memory": "In San Francisco until August 31st"
|
||||
},
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
Once a memory reaches its expiration date, it won't be included in search or get results.
|
||||
</Note>
|
||||
|
||||
#### Monitor Memories
|
||||
|
||||
@@ -466,6 +553,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/" \
|
||||
"metadata": {"food": "vegan"},
|
||||
"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"
|
||||
}
|
||||
@@ -482,6 +570,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/" \
|
||||
"metadata": {"food": "vegan"},
|
||||
"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"
|
||||
}
|
||||
@@ -527,6 +616,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
||||
"metadata": {"food": "vegan"},
|
||||
"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"
|
||||
}
|
||||
@@ -617,6 +707,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
||||
"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"
|
||||
}
|
||||
@@ -682,6 +773,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
||||
"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"
|
||||
}
|
||||
@@ -756,6 +848,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
||||
"metadata": {"food": "vegan"},
|
||||
"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"
|
||||
}
|
||||
@@ -856,6 +949,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&page=1&page_size=50"
|
||||
"hash":"62bc074f56d1f909f1b4c2b639f56f6a",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-25T23:57:00.108347-07:00",
|
||||
"updated_at":"2024-07-25T23:57:00.108367-07:00",
|
||||
"categories":None
|
||||
@@ -867,6 +961,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&page=1&page_size=50"
|
||||
"hash":"35a305373d639b0bffc6c2a3e2eb4244",
|
||||
"metadata":"None",
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-26T00:31:03.543759-07:00",
|
||||
"updated_at":"2024-07-26T00:31:03.543778-07:00",
|
||||
"categories":None
|
||||
@@ -890,6 +985,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&page=1&page_size=50"
|
||||
"hash":"62bc074f56d1f909f1b4c2b639f56f6a",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-25T23:57:00.108347-07:00",
|
||||
"updated_at":"2024-07-25T23:57:00.108367-07:00",
|
||||
"categories":None
|
||||
@@ -901,6 +997,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&page=1&page_size=50"
|
||||
"hash":"35a305373d639b0bffc6c2a3e2eb4244",
|
||||
"metadata":"None",
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-26T00:31:03.543759-07:00",
|
||||
"updated_at":"2024-07-26T00:31:03.543778-07:00",
|
||||
"categories":None
|
||||
@@ -945,6 +1042,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?agent_id=ai-tutor&page=1&page_size
|
||||
"hash":"62bc074f56d1f909f1b4c2b639f56f6a",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-25T23:57:00.108347-07:00",
|
||||
"updated_at":"2024-07-25T23:57:00.108367-07:00",
|
||||
"categories":None
|
||||
@@ -956,6 +1054,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?agent_id=ai-tutor&page=1&page_size
|
||||
"hash":"35a305373d639b0bffc6c2a3e2eb4244",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-26T00:31:03.543759-07:00",
|
||||
"updated_at":"2024-07-26T00:31:03.543778-07:00",
|
||||
"categories":None
|
||||
@@ -979,6 +1078,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?agent_id=ai-tutor&page=1&page_size
|
||||
"hash": "62bc074f56d1f909f1b4c2b639f56f6a",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-25T23:57:00.108347-07:00",
|
||||
"updated_at": "2024-07-25T23:57:00.108367-07:00"
|
||||
},
|
||||
@@ -989,6 +1089,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?agent_id=ai-tutor&page=1&page_size
|
||||
"hash": "35a305373d639b0bffc6c2a3e2eb4244",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-26T00:31:03.543759-07:00",
|
||||
"updated_at": "2024-07-26T00:31:03.543778-07:00"
|
||||
}
|
||||
@@ -1004,17 +1105,17 @@ curl -X GET "https://api.mem0.ai/v1/memories/?agent_id=ai-tutor&page=1&page_size
|
||||
<CodeGroup>
|
||||
|
||||
```python Python
|
||||
short_term_memories = client.get_all(user_id="alex123", run_id="trip-planning-2024", page=1, page_size=50)
|
||||
short_term_memories = client.get_all(user_id="alex", run_id="trip-planning-2024", page=1, page_size=50)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
client.getAll({ user_id: "alex123", run_id: "trip-planning-2024", page: 1, page_size: 50 })
|
||||
client.getAll({ user_id: "alex", run_id: "trip-planning-2024", page: 1, page_size: 50 })
|
||||
.then(memories => console.log(memories))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&run_id=trip-planning-2024&page=1&page_size=50" \
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&run_id=trip-planning-2024&page=1&page_size=50" \
|
||||
-H "Authorization: Token your-api-key"
|
||||
```
|
||||
|
||||
@@ -1027,10 +1128,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&run_id=trip-planni
|
||||
{
|
||||
"id":"06d8df63-7bd2-4fad-9acb-60871bcecee0",
|
||||
"memory":"Planning a trip to Japan next month. Interested in vegetarian restaurants in Tokyo.",
|
||||
"user_id":"alex123",
|
||||
"user_id":"alex",
|
||||
"hash":"d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-26T00:25:16.566471-07:00",
|
||||
"updated_at":"2024-07-26T00:25:16.566492-07:00",
|
||||
"categories":None
|
||||
@@ -1038,10 +1140,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&run_id=trip-planni
|
||||
{
|
||||
"id":"b4229775-d860-4ccb-983f-0f628ca112f5",
|
||||
"memory":"Planning a trip to Japan next month. Interested in vegetarian restaurants in Tokyo.",
|
||||
"user_id":"alex123",
|
||||
"user_id":"alex",
|
||||
"hash":"d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-26T00:33:20.350542-07:00",
|
||||
"updated_at":"2024-07-26T00:33:20.350560-07:00",
|
||||
"categories":None
|
||||
@@ -1049,10 +1152,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&run_id=trip-planni
|
||||
{
|
||||
"id":"df1aca24-76cf-4b92-9f58-d03857efcb64",
|
||||
"memory":"Planning a trip to Japan next month. Interested in vegetarian restaurants in Tokyo.",
|
||||
"user_id":"alex123",
|
||||
"user_id":"alex",
|
||||
"hash":"d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-26T00:51:09.642275-07:00",
|
||||
"updated_at":"2024-07-26T00:51:09.642295-07:00",
|
||||
"categories":None
|
||||
@@ -1072,10 +1176,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&run_id=trip-planni
|
||||
{
|
||||
"id": "06d8df63-7bd2-4fad-9acb-60871bcecee0",
|
||||
"memory": "Planning a trip to Japan next month. Interested in vegetarian restaurants in Tokyo.",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-26T00:25:16.566471-07:00",
|
||||
"updated_at": "2024-07-26T00:25:16.566492-07:00",
|
||||
"categories": ["food_preferences"]
|
||||
@@ -1083,10 +1188,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&run_id=trip-planni
|
||||
{
|
||||
"id": "b4229775-d860-4ccb-983f-0f628ca112f5",
|
||||
"memory": "Planning a trip to Japan next month. Interested in vegetarian restaurants in Tokyo.",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-26T00:33:20.350542-07:00",
|
||||
"updated_at": "2024-07-26T00:33:20.350560-07:00",
|
||||
"categories": ["food_preferences"]
|
||||
@@ -1094,10 +1200,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&run_id=trip-planni
|
||||
{
|
||||
"id": "df1aca24-76cf-4b92-9f58-d03857efcb64",
|
||||
"memory": "Planning a trip to Japan next month. Interested in vegetarian restaurants in Tokyo.",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata":None,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-26T00:51:09.642275-07:00",
|
||||
"updated_at": "2024-07-26T00:51:09.642295-07:00",
|
||||
"categories": None
|
||||
@@ -1133,10 +1240,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/582bbe6d-506b-48c6-a4c6-5df3b1e6342
|
||||
{
|
||||
"id":"06d8df63-7bd2-4fad-9acb-60871bcecee0",
|
||||
"memory":"Planning a trip to Japan next month. Interested in vegetarian restaurants in Tokyo.",
|
||||
"user_id":"alex123",
|
||||
"user_id":"alex",
|
||||
"hash":"d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata":"None",
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-26T00:25:16.566471-07:00",
|
||||
"updated_at":"2024-07-26T00:25:16.566492-07:00",
|
||||
"categories": ["travel"]
|
||||
@@ -1152,55 +1260,55 @@ You can filter memories by their categories when using get_all:
|
||||
|
||||
```python Python
|
||||
# Get memories with specific categories
|
||||
memories = client.get_all(user_id="alex123", categories=["likes"])
|
||||
memories = client.get_all(user_id="alex", categories=["likes"])
|
||||
|
||||
# Get memories with multiple categories
|
||||
memories = client.get_all(user_id="alex123", categories=["likes", "food_preferences"])
|
||||
memories = client.get_all(user_id="alex", categories=["likes", "food_preferences"])
|
||||
|
||||
# Custom pagination with categories
|
||||
memories = client.get_all(user_id="alex123", categories=["likes"], page=1, page_size=50)
|
||||
memories = client.get_all(user_id="alex", categories=["likes"], page=1, page_size=50)
|
||||
|
||||
# Get memories with specific keywords
|
||||
memories = client.get_all(user_id="alex123", keywords="to play", page=1, page_size=50)
|
||||
memories = client.get_all(user_id="alex", keywords="to play", page=1, page_size=50)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Get memories with specific categories
|
||||
client.getAll({ user_id: "alex123", categories: ["likes"] })
|
||||
client.getAll({ user_id: "alex", categories: ["likes"] })
|
||||
.then(memories => console.log(memories))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// Get memories with multiple categories
|
||||
client.getAll({ user_id: "alex123", categories: ["likes", "food_preferences"] })
|
||||
client.getAll({ user_id: "alex", categories: ["likes", "food_preferences"] })
|
||||
.then(memories => console.log(memories))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// Custom pagination with categories
|
||||
client.getAll({ user_id: "alex123", categories: ["likes"], page: 1, page_size: 50 })
|
||||
client.getAll({ user_id: "alex", categories: ["likes"], page: 1, page_size: 50 })
|
||||
.then(memories => console.log(memories))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// Get memories with specific keywords
|
||||
client.getAll({ user_id: "alex123", keywords: "to play", page: 1, page_size: 50 })
|
||||
client.getAll({ user_id: "alex", keywords: "to play", page: 1, page_size: 50 })
|
||||
.then(memories => console.log(memories))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
# Get memories with specific categories
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&categories=likes" \
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&categories=likes" \
|
||||
-H "Authorization: Token your-api-key"
|
||||
|
||||
# Get memories with multiple categories
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&categories=likes,food_preferences" \
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&categories=likes,food_preferences" \
|
||||
-H "Authorization: Token your-api-key"
|
||||
|
||||
# Custom pagination with categories
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&categories=likes&page=1&page_size=50" \
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&categories=likes&page=1&page_size=50" \
|
||||
-H "Authorization: Token your-api-key"
|
||||
|
||||
# Get memories with specific keywords
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&keywords=to play&page=1&page_size=50" \
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&keywords=to play&page=1&page_size=50" \
|
||||
-H "Authorization: Token your-api-key"
|
||||
```
|
||||
|
||||
@@ -1213,10 +1321,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&keywords=to play&p
|
||||
{
|
||||
"id": "06d8df63-7bd2-4fad-9acb-60871bcecee0",
|
||||
"memory": "Likes pizza and pasta",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata": null,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-26T00:25:16.566471-07:00",
|
||||
"updated_at": "2024-07-26T00:25:16.566492-07:00",
|
||||
"categories": ["likes", "food_preferences"]
|
||||
@@ -1224,10 +1333,11 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex123&keywords=to play&p
|
||||
{
|
||||
"id": "b4229775-d860-4ccb-983f-0f628ca112f5",
|
||||
"memory": "Likes to travel to beach destinations",
|
||||
"user_id": "alex123",
|
||||
"user_id": "alex",
|
||||
"hash": "d2088c936e259f2f5d2d75543d31401c",
|
||||
"metadata": null,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-26T00:33:20.350542-07:00",
|
||||
"updated_at": "2024-07-26T00:33:20.350560-07:00",
|
||||
"categories": ["likes"]
|
||||
@@ -1355,6 +1465,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
|
||||
"hash":"62bc074f56d1f909f1b4c2b639f56f6a",
|
||||
"metadata":null,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-25T23:57:00.108347-07:00",
|
||||
"updated_at":"2024-07-25T23:57:00.108367-07:00",
|
||||
"categories": ["food_preferences"]
|
||||
@@ -1375,6 +1486,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
|
||||
"hash":"62bc074f56d1f909f1b4c2b639f56f6a",
|
||||
"metadata":null,
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at":"2024-07-25T23:57:00.108347-07:00",
|
||||
"updated_at":"2024-07-25T23:57:00.108367-07:00",
|
||||
"categories": ["food_preferences"]
|
||||
@@ -1464,45 +1576,25 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
|
||||
}'
|
||||
```
|
||||
|
||||
```json Output (Default)
|
||||
```json Output
|
||||
[
|
||||
{
|
||||
"id":"f38b689d-6b24-45b7-bced-17fbb4d8bac7",
|
||||
"memory":"Name: Alex. Vegetarian. Allergic to nuts.",
|
||||
"user_id":"alex",
|
||||
"hash":"62bc074f56d1f909f1b4c2b639f56f6a",
|
||||
"metadata":{"food":"vegan"},
|
||||
"immutable": false,
|
||||
"created_at":"2024-07-25T23:57:00.108347-07:00",
|
||||
"updated_at":"2024-07-25T23:57:00.108367-07:00",
|
||||
"categories": ["food_preferences"]
|
||||
}
|
||||
{
|
||||
"id": "654fee-b411-4afe-b7e5-35789b72c4a5",
|
||||
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
|
||||
"user_id": "alex",
|
||||
"metadata": {"food": "vegan"},
|
||||
"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",
|
||||
"hash":"62bc074f56d1f909f1b4c2b639f56f6a",
|
||||
"metadata":{"food":"vegan"},
|
||||
"immutable": false,
|
||||
"created_at":"2024-07-25T23:57:00.108347-07:00",
|
||||
"updated_at":"2024-07-25T23:57:00.108367-07:00",
|
||||
"categories": ["food_preferences"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### 4.5 Memory History
|
||||
|
||||
Get history of how a memory has changed over time.
|
||||
@@ -1585,7 +1677,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/<memory-id-here>/history/" \
|
||||
],
|
||||
"old_memory":"None",
|
||||
"new_memory":"Turned vegetarian.",
|
||||
"user_id":"alex123456",
|
||||
"user_id":"alex",
|
||||
"event":"ADD",
|
||||
"metadata":"None",
|
||||
"created_at":"2024-07-26T01:02:41.737310-07:00",
|
||||
@@ -1815,8 +1907,7 @@ response = client.batch_update(update_memories)
|
||||
print(response)
|
||||
```
|
||||
```javascript JavaScript
|
||||
const updateMemories = [
|
||||
{"memory_id": "285ed74b-6e05-4043-b16b-3abd5b533496",
|
||||
const updateMemories = [{"memory_id": "285ed74b-6e05-4043-b16b-3abd5b533496",
|
||||
text: "Watches football"
|
||||
},
|
||||
{"memory_id": "2c9bd859-d1b7-4d33-a6b8-94e0147c4f07",
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Thread } from "@/components/assistant-ui/thread";
|
||||
import { ThreadList } from "@/components/assistant-ui/thread-list";
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Sun, Moon, MessageSquare } from "lucide-react";
|
||||
import { Sun, Moon, AlignJustify } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ThemeAwareLogo from "@/components/mem0/theme-aware-logo";
|
||||
import Link from "next/link";
|
||||
@@ -63,7 +63,7 @@ export const Assistant = () => {
|
||||
|
||||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<div className={`h-dvh bg-[#f8fafc] dark:bg-zinc-900 text-[#1e293b] ${isDarkMode ? "dark" : ""}`}>
|
||||
<div className={`bg-[#f8fafc] dark:bg-zinc-900 text-[#1e293b] ${isDarkMode ? "dark" : ""}`}>
|
||||
<header className="h-16 border-b border-[#e2e8f0] flex items-center justify-between px-4 sm:px-6 bg-white dark:bg-zinc-900 dark:border-zinc-800 dark:text-white">
|
||||
<div className="flex items-center">
|
||||
<Link href="/" className="flex items-center">
|
||||
@@ -71,29 +71,34 @@ export const Assistant = () => {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="text-[#475569] dark:text-zinc-300 md:hidden"
|
||||
>
|
||||
<MessageSquare className="w-10 h-10" />
|
||||
</Button>
|
||||
<AlignJustify size={24} className="md:hidden" />
|
||||
</Button>
|
||||
|
||||
|
||||
<div className="md:flex items-center hidden">
|
||||
<button
|
||||
className="p-2 rounded-full hover:bg-[#eef2ff] dark:hover:bg-zinc-800 text-[#475569] dark:text-zinc-300"
|
||||
onClick={toggleDarkMode}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{isDarkMode ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
|
||||
{isDarkMode ? <Sun className="w-6 h-6" /> : <Moon className="w-6 h-6" />}
|
||||
</button>
|
||||
<GithubButton url="https://github.com/mem0ai/mem0/tree/main/examples" />
|
||||
|
||||
<Link href={"https://app.mem0.ai/"} target="_blank" className="py-2 ml-2 px-4 font-semibold dark:bg-zinc-100 dark:hover:bg-zinc-200 bg-zinc-800 text-white rounded-full hover:bg-zinc-900 dark:text-[#475569]">
|
||||
Save Memories
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-[260px_1fr] gap-x-0 h-[calc(100vh-8rem)] md:h-[calc(100vh-4rem)]">
|
||||
<div className="grid grid-cols-1 md:grid-cols-[260px_1fr] gap-x-0 h-[calc(100dvh-4rem)]">
|
||||
<ThreadList onResetUserId={resetUserId} isDarkMode={isDarkMode} />
|
||||
<Thread sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} onResetUserId={resetUserId} isDarkMode={isDarkMode} />
|
||||
<Thread sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} onResetUserId={resetUserId} isDarkMode={isDarkMode} toggleDarkMode={toggleDarkMode} />
|
||||
</div>
|
||||
</div>
|
||||
</AssistantRuntimeProvider>
|
||||
|
||||
@@ -19,14 +19,14 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import ThemeAwareLogo from "@/components/assistant-ui/theme-aware-logo";
|
||||
import Link from "next/link";
|
||||
// import ThemeAwareLogo from "@/components/assistant-ui/theme-aware-logo";
|
||||
// import Link from "next/link";
|
||||
interface ThreadListProps {
|
||||
onResetUserId?: () => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const ThreadList: FC<ThreadListProps> = ({ onResetUserId, isDarkMode }) => {
|
||||
export const ThreadList: FC<ThreadListProps> = ({ onResetUserId }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -79,13 +79,7 @@ export const ThreadList: FC<ThreadListProps> = ({ onResetUserId, isDarkMode }) =
|
||||
</div>
|
||||
<ThreadListItems />
|
||||
</div>
|
||||
<div>
|
||||
<Link href="https://www.assistant-ui.com/" target="_blank" className="flex justify-center items-center gap-2">
|
||||
<h1 className="text-sm text-[#475569] dark:text-zinc-300 text-center">built using</h1>
|
||||
<ThemeAwareLogo width={24} height={24} isDarkMode={isDarkMode} />
|
||||
<p className="text-md font-bold dark:text-zinc-300">assistant-ui</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</ThreadListPrimitive.Root>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,9 +22,12 @@ import {
|
||||
SendHorizontalIcon,
|
||||
ArchiveIcon,
|
||||
PlusIcon,
|
||||
Sun,
|
||||
Moon,
|
||||
SaveIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import { Dispatch, SetStateAction, useState, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
@@ -42,14 +45,14 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import GithubButton from "../mem0/github-button";
|
||||
import Link from "next/link";
|
||||
import ThemeAwareLogo from "./theme-aware-logo";
|
||||
|
||||
interface ThreadProps {
|
||||
sidebarOpen: boolean;
|
||||
setSidebarOpen: Dispatch<SetStateAction<boolean>>;
|
||||
onResetUserId?: () => void;
|
||||
isDarkMode: boolean;
|
||||
toggleDarkMode: () => void;
|
||||
}
|
||||
|
||||
export const Thread: FC<ThreadProps> = ({
|
||||
@@ -57,12 +60,14 @@ export const Thread: FC<ThreadProps> = ({
|
||||
setSidebarOpen,
|
||||
onResetUserId,
|
||||
isDarkMode,
|
||||
toggleDarkMode
|
||||
}) => {
|
||||
const [resetDialogOpen, setResetDialogOpen] = useState(false);
|
||||
const composerInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
return (
|
||||
<ThreadPrimitive.Root
|
||||
className="bg-[#f8fafc] dark:bg-zinc-900 box-border h-full flex flex-col overflow-hidden relative"
|
||||
className="bg-[#f8fafc] dark:bg-zinc-900 box-border flex flex-col overflow-hidden relative h-[calc(100dvh-4rem)] pb-4 md:h-full"
|
||||
style={{
|
||||
["--thread-max-width" as string]: "42rem",
|
||||
}}
|
||||
@@ -78,13 +83,13 @@ export const Thread: FC<ThreadProps> = ({
|
||||
{/* Mobile sidebar drawer */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-y-0 left-0 z-40 w-[85%] bg-white dark:bg-zinc-900 transform transition-transform duration-300 ease-in-out md:hidden",
|
||||
"fixed inset-y-0 left-0 z-40 w-[75%] bg-white shadow-lg rounded-r-lg dark:bg-zinc-900 transform transition-transform duration-300 ease-in-out md:hidden",
|
||||
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||
)}
|
||||
>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-between border-b dark:text-white border-[#e2e8f0] dark:border-zinc-800 p-4">
|
||||
<h2 className="font-medium">Recent Chats</h2>
|
||||
<h2 className="font-medium">Settings</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{onResetUserId && (
|
||||
<AlertDialog
|
||||
@@ -141,13 +146,44 @@ export const Thread: FC<ThreadProps> = ({
|
||||
<div className="flex flex-col justify-between items-stretch gap-1.5 h-full dark:text-white">
|
||||
<ThreadListPrimitive.Root className="flex flex-col items-stretch gap-1.5 h-full dark:text-white">
|
||||
<ThreadListPrimitive.New asChild>
|
||||
<div className="flex items-center flex-col gap-2 w-full">
|
||||
<Button
|
||||
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 dark:data-[active]:bg-zinc-800 flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start bg-[#4f46e5] text-white dark:bg-[#6366f1]"
|
||||
className="hover:bg-zinc-600 w-full dark:hover:bg-zinc-800 dark:data-[active]:bg-zinc-800 flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start bg-[#4f46e5] text-white dark:bg-[#6366f1]"
|
||||
variant="default"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
New Thread
|
||||
</Button>
|
||||
<Button
|
||||
className="hover:bg-zinc-600 w-full dark:hover:bg-zinc-700 dark:data-[active]:bg-zinc-800 flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start bg-zinc-800 text-white"
|
||||
onClick={toggleDarkMode}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Sun className="w-6 h-6" />
|
||||
<span>Toggle Light Mode</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Moon className="w-6 h-6" />
|
||||
<span>Toggle Dark Mode</span>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<GithubButton url="https://github.com/mem0ai/mem0/tree/main/examples" className="w-full rounded-lg h-9 pl-2 text-sm font-semibold bg-zinc-800 dark:border-zinc-800 dark:text-white text-white hover:bg-zinc-900" text="View on Github" />
|
||||
|
||||
<Link
|
||||
href={"https://app.mem0.ai/"}
|
||||
target="_blank"
|
||||
className="py-2 px-4 w-full rounded-lg h-9 pl-3 text-sm font-semibold dark:bg-zinc-800 dark:hover:bg-zinc-700 bg-zinc-800 text-white hover:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<SaveIcon className="w-4 h-4" />
|
||||
Save Memories
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</ThreadListPrimitive.New>
|
||||
<div className="mt-4 mb-2">
|
||||
<h2 className="text-sm font-medium text-[#475569] dark:text-zinc-300 px-2.5">
|
||||
@@ -156,29 +192,18 @@ export const Thread: FC<ThreadProps> = ({
|
||||
</div>
|
||||
<ThreadListPrimitive.Items components={{ ThreadListItem }} />
|
||||
</ThreadListPrimitive.Root>
|
||||
<div>
|
||||
<Link
|
||||
href="https://www.assistant-ui.com/"
|
||||
target="_blank"
|
||||
className="flex justify-center items-center gap-2"
|
||||
>
|
||||
<h1 className="text-sm text-[#475569] dark:text-zinc-300 text-center">
|
||||
built using
|
||||
</h1>
|
||||
<ThemeAwareLogo width={24} height={24} isDarkMode={isDarkMode} />
|
||||
<p className="text-md font-bold dark:text-zinc-300">
|
||||
assistant-ui
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="flex h-full flex-col items-center px-4 pt-8 justify-end">
|
||||
<ThreadWelcome />
|
||||
<ScrollArea className="flex-1 w-full">
|
||||
<div className="flex h-full flex-col w-full items-center px-4 pt-8 justify-end">
|
||||
<ThreadWelcome
|
||||
composerInputRef={
|
||||
composerInputRef as React.RefObject<HTMLTextAreaElement>
|
||||
}
|
||||
/>
|
||||
|
||||
<ThreadPrimitive.Messages
|
||||
components={{
|
||||
@@ -194,9 +219,13 @@ export const Thread: FC<ThreadProps> = ({
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="sticky bottom-0 mt-3 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit px-4 pb-4 mx-auto">
|
||||
<div className="sticky bottom-0 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit px-4 md:pb-4 mx-auto">
|
||||
<ThreadScrollToBottom />
|
||||
<Composer />
|
||||
<Composer
|
||||
composerInputRef={
|
||||
composerInputRef as React.RefObject<HTMLTextAreaElement>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ThreadPrimitive.Root>
|
||||
);
|
||||
@@ -216,57 +245,74 @@ const ThreadScrollToBottom: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadWelcome: FC = () => {
|
||||
interface ThreadWelcomeProps {
|
||||
composerInputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
const ThreadWelcome: FC<ThreadWelcomeProps> = ({ composerInputRef }) => {
|
||||
return (
|
||||
<ThreadPrimitive.Empty>
|
||||
<div className="flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col">
|
||||
<div className="flex w-full flex-grow flex-col items-center justify-start h-[calc(100vh-23rem)] md:h-[calc(100vh-18rem)]">
|
||||
<div className="flex w-full flex-grow flex-col mt-8 md:h-[calc(100vh-15rem)]">
|
||||
<div className="flex w-full flex-grow flex-col items-center justify-start">
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="text-2xl md:text-4xl font-bold text-[#1e293b] dark:text-white mb-2">
|
||||
<div className="text-[2rem] leading-[1] tracking-[-0.02em] md:text-4xl font-bold text-[#1e293b] dark:text-white mb-2 text-center md:w-full w-5/6">
|
||||
Mem0 - ChatGPT with memory
|
||||
</div>
|
||||
<p className="text-center text-sm text-[#1e293b] dark:text-white mb-2 w-3/4">
|
||||
<p className="text-center text-md text-[#1e293b] dark:text-white mb-2 md:w-3/4 w-5/6">
|
||||
A personalized AI chat app powered by Mem0 that remembers your
|
||||
preferences, facts, and memories.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center justify-center mt-16">
|
||||
<p className="mt-4 font-medium text-[#1e293b] dark:text-white">
|
||||
How can I help you today?
|
||||
</p>
|
||||
<ThreadWelcomeSuggestions />
|
||||
<ThreadWelcomeSuggestions composerInputRef={composerInputRef} />
|
||||
</div>
|
||||
</div>
|
||||
</ThreadPrimitive.Empty>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadWelcomeSuggestions: FC = () => {
|
||||
interface ThreadWelcomeSuggestionsProps {
|
||||
composerInputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
const ThreadWelcomeSuggestions: FC<ThreadWelcomeSuggestionsProps> = ({ composerInputRef }) => {
|
||||
return (
|
||||
<div className="mt-3 flex w-full items-stretch justify-center gap-4 dark:text-white">
|
||||
<div className="mt-3 flex flex-col md:flex-row w-full md:items-stretch justify-center gap-4 dark:text-white items-center">
|
||||
<ThreadPrimitive.Suggestion
|
||||
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
|
||||
className="hover:bg-[#eef2ff] w-full dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
|
||||
prompt="I like to travel to "
|
||||
method="replace"
|
||||
onClick={() => {
|
||||
composerInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">
|
||||
Travel
|
||||
</span>
|
||||
</ThreadPrimitive.Suggestion>
|
||||
<ThreadPrimitive.Suggestion
|
||||
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
|
||||
className="hover:bg-[#eef2ff] w-full dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
|
||||
prompt="I like to eat "
|
||||
method="replace"
|
||||
onClick={() => {
|
||||
composerInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">
|
||||
Food
|
||||
</span>
|
||||
</ThreadPrimitive.Suggestion>
|
||||
<ThreadPrimitive.Suggestion
|
||||
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
|
||||
className="hover:bg-[#eef2ff] w-full dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
|
||||
prompt="I am working on "
|
||||
method="replace"
|
||||
onClick={() => {
|
||||
composerInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">
|
||||
Project details
|
||||
@@ -276,7 +322,11 @@ const ThreadWelcomeSuggestions: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Composer: FC = () => {
|
||||
interface ComposerProps {
|
||||
composerInputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
}
|
||||
|
||||
const Composer: FC<ComposerProps> = ({ composerInputRef }) => {
|
||||
return (
|
||||
<ComposerPrimitive.Root className="focus-within:border-[#4f46e5]/20 dark:focus-within:border-[#6366f1]/20 flex w-full flex-wrap items-end rounded-full border border-[#e2e8f0] dark:border-zinc-700 bg-white dark:bg-zinc-800 px-2.5 shadow-sm transition-colors ease-in">
|
||||
<ComposerPrimitive.Input
|
||||
@@ -284,6 +334,7 @@ const Composer: FC = () => {
|
||||
autoFocus
|
||||
placeholder="Message to Mem0..."
|
||||
className="placeholder:text-zinc-400 dark:placeholder:text-zinc-500 max-h-40 flex-grow resize-none border-none bg-transparent px-2 py-4 text-sm outline-none focus:ring-0 disabled:cursor-not-allowed text-[#1e293b] dark:text-zinc-200"
|
||||
ref={composerInputRef}
|
||||
/>
|
||||
<ComposerAction />
|
||||
</ComposerPrimitive.Root>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
const GithubButton = ({ url }: { url: string }) => {
|
||||
const GithubButton = ({ url, className, text }: { url: string, className?: string, text?: string }) => {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center bg-black text-white rounded-full shadow-lg hover:bg-gray-800 transition border border-gray-700"
|
||||
className={cn("flex items-center bg-black text-white rounded-full shadow-lg hover:bg-gray-800 transition border border-gray-700", className)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
className="w-6 h-6"
|
||||
className="w-5 h-5 md:w-6 md:h-6"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
@@ -20,6 +20,7 @@ const GithubButton = ({ url }: { url: string }) => {
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{text && <span className="ml-2">{text}</span>}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import MemoryClient from "mem0ai";
|
||||
import { OpenAI } from "openai";
|
||||
import { zodResponsesFunction } from "openai/helpers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const mem0Config = {
|
||||
apiKey: process.env.MEM0_API_KEY, // GET THIS API KEY FROM MEM0 (https://app.mem0.ai/dashboard/api-keys)
|
||||
user_id: "sample-user",
|
||||
};
|
||||
|
||||
async function run() {
|
||||
// RESPONES WITHOUT MEMORIES
|
||||
console.log("\n\nRESPONES WITHOUT MEMORIES\n\n");
|
||||
await main();
|
||||
|
||||
// ADDING SOME SAMPLE MEMORIES
|
||||
await addSampleMemories();
|
||||
|
||||
// RESPONES WITH MEMORIES
|
||||
console.log("\n\nRESPONES WITH MEMORIES\n\n");
|
||||
await main(true);
|
||||
}
|
||||
|
||||
// OpenAI Response Schema
|
||||
const CarSchema = z.object({
|
||||
car_name: z.string(),
|
||||
car_price: z.string(),
|
||||
car_url: z.string(),
|
||||
car_image: z.string(),
|
||||
car_description: z.string(),
|
||||
});
|
||||
|
||||
const Cars = z.object({
|
||||
cars: z.array(CarSchema),
|
||||
});
|
||||
|
||||
async function main(memory = false) {
|
||||
const openAIClient = new OpenAI();
|
||||
const mem0Client = new MemoryClient(mem0Config);
|
||||
|
||||
const input = "Suggest me some cars that I can buy today.";
|
||||
|
||||
const tool = zodResponsesFunction({ name: "carRecommendations", parameters: Cars });
|
||||
|
||||
// First, let's store the user's memories from user input if any
|
||||
await mem0Client.add([{
|
||||
role: "user",
|
||||
content: input,
|
||||
}], mem0Config);
|
||||
|
||||
// Then search for relevant memories
|
||||
let relevantMemories = []
|
||||
if (memory) {
|
||||
relevantMemories = await mem0Client.search(input, mem0Config);
|
||||
}
|
||||
|
||||
const response = await openAIClient.responses.create({
|
||||
model: "gpt-4o",
|
||||
tools: [{ type: "web_search_preview" }, tool],
|
||||
input: `${getMemoryString(relevantMemories)}\n${input}`,
|
||||
});
|
||||
|
||||
console.log(response.output);
|
||||
}
|
||||
|
||||
async function addSampleMemories() {
|
||||
const mem0Client = new MemoryClient(mem0Config);
|
||||
|
||||
const myInterests = "I Love BMW, Audi and Porsche. I Hate Mercedes. I love Red cars and Maroon cars. I have a budget of 120K to 150K USD. I like Audi the most.";
|
||||
|
||||
await mem0Client.add([{
|
||||
role: "user",
|
||||
content: myInterests,
|
||||
}], mem0Config);
|
||||
}
|
||||
|
||||
const getMemoryString = (memories) => {
|
||||
const MEMORY_STRING_PREFIX = "These are the memories I have stored. Give more weightage to the question by users and try to answer that first. You have to modify your answer based on the memories I have provided. If the memories are irrelevant you can ignore them. Also don't reply to this section of the prompt, or the memories, they are only for your reference. The MEMORIES of the USER are: \n\n";
|
||||
const memoryString = memories.map((mem) => `${mem.memory}`).join("\n") ?? "";
|
||||
return memoryString.length > 0 ? `${MEMORY_STRING_PREFIX}${memoryString}` : "";
|
||||
};
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "openai-inbuilt-tools",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"license": "ISC",
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b",
|
||||
"dependencies": {
|
||||
"mem0ai": "^2.1.2",
|
||||
"openai": "^4.87.2",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
+16
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.8",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -31,9 +31,10 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"build": "npm run clean && prettier --check . && tsup",
|
||||
"dev": "nodemon",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && npx prettier --check . && npx tsup",
|
||||
"dev": "npx nodemon",
|
||||
"start": "npx ts-node src/oss/examples/basic.ts",
|
||||
"test": "jest",
|
||||
"test:ts": "jest --config jest.config.js",
|
||||
"test:watch": "jest --config jest.config.js --watch",
|
||||
@@ -55,7 +56,13 @@
|
||||
"sourcemap": true,
|
||||
"clean": true,
|
||||
"treeshake": true,
|
||||
"minify": false
|
||||
"minify": false,
|
||||
"external": [
|
||||
"@mem0/community"
|
||||
],
|
||||
"noExternal": [
|
||||
"!src/community/**"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"mem0",
|
||||
@@ -74,7 +81,9 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"fix-tsup-cjs": "^1.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.5.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-jest": "^29.2.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.3.0",
|
||||
@@ -96,7 +105,8 @@
|
||||
"groq-sdk": "0.3.0",
|
||||
"pg": "8.11.3",
|
||||
"redis": "4.7.0",
|
||||
"sqlite3": "5.1.7"
|
||||
"sqlite3": "5.1.7",
|
||||
"ollama": "^0.5.14"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"posthog-node": {
|
||||
|
||||
@@ -23,6 +23,8 @@ export type {
|
||||
Message,
|
||||
AllUsers,
|
||||
User,
|
||||
FeedbackPayload,
|
||||
Feedback,
|
||||
} from "./mem0.types";
|
||||
|
||||
// Export telemetry types
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Webhook,
|
||||
WebhookPayload,
|
||||
Message,
|
||||
FeedbackPayload,
|
||||
} from "./mem0.types";
|
||||
import { captureClientEvent, generateHash } from "./telemetry";
|
||||
|
||||
@@ -205,6 +206,10 @@ export default class MemoryClient {
|
||||
if (options.project_name) delete options.project_name;
|
||||
}
|
||||
|
||||
if (options.api_version) {
|
||||
options.version = options.api_version.toString();
|
||||
}
|
||||
|
||||
const payload = this._preparePayload(messages, options);
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v1/memories/`,
|
||||
@@ -556,6 +561,18 @@ export default class MemoryClient {
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
async feedback(data: FeedbackPayload): Promise<{ message: string }> {
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v1/feedback/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export { MemoryClient };
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export interface MemoryOptions {
|
||||
api_version?: API_VERSION | string;
|
||||
version?: API_VERSION | string;
|
||||
user_id?: string;
|
||||
agent_id?: string;
|
||||
app_id?: string;
|
||||
@@ -17,6 +19,7 @@ export interface MemoryOptions {
|
||||
enable_graph?: boolean;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
custom_categories?: custom_categories[];
|
||||
}
|
||||
|
||||
export interface ProjectOptions {
|
||||
@@ -28,6 +31,12 @@ export enum API_VERSION {
|
||||
V2 = "v2",
|
||||
}
|
||||
|
||||
export enum Feedback {
|
||||
POSITIVE = "POSITIVE",
|
||||
NEGATIVE = "NEGATIVE",
|
||||
VERY_NEGATIVE = "VERY_NEGATIVE",
|
||||
}
|
||||
|
||||
export interface MultiModalMessages {
|
||||
type: "image_url";
|
||||
image_url: {
|
||||
@@ -161,3 +170,9 @@ export interface WebhookPayload {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface FeedbackPayload {
|
||||
memory_id: string;
|
||||
feedback?: Feedback | null;
|
||||
feedback_reason?: string | null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { TelemetryClient } from "./telemetry.types";
|
||||
|
||||
let version = "1.0.20";
|
||||
|
||||
const MEM0_TELEMETRY = process.env.MEM0_TELEMETRY !== "false";
|
||||
const MEM0_TELEMETRY = "false";
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
const POSTHOG_HOST = "https://us.i.posthog.com";
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
build
|
||||
|
||||
# Lock files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Coverage
|
||||
coverage
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"name": "@mem0/community",
|
||||
"version": "0.0.1",
|
||||
"description": "Community features for Mem0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"require": "./dist/index.js",
|
||||
"import": "./dist/index.mjs"
|
||||
},
|
||||
"./langchain": {
|
||||
"types": "./dist/integrations/langchain/index.d.ts",
|
||||
"require": "./dist/integrations/langchain/index.js",
|
||||
"import": "./dist/integrations/langchain/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && npx prettier --check . && npx tsup",
|
||||
"dev": "npx nodemon",
|
||||
"test": "jest",
|
||||
"test:ts": "jest --config jest.config.js",
|
||||
"test:watch": "jest --config jest.config.js --watch",
|
||||
"format": "npm run clean && prettier --write .",
|
||||
"format:check": "npm run clean && prettier --check .",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"tsup": {
|
||||
"entry": {
|
||||
"index": "src/index.ts",
|
||||
"integrations/langchain/index": "src/integrations/langchain/index.ts"
|
||||
},
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
],
|
||||
"dts": {
|
||||
"resolve": true,
|
||||
"compilerOptions": {
|
||||
"rootDir": "src"
|
||||
}
|
||||
},
|
||||
"splitting": false,
|
||||
"sourcemap": true,
|
||||
"clean": true,
|
||||
"treeshake": true,
|
||||
"minify": false,
|
||||
"outDir": "dist",
|
||||
"tsconfig": "./tsconfig.json"
|
||||
},
|
||||
"keywords": [
|
||||
"mem0",
|
||||
"community",
|
||||
"ai",
|
||||
"memory"
|
||||
],
|
||||
"author": "Deshraj Yadav",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.6",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"dotenv": "^16.4.5",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.5.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-jest": "^29.2.6",
|
||||
"tsup": "^8.3.0",
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@langchain/core": "^0.3.42",
|
||||
"axios": "1.7.7",
|
||||
"mem0ai": "^2.1.8",
|
||||
"uuid": "9.0.1",
|
||||
"zod": "3.22.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./integrations/langchain";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./mem0";
|
||||
@@ -0,0 +1,314 @@
|
||||
import { MemoryClient } from "mem0ai";
|
||||
import type { Memory, MemoryOptions, SearchOptions } from "mem0ai";
|
||||
|
||||
import {
|
||||
InputValues,
|
||||
OutputValues,
|
||||
MemoryVariables,
|
||||
getInputValue,
|
||||
getOutputValue,
|
||||
} from "@langchain/core/memory";
|
||||
import {
|
||||
AIMessage,
|
||||
BaseMessage,
|
||||
ChatMessage,
|
||||
getBufferString,
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import {
|
||||
BaseChatMemory,
|
||||
BaseChatMemoryInput,
|
||||
} from "@langchain/community/memory/chat_memory";
|
||||
|
||||
/**
|
||||
* Extracts and formats memory content into a system prompt
|
||||
* @param memory Array of Memory objects from mem0ai
|
||||
* @returns Formatted system prompt string
|
||||
*/
|
||||
export const mem0MemoryContextToSystemPrompt = (memory: Memory[]): string => {
|
||||
if (!memory || !Array.isArray(memory)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return memory
|
||||
.filter((m) => m?.memory)
|
||||
.map((m) => m.memory)
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Condenses memory content into a single HumanMessage with context
|
||||
* @param memory Array of Memory objects from mem0ai
|
||||
* @returns HumanMessage containing formatted memory context
|
||||
*/
|
||||
export const condenseMem0MemoryIntoHumanMessage = (
|
||||
memory: Memory[],
|
||||
): HumanMessage => {
|
||||
const basePrompt =
|
||||
"These are the memories I have stored. Give more weightage to the question by users and try to answer that first. You have to modify your answer based on the memories I have provided. If the memories are irrelevant you can ignore them. Also don't reply to this section of the prompt, or the memories, they are only for your reference. The MEMORIES of the USER are: \n\n";
|
||||
const systemPrompt = mem0MemoryContextToSystemPrompt(memory);
|
||||
|
||||
return new HumanMessage(`${basePrompt}\n${systemPrompt}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts Mem0 memories to a list of BaseMessages
|
||||
* @param memories Array of Memory objects from mem0ai
|
||||
* @returns Array of BaseMessage objects
|
||||
*/
|
||||
export const mem0MemoryToMessages = (memories: Memory[]): BaseMessage[] => {
|
||||
if (!memories || !Array.isArray(memories)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages: BaseMessage[] = [];
|
||||
|
||||
// Add memories as system message if present
|
||||
const memoryContent = memories
|
||||
.filter((m) => m?.memory)
|
||||
.map((m) => m.memory)
|
||||
.join("\n");
|
||||
|
||||
if (memoryContent) {
|
||||
messages.push(new SystemMessage(memoryContent));
|
||||
}
|
||||
|
||||
// Add conversation messages
|
||||
memories.forEach((memory) => {
|
||||
if (memory.messages) {
|
||||
memory.messages.forEach((msg) => {
|
||||
const content =
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content);
|
||||
if (msg.role === "user") {
|
||||
messages.push(new HumanMessage(content));
|
||||
} else if (msg.role === "assistant") {
|
||||
messages.push(new AIMessage(content));
|
||||
} else if (content) {
|
||||
messages.push(new ChatMessage(content, msg.role));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return messages;
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface defining the structure of the input data for the Mem0Client
|
||||
*/
|
||||
export interface ClientOptions {
|
||||
apiKey: string;
|
||||
host?: string;
|
||||
organizationName?: string;
|
||||
projectName?: string;
|
||||
organizationId?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface defining the structure of the input data for the Mem0Memory
|
||||
* class. It includes properties like memoryKey, sessionId, and apiKey.
|
||||
*/
|
||||
export interface Mem0MemoryInput extends BaseChatMemoryInput {
|
||||
sessionId: string;
|
||||
apiKey: string;
|
||||
humanPrefix?: string;
|
||||
aiPrefix?: string;
|
||||
memoryOptions?: MemoryOptions | SearchOptions;
|
||||
mem0Options?: ClientOptions;
|
||||
separateMessages?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used to manage the memory of a chat session using the Mem0 service.
|
||||
* It handles loading and saving chat history, and provides methods to format
|
||||
* the memory content for use in chat models.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const memory = new Mem0Memory({
|
||||
* sessionId: "user123" // or use user_id inside of memoryOptions (recommended),
|
||||
* apiKey: "your-api-key",
|
||||
* memoryOptions: {
|
||||
* user_id: "user123",
|
||||
* run_id: "run123"
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* // Use with a chat model
|
||||
* const model = new ChatOpenAI({
|
||||
* modelName: "gpt-3.5-turbo",
|
||||
* temperature: 0,
|
||||
* });
|
||||
*
|
||||
* const chain = new ConversationChain({ llm: model, memory });
|
||||
* ```
|
||||
*/
|
||||
export class Mem0Memory extends BaseChatMemory implements Mem0MemoryInput {
|
||||
memoryKey = "history";
|
||||
|
||||
apiKey: string;
|
||||
|
||||
sessionId: string;
|
||||
|
||||
humanPrefix = "Human";
|
||||
|
||||
aiPrefix = "AI";
|
||||
|
||||
mem0Client: InstanceType<typeof MemoryClient>;
|
||||
|
||||
memoryOptions: MemoryOptions | SearchOptions;
|
||||
|
||||
mem0Options: ClientOptions;
|
||||
|
||||
// Whether to return separate messages for chat history with a SystemMessage containing (facts and summary) or return a single HumanMessage with the entire memory context.
|
||||
// Defaults to false (return a single HumanMessage) in order to allow more flexibility with different models.
|
||||
separateMessages?: boolean;
|
||||
|
||||
constructor(fields: Mem0MemoryInput) {
|
||||
if (!fields.apiKey) {
|
||||
throw new Error("apiKey is required for Mem0Memory");
|
||||
}
|
||||
if (!fields.sessionId) {
|
||||
throw new Error("sessionId is required for Mem0Memory");
|
||||
}
|
||||
|
||||
super({
|
||||
returnMessages: fields?.returnMessages ?? false,
|
||||
inputKey: fields?.inputKey,
|
||||
outputKey: fields?.outputKey,
|
||||
});
|
||||
|
||||
this.apiKey = fields.apiKey;
|
||||
this.sessionId = fields.sessionId;
|
||||
this.humanPrefix = fields.humanPrefix ?? this.humanPrefix;
|
||||
this.aiPrefix = fields.aiPrefix ?? this.aiPrefix;
|
||||
this.memoryOptions = fields.memoryOptions ?? {};
|
||||
this.mem0Options = fields.mem0Options ?? {
|
||||
apiKey: this.apiKey,
|
||||
};
|
||||
this.separateMessages = fields.separateMessages ?? false;
|
||||
try {
|
||||
this.mem0Client = new MemoryClient({
|
||||
...this.mem0Options,
|
||||
apiKey: this.apiKey,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize Mem0Client:", error);
|
||||
throw new Error(
|
||||
"Failed to initialize Mem0Client. Please check your configuration.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get memoryKeys(): string[] {
|
||||
return [this.memoryKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves memories from the Mem0 service and formats them for use
|
||||
* @param values Input values containing optional search query
|
||||
* @returns Promise resolving to formatted memory variables
|
||||
*/
|
||||
async loadMemoryVariables(values: InputValues): Promise<MemoryVariables> {
|
||||
const searchType = values.input ? "search" : "get_all";
|
||||
let memories: Memory[] = [];
|
||||
|
||||
try {
|
||||
if (searchType === "get_all") {
|
||||
memories = await this.mem0Client.getAll({
|
||||
user_id: this.sessionId,
|
||||
...this.memoryOptions,
|
||||
});
|
||||
} else {
|
||||
memories = await this.mem0Client.search(values.input, {
|
||||
user_id: this.sessionId,
|
||||
...this.memoryOptions,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading memories:", error);
|
||||
return this.returnMessages
|
||||
? { [this.memoryKey]: [] }
|
||||
: { [this.memoryKey]: "" };
|
||||
}
|
||||
|
||||
if (this.returnMessages) {
|
||||
return {
|
||||
[this.memoryKey]: this.separateMessages
|
||||
? mem0MemoryToMessages(memories)
|
||||
: [condenseMem0MemoryIntoHumanMessage(memories)],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
[this.memoryKey]: this.separateMessages
|
||||
? getBufferString(
|
||||
mem0MemoryToMessages(memories),
|
||||
this.humanPrefix,
|
||||
this.aiPrefix,
|
||||
)
|
||||
: (condenseMem0MemoryIntoHumanMessage(memories).content ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current conversation context to the Mem0 service
|
||||
* @param inputValues Input messages to be saved
|
||||
* @param outputValues Output messages to be saved
|
||||
* @returns Promise resolving when the context has been saved
|
||||
*/
|
||||
async saveContext(
|
||||
inputValues: InputValues,
|
||||
outputValues: OutputValues,
|
||||
): Promise<void> {
|
||||
const input = getInputValue(inputValues, this.inputKey);
|
||||
const output = getOutputValue(outputValues, this.outputKey);
|
||||
|
||||
if (!input || !output) {
|
||||
console.warn("Missing input or output values, skipping memory save");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const messages = [
|
||||
{
|
||||
role: "user",
|
||||
content: `${input}`,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: `${output}`,
|
||||
},
|
||||
];
|
||||
|
||||
await this.mem0Client.add(messages, {
|
||||
user_id: this.sessionId,
|
||||
...this.memoryOptions,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving memory context:", error);
|
||||
// Continue execution even if memory save fails
|
||||
}
|
||||
|
||||
await super.saveContext(inputValues, outputValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all memories for the current session
|
||||
* @returns Promise resolving when memories have been cleared
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
// Note: Implement clear functionality if Mem0Client provides it
|
||||
// await this.mem0Client.clear(this.sessionId);
|
||||
} catch (error) {
|
||||
console.error("Error clearing memories:", error);
|
||||
}
|
||||
|
||||
await super.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"],
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
@@ -116,6 +116,36 @@ async function runTests(memory: Memory) {
|
||||
}
|
||||
}
|
||||
|
||||
async function demoLocalMemory() {
|
||||
console.log("\n=== Testing In-Memory Vector Store with Ollama===\n");
|
||||
|
||||
const memory = new Memory({
|
||||
version: "v1.1",
|
||||
embedder: {
|
||||
provider: "ollama",
|
||||
config: {
|
||||
model: "nomic-embed-text:latest",
|
||||
},
|
||||
},
|
||||
vectorStore: {
|
||||
provider: "memory",
|
||||
config: {
|
||||
collectionName: "memories",
|
||||
dimension: 768, // 768 is the dimension of the nomic-embed-text model
|
||||
},
|
||||
},
|
||||
llm: {
|
||||
provider: "ollama",
|
||||
config: {
|
||||
model: "llama3.1:8b",
|
||||
},
|
||||
},
|
||||
// historyDbPath: "memory.db",
|
||||
});
|
||||
|
||||
await runTests(memory);
|
||||
}
|
||||
|
||||
async function demoMemoryStore() {
|
||||
console.log("\n=== Testing In-Memory Vector Store ===\n");
|
||||
|
||||
@@ -346,6 +376,9 @@ async function main() {
|
||||
// Test in-memory store
|
||||
await demoMemoryStore();
|
||||
|
||||
// Test in-memory store with Ollama
|
||||
await demoLocalMemory();
|
||||
|
||||
// Test graph memory if Neo4j environment variables are set
|
||||
if (
|
||||
process.env.NEO4J_URL &&
|
||||
@@ -384,4 +417,4 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// main();
|
||||
main();
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Memory } from "../src";
|
||||
import { Ollama } from "ollama";
|
||||
import * as readline from "readline";
|
||||
|
||||
const memory = new Memory({
|
||||
embedder: {
|
||||
provider: "ollama",
|
||||
config: {
|
||||
model: "nomic-embed-text:latest",
|
||||
},
|
||||
},
|
||||
vectorStore: {
|
||||
provider: "memory",
|
||||
config: {
|
||||
collectionName: "memories",
|
||||
dimension: 768, // since we are using nomic-embed-text
|
||||
},
|
||||
},
|
||||
llm: {
|
||||
provider: "ollama",
|
||||
config: {
|
||||
model: "llama3.1:8b",
|
||||
},
|
||||
},
|
||||
historyDbPath: "local-llms.db",
|
||||
});
|
||||
|
||||
async function chatWithMemories(message: string, userId = "default_user") {
|
||||
const relevantMemories = await memory.search(message, { userId: userId });
|
||||
|
||||
const memoriesStr = relevantMemories.results
|
||||
.map((entry) => `- ${entry.memory}`)
|
||||
.join("\n");
|
||||
|
||||
const systemPrompt = `You are a helpful AI. Answer the question based on query and memories.
|
||||
User Memories:
|
||||
${memoriesStr}`;
|
||||
|
||||
const messages = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: message },
|
||||
];
|
||||
|
||||
const ollama = new Ollama();
|
||||
const response = await ollama.chat({
|
||||
model: "llama3.1:8b",
|
||||
messages: messages,
|
||||
});
|
||||
|
||||
const assistantResponse = response.message.content || "";
|
||||
|
||||
messages.push({ role: "assistant", content: assistantResponse });
|
||||
await memory.add(messages, { userId: userId });
|
||||
|
||||
return assistantResponse;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
console.log("Chat with AI (type 'exit' to quit)");
|
||||
|
||||
const askQuestion = (): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question("You: ", (input) => {
|
||||
resolve(input.trim());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const userInput = await askQuestion();
|
||||
|
||||
if (userInput.toLowerCase() === "exit") {
|
||||
console.log("Goodbye!");
|
||||
rl.close();
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await chatWithMemories(userInput, "sample_user");
|
||||
console.log(`AI: ${response}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("An error occurred:", error);
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Ollama } from "ollama";
|
||||
import { Embedder } from "./base";
|
||||
import { EmbeddingConfig } from "../types";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class OllamaEmbedder implements Embedder {
|
||||
private ollama: Ollama;
|
||||
private model: string;
|
||||
// Using this variable to avoid calling the Ollama server multiple times
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(config: EmbeddingConfig) {
|
||||
this.ollama = new Ollama({
|
||||
host: config.url || "http://localhost:11434",
|
||||
});
|
||||
this.model = config.model || "nomic-embed-text:latest";
|
||||
this.ensureModelExists().catch((err) => {
|
||||
logger.error(`Error ensuring model exists: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
async embed(text: string): Promise<number[]> {
|
||||
try {
|
||||
await this.ensureModelExists();
|
||||
} catch (err) {
|
||||
logger.error(`Error ensuring model exists: ${err}`);
|
||||
}
|
||||
const response = await this.ollama.embeddings({
|
||||
model: this.model,
|
||||
prompt: text,
|
||||
});
|
||||
return response.embedding;
|
||||
}
|
||||
|
||||
async embedBatch(texts: string[]): Promise<number[][]> {
|
||||
const response = await Promise.all(texts.map((text) => this.embed(text)));
|
||||
return response;
|
||||
}
|
||||
|
||||
private async ensureModelExists(): Promise<boolean> {
|
||||
if (this.initialized) {
|
||||
return true;
|
||||
}
|
||||
const local_models = await this.ollama.list();
|
||||
if (!local_models.models.find((m: any) => m.name === this.model)) {
|
||||
logger.info(`Pulling model ${this.model}...`);
|
||||
await this.ollama.pull({ model: this.model });
|
||||
}
|
||||
this.initialized = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ export * from "./memory/memory.types";
|
||||
export * from "./types";
|
||||
export * from "./embeddings/base";
|
||||
export * from "./embeddings/openai";
|
||||
export * from "./embeddings/ollama";
|
||||
export * from "./llms/base";
|
||||
export * from "./llms/openai";
|
||||
export * from "./llms/openai_structured";
|
||||
export * from "./llms/anthropic";
|
||||
export * from "./llms/groq";
|
||||
export * from "./llms/ollama";
|
||||
export * from "./vector_stores/base";
|
||||
export * from "./vector_stores/memory";
|
||||
export * from "./vector_stores/qdrant";
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Ollama } from "ollama";
|
||||
import { LLM, LLMResponse } from "./base";
|
||||
import { LLMConfig, Message } from "../types";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class OllamaLLM implements LLM {
|
||||
private ollama: Ollama;
|
||||
private model: string;
|
||||
// Using this variable to avoid calling the Ollama server multiple times
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(config: LLMConfig) {
|
||||
this.ollama = new Ollama({
|
||||
host: config.config?.url || "http://localhost:11434",
|
||||
});
|
||||
this.model = config.model || "llama3.1:8b";
|
||||
this.ensureModelExists().catch((err) => {
|
||||
logger.error(`Error ensuring model exists: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
async generateResponse(
|
||||
messages: Message[],
|
||||
responseFormat?: { type: string },
|
||||
tools?: any[],
|
||||
): Promise<string | LLMResponse> {
|
||||
try {
|
||||
await this.ensureModelExists();
|
||||
} catch (err) {
|
||||
logger.error(`Error ensuring model exists: ${err}`);
|
||||
}
|
||||
|
||||
const completion = await this.ollama.chat({
|
||||
model: this.model,
|
||||
messages: messages.map((msg) => {
|
||||
const role = msg.role as "system" | "user" | "assistant";
|
||||
return {
|
||||
role,
|
||||
content:
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content),
|
||||
};
|
||||
}),
|
||||
...(responseFormat?.type === "json_object" && { format: "json" }),
|
||||
...(tools && { tools, tool_choice: "auto" }),
|
||||
});
|
||||
|
||||
const response = completion.message;
|
||||
|
||||
if (response.tool_calls) {
|
||||
return {
|
||||
content: response.content || "",
|
||||
role: response.role,
|
||||
toolCalls: response.tool_calls.map((call) => ({
|
||||
name: call.function.name,
|
||||
arguments: JSON.stringify(call.function.arguments),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return response.content || "";
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
try {
|
||||
await this.ensureModelExists();
|
||||
} catch (err) {
|
||||
logger.error(`Error ensuring model exists: ${err}`);
|
||||
}
|
||||
|
||||
const completion = await this.ollama.chat({
|
||||
messages: messages.map((msg) => {
|
||||
const role = msg.role as "system" | "user" | "assistant";
|
||||
return {
|
||||
role,
|
||||
content:
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content),
|
||||
};
|
||||
}),
|
||||
model: this.model,
|
||||
});
|
||||
const response = completion.message;
|
||||
return {
|
||||
content: response.content || "",
|
||||
role: response.role,
|
||||
};
|
||||
}
|
||||
|
||||
private async ensureModelExists(): Promise<boolean> {
|
||||
if (this.initialized) {
|
||||
return true;
|
||||
}
|
||||
const local_models = await this.ollama.list();
|
||||
if (!local_models.models.find((m: any) => m.name === this.model)) {
|
||||
logger.info(`Pulling model ${this.model}...`);
|
||||
await this.ollama.pull({ model: this.model });
|
||||
}
|
||||
this.initialized = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,9 @@ export interface Message {
|
||||
}
|
||||
|
||||
export interface EmbeddingConfig {
|
||||
apiKey: string;
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface VectorStoreConfig {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OpenAIEmbedder } from "../embeddings/openai";
|
||||
import { OllamaEmbedder } from "../embeddings/ollama";
|
||||
import { OpenAILLM } from "../llms/openai";
|
||||
import { OpenAIStructuredLLM } from "../llms/openai_structured";
|
||||
import { AnthropicLLM } from "../llms/anthropic";
|
||||
@@ -10,12 +11,14 @@ import { LLM } from "../llms/base";
|
||||
import { VectorStore } from "../vector_stores/base";
|
||||
import { Qdrant } from "../vector_stores/qdrant";
|
||||
import { RedisDB } from "../vector_stores/redis";
|
||||
|
||||
import { OllamaLLM } from "../llms/ollama";
|
||||
export class EmbedderFactory {
|
||||
static create(provider: string, config: EmbeddingConfig): Embedder {
|
||||
switch (provider.toLowerCase()) {
|
||||
case "openai":
|
||||
return new OpenAIEmbedder(config);
|
||||
case "ollama":
|
||||
return new OllamaEmbedder(config);
|
||||
default:
|
||||
throw new Error(`Unsupported embedder provider: ${provider}`);
|
||||
}
|
||||
@@ -33,6 +36,8 @@ export class LLMFactory {
|
||||
return new AnthropicLLM(config);
|
||||
case "groq":
|
||||
return new GroqLLM(config);
|
||||
case "ollama":
|
||||
return new OllamaLLM(config);
|
||||
default:
|
||||
throw new Error(`Unsupported LLM provider: ${provider}`);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ interface RedisDocument {
|
||||
run_id?: string;
|
||||
user_id?: string;
|
||||
metadata?: string;
|
||||
vector_score?: number;
|
||||
__vector_score?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,6 +108,30 @@ const EXCLUDED_KEYS = new Set([
|
||||
"updated_at",
|
||||
]);
|
||||
|
||||
// Utility function to convert object keys to snake_case
|
||||
function toSnakeCase(obj: Record<string, any>): Record<string, any> {
|
||||
if (typeof obj !== "object" || obj === null) return obj;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([key, value]) => [
|
||||
key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`),
|
||||
value,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// Utility function to convert object keys to camelCase
|
||||
function toCamelCase(obj: Record<string, any>): Record<string, any> {
|
||||
if (typeof obj !== "object" || obj === null) return obj;
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([key, value]) => [
|
||||
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
|
||||
value,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export class RedisDB implements VectorStore {
|
||||
private client: RedisClientType<
|
||||
RedisDefaultModules & RedisModules & RedisFunctions & RedisScripts
|
||||
@@ -272,7 +296,7 @@ export class RedisDB implements VectorStore {
|
||||
payloads: Record<string, any>[],
|
||||
): Promise<void> {
|
||||
const data = vectors.map((vector, idx) => {
|
||||
const payload = payloads[idx];
|
||||
const payload = toSnakeCase(payloads[idx]);
|
||||
const id = ids[idx];
|
||||
|
||||
// Create entry with required fields
|
||||
@@ -322,8 +346,9 @@ export class RedisDB implements VectorStore {
|
||||
limit: number = 5,
|
||||
filters?: SearchFilters,
|
||||
): Promise<VectorStoreResult[]> {
|
||||
const filterExpr = filters
|
||||
? Object.entries(filters)
|
||||
const snakeFilters = filters ? toSnakeCase(filters) : undefined;
|
||||
const filterExpr = snakeFilters
|
||||
? Object.entries(snakeFilters)
|
||||
.filter(([_, value]) => value !== null)
|
||||
.map(([key, value]) => `@${key}:{${value}}`)
|
||||
.join(" ")
|
||||
@@ -344,8 +369,9 @@ export class RedisDB implements VectorStore {
|
||||
"memory",
|
||||
"metadata",
|
||||
"created_at",
|
||||
"__vector_score",
|
||||
],
|
||||
SORTBY: "vector_score",
|
||||
SORTBY: "__vector_score",
|
||||
DIALECT: 2,
|
||||
LIMIT: {
|
||||
from: 0,
|
||||
@@ -356,12 +382,12 @@ export class RedisDB implements VectorStore {
|
||||
try {
|
||||
const results = (await this.client.ft.search(
|
||||
this.indexName,
|
||||
`${filterExpr} =>[KNN ${limit} @embedding $vec AS vector_score]`,
|
||||
`${filterExpr} =>[KNN ${limit} @embedding $vec AS __vector_score]`,
|
||||
searchOptions,
|
||||
)) as unknown as RedisSearchResult;
|
||||
|
||||
return results.documents.map((doc) => {
|
||||
const payload = {
|
||||
const resultPayload = {
|
||||
hash: doc.value.hash,
|
||||
data: doc.value.memory,
|
||||
created_at: new Date(parseInt(doc.value.created_at)).toISOString(),
|
||||
@@ -376,8 +402,8 @@ export class RedisDB implements VectorStore {
|
||||
|
||||
return {
|
||||
id: doc.value.memory_id,
|
||||
payload,
|
||||
score: doc.value.vector_score,
|
||||
payload: toCamelCase(resultPayload),
|
||||
score: Number(doc.value.__vector_score) ?? 0,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -493,26 +519,27 @@ export class RedisDB implements VectorStore {
|
||||
vector: number[],
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> {
|
||||
const snakePayload = toSnakeCase(payload);
|
||||
const entry: Record<string, any> = {
|
||||
memory_id: vectorId,
|
||||
hash: payload.hash,
|
||||
memory: payload.data,
|
||||
created_at: new Date(payload.created_at).getTime(),
|
||||
updated_at: new Date(payload.updated_at).getTime(),
|
||||
hash: snakePayload.hash,
|
||||
memory: snakePayload.data,
|
||||
created_at: new Date(snakePayload.created_at).getTime(),
|
||||
updated_at: new Date(snakePayload.updated_at).getTime(),
|
||||
embedding: Buffer.from(new Float32Array(vector).buffer),
|
||||
};
|
||||
|
||||
// Add optional fields
|
||||
["agent_id", "run_id", "user_id"].forEach((field) => {
|
||||
if (field in payload) {
|
||||
entry[field] = payload[field];
|
||||
if (field in snakePayload) {
|
||||
entry[field] = snakePayload[field];
|
||||
}
|
||||
});
|
||||
|
||||
// Add metadata excluding specific keys
|
||||
entry.metadata = JSON.stringify(
|
||||
Object.fromEntries(
|
||||
Object.entries(payload).filter(([key]) => !EXCLUDED_KEYS.has(key)),
|
||||
Object.entries(snakePayload).filter(([key]) => !EXCLUDED_KEYS.has(key)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -557,8 +584,9 @@ export class RedisDB implements VectorStore {
|
||||
filters?: SearchFilters,
|
||||
limit: number = 100,
|
||||
): Promise<[VectorStoreResult[], number]> {
|
||||
const filterExpr = filters
|
||||
? Object.entries(filters)
|
||||
const snakeFilters = filters ? toSnakeCase(filters) : undefined;
|
||||
const filterExpr = snakeFilters
|
||||
? Object.entries(snakeFilters)
|
||||
.filter(([_, value]) => value !== null)
|
||||
.map(([key, value]) => `@${key}:{${value}}`)
|
||||
.join(" ")
|
||||
@@ -581,7 +609,7 @@ export class RedisDB implements VectorStore {
|
||||
|
||||
const items = results.documents.map((doc) => ({
|
||||
id: doc.value.memory_id,
|
||||
payload: {
|
||||
payload: toCamelCase({
|
||||
hash: doc.value.hash,
|
||||
data: doc.value.memory,
|
||||
created_at: new Date(parseInt(doc.value.created_at)).toISOString(),
|
||||
@@ -592,7 +620,7 @@ export class RedisDB implements VectorStore {
|
||||
...(doc.value.run_id && { run_id: doc.value.run_id }),
|
||||
...(doc.value.user_id && { user_id: doc.value.user_id }),
|
||||
...JSON.parse(doc.value.metadata || "{}"),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
return [items, results.total];
|
||||
|
||||
+22
-2
@@ -3,10 +3,11 @@ import os
|
||||
import warnings
|
||||
from functools import wraps
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from enum import Enum
|
||||
|
||||
import httpx
|
||||
|
||||
from mem0.memory.setup import setup_config, get_user_id
|
||||
from mem0.memory.setup import get_user_id, setup_config
|
||||
from mem0.memory.telemetry import capture_client_event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -110,7 +111,7 @@ class MemoryClient:
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
error_message = error_data.get("detail", str(e))
|
||||
except:
|
||||
except Exception:
|
||||
error_message = str(e)
|
||||
raise ValueError(f"Error: {error_message}")
|
||||
|
||||
@@ -998,3 +999,22 @@ class AsyncMemoryClient:
|
||||
response.raise_for_status()
|
||||
capture_client_event("async_client.delete_webhook", self.sync_client, {"webhook_id": webhook_id})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def feedback(self, memory_id: str, feedback: Optional[str] = None, feedback_reason: Optional[str] = None) -> Dict[str, str]:
|
||||
VALID_FEEDBACK_VALUES = {"POSITIVE", "NEGATIVE", "VERY_NEGATIVE"}
|
||||
|
||||
feedback = feedback.upper() if feedback else None
|
||||
if feedback is not None and feedback not in VALID_FEEDBACK_VALUES:
|
||||
raise ValueError(f'feedback must be one of {", ".join(VALID_FEEDBACK_VALUES)} or None')
|
||||
|
||||
data = {
|
||||
"memory_id": memory_id,
|
||||
"feedback": feedback,
|
||||
"feedback_reason": feedback_reason
|
||||
}
|
||||
|
||||
response = await self.async_client.post("/v1/feedback/", json=data)
|
||||
response.raise_for_status()
|
||||
capture_client_event("async_client.feedback", self.sync_client, data)
|
||||
return response.json()
|
||||
@@ -48,8 +48,12 @@ class MemoryConfig(BaseModel):
|
||||
description="The version of the API",
|
||||
default="v1.1",
|
||||
)
|
||||
custom_prompt: Optional[str] = Field(
|
||||
description="Custom prompt for the memory",
|
||||
custom_fact_extraction_prompt: Optional[str] = Field(
|
||||
description="Custom prompt for the fact extraction",
|
||||
default=None,
|
||||
)
|
||||
custom_update_memory_prompt: Optional[str] = Field(
|
||||
description="Custom prompt for the update memory",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ class BaseEmbedderConfig(ABC):
|
||||
memory_add_embedding_type: Optional[str] = None,
|
||||
memory_update_embedding_type: Optional[str] = None,
|
||||
memory_search_embedding_type: Optional[str] = None,
|
||||
# LM Studio specific
|
||||
lmstudio_base_url: Optional[str] = "http://localhost:1234/v1",
|
||||
):
|
||||
"""
|
||||
Initializes a configuration class instance for the Embeddings.
|
||||
@@ -58,6 +60,8 @@ class BaseEmbedderConfig(ABC):
|
||||
:type memory_update_embedding_type: Optional[str], optional
|
||||
:param memory_search_embedding_type: The type of embedding to use for the search memory action, defaults to None
|
||||
:type memory_search_embedding_type: Optional[str], optional
|
||||
:param lmstudio_base_url: LM Studio base URL to be use, defaults to "http://localhost:1234/v1"
|
||||
:type lmstudio_base_url: Optional[str], optional
|
||||
"""
|
||||
|
||||
self.model = model
|
||||
@@ -82,3 +86,6 @@ class BaseEmbedderConfig(ABC):
|
||||
self.memory_add_embedding_type = memory_add_embedding_type
|
||||
self.memory_update_embedding_type = memory_update_embedding_type
|
||||
self.memory_search_embedding_type = memory_search_embedding_type
|
||||
|
||||
# LM Studio specific
|
||||
self.lmstudio_base_url = lmstudio_base_url
|
||||
|
||||
@@ -39,6 +39,8 @@ class BaseLlmConfig(ABC):
|
||||
deepseek_base_url: Optional[str] = None,
|
||||
# XAI specific
|
||||
xai_base_url: Optional[str] = None,
|
||||
# LM Studio specific
|
||||
lmstudio_base_url: Optional[str] = "http://localhost:1234/v1",
|
||||
):
|
||||
"""
|
||||
Initializes a configuration class instance for the LLM.
|
||||
@@ -83,6 +85,8 @@ class BaseLlmConfig(ABC):
|
||||
:type deepseek_base_url: Optional[str], optional
|
||||
:param xai_base_url: XAI base URL to be use, defaults to None
|
||||
:type xai_base_url: Optional[str], optional
|
||||
:param lmstudio_base_url: LM Studio base URL to be use, defaults to "http://localhost:1234/v1"
|
||||
:type lmstudio_base_url: Optional[str], optional
|
||||
"""
|
||||
|
||||
self.model = model
|
||||
@@ -116,3 +120,6 @@ class BaseLlmConfig(ABC):
|
||||
|
||||
# XAI specific
|
||||
self.xai_base_url = xai_base_url
|
||||
|
||||
# LM Studio specific
|
||||
self.lmstudio_base_url = lmstudio_base_url
|
||||
|
||||
+165
-145
@@ -58,169 +58,189 @@ Following is a conversation between the user and the assistant. You have to extr
|
||||
You should detect the language of the user input and record the facts in the same language.
|
||||
"""
|
||||
|
||||
DEFAULT_UPDATE_MEMORY_PROMPT = """You are a smart memory manager which controls the memory of a system.
|
||||
You can perform four operations: (1) add into the memory, (2) update the memory, (3) delete from the memory, and (4) no change.
|
||||
|
||||
def get_update_memory_messages(retrieved_old_memory_dict, response_content):
|
||||
return f"""You are a smart memory manager which controls the memory of a system.
|
||||
You can perform four operations: (1) add into the memory, (2) update the memory, (3) delete from the memory, and (4) no change.
|
||||
Based on the above four operations, the memory will change.
|
||||
|
||||
Based on the above four operations, the memory will change.
|
||||
Compare newly retrieved facts with the existing memory. For each new fact, decide whether to:
|
||||
- ADD: Add it to the memory as a new element
|
||||
- UPDATE: Update an existing memory element
|
||||
- DELETE: Delete an existing memory element
|
||||
- NONE: Make no change (if the fact is already present or irrelevant)
|
||||
|
||||
Compare newly retrieved facts with the existing memory. For each new fact, decide whether to:
|
||||
- ADD: Add it to the memory as a new element
|
||||
- UPDATE: Update an existing memory element
|
||||
- DELETE: Delete an existing memory element
|
||||
- NONE: Make no change (if the fact is already present or irrelevant)
|
||||
There are specific guidelines to select which operation to perform:
|
||||
|
||||
There are specific guidelines to select which operation to perform:
|
||||
1. **Add**: If the retrieved facts contain new information not present in the memory, then you have to add it by generating a new ID in the id field.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "User is a software engineer"
|
||||
}
|
||||
]
|
||||
- Retrieved facts: ["Name is John"]
|
||||
- New Memory:
|
||||
{
|
||||
"memory" : [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "User is a software engineer",
|
||||
"event" : "NONE"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Name is John",
|
||||
"event" : "ADD"
|
||||
}
|
||||
]
|
||||
|
||||
1. **Add**: If the retrieved facts contain new information not present in the memory, then you have to add it by generating a new ID in the id field.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{{
|
||||
"id" : "0",
|
||||
"text" : "User is a software engineer"
|
||||
}}
|
||||
]
|
||||
- Retrieved facts: ["Name is John"]
|
||||
- New Memory:
|
||||
{{
|
||||
"memory" : [
|
||||
{{
|
||||
"id" : "0",
|
||||
"text" : "User is a software engineer",
|
||||
"event" : "NONE"
|
||||
}},
|
||||
{{
|
||||
"id" : "1",
|
||||
"text" : "Name is John",
|
||||
"event" : "ADD"
|
||||
}}
|
||||
]
|
||||
}
|
||||
|
||||
}}
|
||||
|
||||
2. **Update**: If the retrieved facts contain information that is already present in the memory but the information is totally different, then you have to update it.
|
||||
If the retrieved fact contains information that conveys the same thing as the elements present in the memory, then you have to keep the fact which has the most information.
|
||||
Example (a) -- if the memory contains "User likes to play cricket" and the retrieved fact is "Loves to play cricket with friends", then update the memory with the retrieved facts.
|
||||
Example (b) -- if the memory contains "Likes cheese pizza" and the retrieved fact is "Loves cheese pizza", then you do not need to update it because they convey the same information.
|
||||
If the direction is to update the memory, then you have to update it.
|
||||
Please keep in mind while updating you have to keep the same ID.
|
||||
Please note to return the IDs in the output from the input IDs only and do not generate any new ID.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{{
|
||||
"id" : "0",
|
||||
"text" : "I really like cheese pizza"
|
||||
}},
|
||||
{{
|
||||
"id" : "1",
|
||||
"text" : "User is a software engineer"
|
||||
}},
|
||||
{{
|
||||
"id" : "2",
|
||||
"text" : "User likes to play cricket"
|
||||
}}
|
||||
]
|
||||
- Retrieved facts: ["Loves chicken pizza", "Loves to play cricket with friends"]
|
||||
- New Memory:
|
||||
{{
|
||||
"memory" : [
|
||||
{{
|
||||
"id" : "0",
|
||||
"text" : "Loves cheese and chicken pizza",
|
||||
"event" : "UPDATE",
|
||||
"old_memory" : "I really like cheese pizza"
|
||||
}},
|
||||
{{
|
||||
"id" : "1",
|
||||
"text" : "User is a software engineer",
|
||||
"event" : "NONE"
|
||||
}},
|
||||
{{
|
||||
"id" : "2",
|
||||
"text" : "Loves to play cricket with friends",
|
||||
"event" : "UPDATE",
|
||||
"old_memory" : "User likes to play cricket"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
2. **Update**: If the retrieved facts contain information that is already present in the memory but the information is totally different, then you have to update it.
|
||||
If the retrieved fact contains information that conveys the same thing as the elements present in the memory, then you have to keep the fact which has the most information.
|
||||
Example (a) -- if the memory contains "User likes to play cricket" and the retrieved fact is "Loves to play cricket with friends", then update the memory with the retrieved facts.
|
||||
Example (b) -- if the memory contains "Likes cheese pizza" and the retrieved fact is "Loves cheese pizza", then you do not need to update it because they convey the same information.
|
||||
If the direction is to update the memory, then you have to update it.
|
||||
Please keep in mind while updating you have to keep the same ID.
|
||||
Please note to return the IDs in the output from the input IDs only and do not generate any new ID.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "I really like cheese pizza"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "User is a software engineer"
|
||||
},
|
||||
{
|
||||
"id" : "2",
|
||||
"text" : "User likes to play cricket"
|
||||
}
|
||||
]
|
||||
- Retrieved facts: ["Loves chicken pizza", "Loves to play cricket with friends"]
|
||||
- New Memory:
|
||||
{
|
||||
"memory" : [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Loves cheese and chicken pizza",
|
||||
"event" : "UPDATE",
|
||||
"old_memory" : "I really like cheese pizza"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "User is a software engineer",
|
||||
"event" : "NONE"
|
||||
},
|
||||
{
|
||||
"id" : "2",
|
||||
"text" : "Loves to play cricket with friends",
|
||||
"event" : "UPDATE",
|
||||
"old_memory" : "User likes to play cricket"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
3. **Delete**: If the retrieved facts contain information that contradicts the information present in the memory, then you have to delete it. Or if the direction is to delete the memory, then you have to delete it.
|
||||
Please note to return the IDs in the output from the input IDs only and do not generate any new ID.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{{
|
||||
"id" : "0",
|
||||
"text" : "Name is John"
|
||||
}},
|
||||
{{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza"
|
||||
}}
|
||||
]
|
||||
- Retrieved facts: ["Dislikes cheese pizza"]
|
||||
- New Memory:
|
||||
{{
|
||||
"memory" : [
|
||||
{{
|
||||
"id" : "0",
|
||||
"text" : "Name is John",
|
||||
"event" : "NONE"
|
||||
}},
|
||||
{{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza",
|
||||
"event" : "DELETE"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
3. **Delete**: If the retrieved facts contain information that contradicts the information present in the memory, then you have to delete it. Or if the direction is to delete the memory, then you have to delete it.
|
||||
Please note to return the IDs in the output from the input IDs only and do not generate any new ID.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Name is John"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza"
|
||||
}
|
||||
]
|
||||
- Retrieved facts: ["Dislikes cheese pizza"]
|
||||
- New Memory:
|
||||
{
|
||||
"memory" : [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Name is John",
|
||||
"event" : "NONE"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza",
|
||||
"event" : "DELETE"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
4. **No Change**: If the retrieved facts contain information that is already present in the memory, then you do not need to make any changes.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{{
|
||||
"id" : "0",
|
||||
"text" : "Name is John"
|
||||
}},
|
||||
{{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza"
|
||||
}}
|
||||
]
|
||||
- Retrieved facts: ["Name is John"]
|
||||
- New Memory:
|
||||
{{
|
||||
"memory" : [
|
||||
{{
|
||||
"id" : "0",
|
||||
"text" : "Name is John",
|
||||
"event" : "NONE"
|
||||
}},
|
||||
{{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza",
|
||||
"event" : "NONE"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
4. **No Change**: If the retrieved facts contain information that is already present in the memory, then you do not need to make any changes.
|
||||
- **Example**:
|
||||
- Old Memory:
|
||||
[
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Name is John"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza"
|
||||
}
|
||||
]
|
||||
- Retrieved facts: ["Name is John"]
|
||||
- New Memory:
|
||||
{
|
||||
"memory" : [
|
||||
{
|
||||
"id" : "0",
|
||||
"text" : "Name is John",
|
||||
"event" : "NONE"
|
||||
},
|
||||
{
|
||||
"id" : "1",
|
||||
"text" : "Loves cheese pizza",
|
||||
"event" : "NONE"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
def get_update_memory_messages(retrieved_old_memory_dict, response_content, custom_update_memory_prompt=None):
|
||||
if custom_update_memory_prompt is None:
|
||||
global DEFAULT_UPDATE_MEMORY_PROMPT
|
||||
custom_update_memory_prompt = DEFAULT_UPDATE_MEMORY_PROMPT
|
||||
|
||||
return f"""{custom_update_memory_prompt}
|
||||
|
||||
Below is the current content of my memory which I have collected till now. You have to update it in the following format only:
|
||||
|
||||
``
|
||||
```
|
||||
{retrieved_old_memory_dict}
|
||||
``
|
||||
```
|
||||
|
||||
The new retrieved facts are mentioned in the triple backticks. You have to analyze the new retrieved facts and determine whether these facts should be added, updated, or deleted in the memory.
|
||||
|
||||
```
|
||||
{response_content}
|
||||
```
|
||||
|
||||
|
||||
You must return your response in the following JSON structure only:
|
||||
|
||||
{{
|
||||
"memory" : [
|
||||
{{
|
||||
"id" : "<ID of the memory>", # Use existing ID for updates/deletes, or new ID for additions
|
||||
"text" : "<Content of the memory>", # Content of the memory
|
||||
"event" : "<Operation to be performed>", # Must be "ADD", "UPDATE", "DELETE", or "NONE"
|
||||
"old_memory" : "<Old memory content>" # Required only if the event is "UPDATE"
|
||||
}},
|
||||
...
|
||||
]
|
||||
}}
|
||||
|
||||
Follow the instruction mentioned below:
|
||||
- Do not return anything from the custom few shot prompts provided above.
|
||||
- If the current memory is empty, then you have to add the new retrieved facts to the memory.
|
||||
@@ -230,4 +250,4 @@ def get_update_memory_messages(retrieved_old_memory_dict, response_content):
|
||||
- If there is an update, the ID key should remain the same and only the value needs to be updated.
|
||||
|
||||
Do not return anything except the JSON format.
|
||||
"""
|
||||
"""
|
||||
@@ -1,14 +1,25 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class AzureAISearchConfig(BaseModel):
|
||||
collection_name: str = Field("mem0", description="Name of the collection")
|
||||
service_name: str = Field(None, description="Azure Cognitive Search service name")
|
||||
api_key: str = Field(None, description="API key for the Azure Cognitive Search service")
|
||||
service_name: str = Field(None, description="Azure AI Search service name")
|
||||
api_key: str = Field(None, description="API key for the Azure AI Search service")
|
||||
embedding_model_dims: int = Field(None, description="Dimension of the embedding vector")
|
||||
use_compression: bool = Field(False, description="Whether to use scalar quantization vector compression.")
|
||||
compression_type: Optional[str] = Field(
|
||||
None, description="Type of vector compression to use. Options: 'scalar', 'binary', or None"
|
||||
)
|
||||
use_float16: bool = Field(
|
||||
False,
|
||||
description="Whether to store vectors in half precision (Edm.Half) instead of full precision (Edm.Single)",
|
||||
)
|
||||
hybrid_search: bool = Field(
|
||||
False, description="Whether to use hybrid search. If True, vector_filter_mode must be 'preFilter'"
|
||||
)
|
||||
vector_filter_mode: Optional[str] = Field(
|
||||
"preFilter", description="Mode for vector filtering. Options: 'preFilter', 'postFilter'"
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -16,10 +27,30 @@ class AzureAISearchConfig(BaseModel):
|
||||
allowed_fields = set(cls.model_fields.keys())
|
||||
input_fields = set(values.keys())
|
||||
extra_fields = input_fields - allowed_fields
|
||||
|
||||
# Check for use_compression to provide a helpful error
|
||||
if "use_compression" in extra_fields:
|
||||
raise ValueError(
|
||||
"The parameter 'use_compression' is no longer supported. "
|
||||
"Please use 'compression_type=\"scalar\"' instead of 'use_compression=True' "
|
||||
"or 'compression_type=None' instead of 'use_compression=False'."
|
||||
)
|
||||
|
||||
if extra_fields:
|
||||
raise ValueError(
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. "
|
||||
f"Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
|
||||
# Validate compression_type values
|
||||
if "compression_type" in values and values["compression_type"] is not None:
|
||||
valid_types = ["scalar", "binary"]
|
||||
if values["compression_type"].lower() not in valid_types:
|
||||
raise ValueError(
|
||||
f"Invalid compression_type: {values['compression_type']}. "
|
||||
f"Must be one of: {', '.join(valid_types)}, or None"
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
model_config = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
@@ -15,6 +16,9 @@ class ElasticsearchConfig(BaseModel):
|
||||
verify_certs: bool = Field(True, description="Verify SSL certificates")
|
||||
use_ssl: bool = Field(True, description="Use SSL for connection")
|
||||
auto_create_index: bool = Field(True, description="Automatically create index during initialization")
|
||||
custom_search_query: Optional[Callable[[List[float], int, Optional[Dict]], Dict]] = Field(
|
||||
None, description="Custom search query function. Parameters: (query, limit, filters) -> Dict"
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
|
||||
@@ -14,6 +14,7 @@ class OpenSearchConfig(BaseModel):
|
||||
verify_certs: bool = Field(False, description="Verify SSL certificates (default False for OpenSearch)")
|
||||
use_ssl: bool = Field(False, description="Use SSL for connection (default False for OpenSearch)")
|
||||
auto_create_index: bool = Field(True, description="Automatically create index during initialization")
|
||||
http_auth: Optional[object] = Field(None, description="HTTP authentication method / AWS SigV4")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -23,7 +24,7 @@ class OpenSearchConfig(BaseModel):
|
||||
raise ValueError("Host must be provided for OpenSearch")
|
||||
|
||||
# Authentication: Either API key or user/password must be provided
|
||||
if not any([values.get("api_key"), (values.get("user") and values.get("password"))]):
|
||||
if not any([values.get("api_key"), (values.get("user") and values.get("password")), values.get("http_auth")]):
|
||||
raise ValueError("Either api_key or user/password must be provided for OpenSearch authentication")
|
||||
|
||||
return values
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class PineconeConfig(BaseModel):
|
||||
"""Configuration for Pinecone vector database."""
|
||||
|
||||
collection_name: str = Field("mem0", description="Name of the index/collection")
|
||||
embedding_model_dims: int = Field(1536, description="Dimensions of the embedding model")
|
||||
client: Optional[Any] = Field(None, description="Existing Pinecone client instance")
|
||||
api_key: Optional[str] = Field(None, description="API key for Pinecone")
|
||||
environment: Optional[str] = Field(None, description="Pinecone environment")
|
||||
serverless_config: Optional[Dict[str, Any]] = Field(None, description="Configuration for serverless deployment")
|
||||
pod_config: Optional[Dict[str, Any]] = Field(None, description="Configuration for pod-based deployment")
|
||||
hybrid_search: bool = Field(False, description="Whether to enable hybrid search")
|
||||
metric: str = Field("cosine", description="Distance metric for vector similarity")
|
||||
batch_size: int = Field(100, description="Batch size for operations")
|
||||
extra_params: Optional[Dict[str, Any]] = Field(None, description="Additional parameters for Pinecone client")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_api_key_or_client(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
api_key, client = values.get("api_key"), values.get("client")
|
||||
if not api_key and not client and "PINECONE_API_KEY" not in os.environ:
|
||||
raise ValueError(
|
||||
"Either 'api_key' or 'client' must be provided, or PINECONE_API_KEY environment variable must be set."
|
||||
)
|
||||
return values
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_pod_or_serverless(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
pod_config, serverless_config = values.get("pod_config"), values.get("serverless_config")
|
||||
if pod_config and serverless_config:
|
||||
raise ValueError(
|
||||
"Both 'pod_config' and 'serverless_config' cannot be specified. Choose one deployment option."
|
||||
)
|
||||
return values
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
allowed_fields = set(cls.model_fields.keys())
|
||||
input_fields = set(values.keys())
|
||||
extra_fields = input_fields - allowed_fields
|
||||
if extra_fields:
|
||||
raise ValueError(
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = {
|
||||
"arbitrary_types_allowed": True,
|
||||
}
|
||||
@@ -14,9 +14,7 @@ class GoogleMatchingEngineConfig(BaseModel):
|
||||
credentials_path: Optional[str] = Field(None, description="Path to service account credentials file")
|
||||
vector_search_api_endpoint: Optional[str] = Field(None, description="Vector search API endpoint")
|
||||
|
||||
model_config = {
|
||||
"extra": "forbid"
|
||||
}
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -26,4 +24,4 @@ class GoogleMatchingEngineConfig(BaseModel):
|
||||
def model_post_init(self, _context) -> None:
|
||||
"""Set collection_name to index_id if not provided"""
|
||||
if self.collection_name is None:
|
||||
self.collection_name = self.index_id
|
||||
self.collection_name = self.index_id
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import Any, ClassVar, Dict, Optional
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class WeaviateConfig(BaseModel):
|
||||
from weaviate import WeaviateClient
|
||||
|
||||
WeaviateClient: ClassVar[type] = WeaviateClient
|
||||
|
||||
collection_name: str = Field("mem0", description="Name of the collection")
|
||||
embedding_model_dims: int = Field(1536, description="Dimensions of the embedding model")
|
||||
cluster_url: Optional[str] = Field(None, description="URL for Weaviate server")
|
||||
auth_client_secret: Optional[str] = Field(None, description="API key for Weaviate authentication")
|
||||
additional_headers: Optional[Dict[str, str]] = Field(None, description="Additional headers for requests")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_connection_params(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
cluster_url = values.get("cluster_url")
|
||||
|
||||
if not cluster_url:
|
||||
raise ValueError("'cluster_url' must be provided.")
|
||||
|
||||
return values
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
allowed_fields = set(cls.model_fields.keys())
|
||||
input_fields = set(values.keys())
|
||||
extra_fields = input_fields - allowed_fields
|
||||
|
||||
if extra_fields:
|
||||
raise ValueError(
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
model_config = {
|
||||
"arbitrary_types_allowed": True,
|
||||
}
|
||||
@@ -13,7 +13,7 @@ class EmbedderConfig(BaseModel):
|
||||
@field_validator("config")
|
||||
def validate_config(cls, v, values):
|
||||
provider = values.data.get("provider")
|
||||
if provider in ["openai", "ollama", "huggingface", "azure_openai", "gemini", "vertexai", "together"]:
|
||||
if provider in ["openai", "ollama", "huggingface", "azure_openai", "gemini", "vertexai", "together", "lmstudio"]:
|
||||
return v
|
||||
else:
|
||||
raise ValueError(f"Unsupported embedding provider: {provider}")
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import Literal, Optional
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from mem0.configs.embeddings.base import BaseEmbedderConfig
|
||||
from mem0.embeddings.base import EmbeddingBase
|
||||
|
||||
|
||||
class LMStudioEmbedding(EmbeddingBase):
|
||||
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
|
||||
super().__init__(config)
|
||||
|
||||
self.config.model = self.config.model or "nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf"
|
||||
self.config.embedding_dims = self.config.embedding_dims or 1536
|
||||
self.config.api_key = self.config.api_key or "lm-studio"
|
||||
|
||||
self.client = OpenAI(base_url=self.config.lmstudio_base_url, api_key=self.config.api_key)
|
||||
|
||||
def embed(self, text, memory_action: Optional[Literal["add", "search", "update"]] = None):
|
||||
"""
|
||||
Get the embedding for the given text using LM Studio.
|
||||
Args:
|
||||
text (str): The text to embed.
|
||||
memory_action (optional): The type of embedding to use. Must be one of "add", "search", or "update". Defaults to None.
|
||||
Returns:
|
||||
list: The embedding vector.
|
||||
"""
|
||||
text = text.replace("\n", " ")
|
||||
return (
|
||||
self.client.embeddings.create(input=[text], model=self.config.model)
|
||||
.data[0]
|
||||
.embedding
|
||||
)
|
||||
@@ -32,51 +32,17 @@ class AzureOpenAIStructuredLLM(LLMBase):
|
||||
default_headers=default_headers,
|
||||
)
|
||||
|
||||
def _parse_response(self, response, tools):
|
||||
"""
|
||||
Process the response based on whether tools are used or not.
|
||||
|
||||
Args:
|
||||
response: The raw response from API.
|
||||
tools: The list of tools provided in the request.
|
||||
|
||||
Returns:
|
||||
str or dict: The processed response.
|
||||
"""
|
||||
if tools:
|
||||
processed_response = {
|
||||
"content": response.choices[0].message.content,
|
||||
"tool_calls": [],
|
||||
}
|
||||
|
||||
if response.choices[0].message.tool_calls:
|
||||
for tool_call in response.choices[0].message.tool_calls:
|
||||
processed_response["tool_calls"].append(
|
||||
{
|
||||
"name": tool_call.function.name,
|
||||
"arguments": json.loads(tool_call.function.arguments),
|
||||
}
|
||||
)
|
||||
|
||||
return processed_response
|
||||
else:
|
||||
return response.choices[0].message.content
|
||||
|
||||
def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
response_format=None,
|
||||
tools: Optional[List[Dict]] = None,
|
||||
tool_choice: str = "auto",
|
||||
):
|
||||
response_format: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a response based on the given messages using Azure OpenAI.
|
||||
|
||||
Args:
|
||||
messages (list): List of message dicts containing 'role' and 'content'.
|
||||
response_format (str or object, optional): Format of the response. Defaults to "text".
|
||||
tools (list, optional): List of tools that the model can call. Defaults to None.
|
||||
tool_choice (str, optional): Tool choice method. Defaults to "auto".
|
||||
messages (List[Dict[str, str]]): A list of dictionaries, each containing a 'role' and 'content' key.
|
||||
response_format (Optional[str]): The desired format of the response. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: The generated response.
|
||||
@@ -94,5 +60,9 @@ class AzureOpenAIStructuredLLM(LLMBase):
|
||||
params["tools"] = tools
|
||||
params["tool_choice"] = tool_choice
|
||||
|
||||
if tools:
|
||||
params["tools"] = tools
|
||||
params["tool_choice"] = tool_choice
|
||||
|
||||
response = self.client.chat.completions.create(**params)
|
||||
return self._parse_response(response, tools)
|
||||
|
||||
@@ -24,6 +24,7 @@ class LlmConfig(BaseModel):
|
||||
"gemini",
|
||||
"deepseek",
|
||||
"xai",
|
||||
"lmstudio",
|
||||
):
|
||||
return v
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from mem0.configs.llms.base import BaseLlmConfig
|
||||
from mem0.llms.base import LLMBase
|
||||
|
||||
|
||||
class LMStudioLLM(LLMBase):
|
||||
def __init__(self, config: Optional[BaseLlmConfig] = None):
|
||||
super().__init__(config)
|
||||
|
||||
self.config.model = self.config.model or "lmstudio-community/Meta-Llama-3.1-70B-Instruct-GGUF/Meta-Llama-3.1-70B-Instruct-IQ2_M.gguf"
|
||||
self.config.api_key = self.config.api_key or "lm-studio"
|
||||
|
||||
self.client = OpenAI(base_url=self.config.lmstudio_base_url, api_key=self.config.api_key)
|
||||
|
||||
def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
response_format: dict = {"type": "json_object"},
|
||||
tools: Optional[List[Dict]] = None,
|
||||
tool_choice: str = "auto"
|
||||
):
|
||||
"""
|
||||
Generate a response based on the given messages using LM Studio.
|
||||
|
||||
Args:
|
||||
messages (list): List of message dicts containing 'role' and 'content'.
|
||||
response_format (str or object, optional): Format of the response. Defaults to "text".
|
||||
tools (list, optional): List of tools that the model can call. Defaults to None.
|
||||
tool_choice (str, optional): Tool choice method. Defaults to "auto".
|
||||
|
||||
Returns:
|
||||
str: The generated response.
|
||||
"""
|
||||
params = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"temperature": self.config.temperature,
|
||||
"max_tokens": self.config.max_tokens,
|
||||
"top_p": self.config.top_p
|
||||
}
|
||||
if response_format:
|
||||
params["response_format"] = response_format
|
||||
|
||||
response = self.client.chat.completions.create(**params)
|
||||
return response.choices[0].message.content
|
||||
@@ -19,53 +19,18 @@ class OpenAIStructuredLLM(LLMBase):
|
||||
base_url = self.config.openai_base_url or os.getenv("OPENAI_API_BASE") or "https://api.openai.com/v1"
|
||||
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
||||
|
||||
def _parse_response(self, response, tools):
|
||||
"""
|
||||
Process the response based on whether tools are used or not.
|
||||
|
||||
Args:
|
||||
response: The raw response from API.
|
||||
tools (list, optional): List of tools that the model can call.
|
||||
|
||||
Returns:
|
||||
str or dict: The processed response.
|
||||
"""
|
||||
|
||||
if tools:
|
||||
processed_response = {
|
||||
"content": response.choices[0].message.content,
|
||||
"tool_calls": [],
|
||||
}
|
||||
|
||||
if response.choices[0].message.tool_calls:
|
||||
for tool_call in response.choices[0].message.tool_calls:
|
||||
processed_response["tool_calls"].append(
|
||||
{
|
||||
"name": tool_call.function.name,
|
||||
"arguments": json.loads(tool_call.function.arguments),
|
||||
}
|
||||
)
|
||||
|
||||
return processed_response
|
||||
|
||||
else:
|
||||
return response.choices[0].message.content
|
||||
|
||||
def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
response_format=None,
|
||||
tools: Optional[List[Dict]] = None,
|
||||
tool_choice: str = "auto",
|
||||
):
|
||||
response_format: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a response based on the given messages using OpenAI.
|
||||
|
||||
Args:
|
||||
messages (list): List of message dicts containing 'role' and 'content'.
|
||||
response_format (str or object, optional): Format of the response. Defaults to "text".
|
||||
tools (list, optional): List of tools that the model can call. Defaults to None.
|
||||
tool_choice (str, optional): Tool choice method. Defaults to "auto".
|
||||
messages (List[Dict[str, str]]): A list of dictionaries, each containing a 'role' and 'content' key.
|
||||
response_format (Optional[str]): The desired format of the response. Defaults to None.
|
||||
|
||||
|
||||
Returns:
|
||||
str: The generated response.
|
||||
@@ -83,5 +48,4 @@ class OpenAIStructuredLLM(LLMBase):
|
||||
params["tool_choice"] = tool_choice
|
||||
|
||||
response = self.client.beta.chat.completions.parse(**params)
|
||||
|
||||
return self._parse_response(response, tools)
|
||||
return response.choices[0].message.content
|
||||
|
||||
@@ -3,23 +3,20 @@ import logging
|
||||
from mem0.memory.utils import format_entities
|
||||
|
||||
try:
|
||||
from langchain_community.graphs import Neo4jGraph
|
||||
from langchain_neo4j import Neo4jGraph
|
||||
except ImportError:
|
||||
raise ImportError("langchain_community is not installed. Please install it using pip install langchain-community")
|
||||
raise ImportError("langchain_neo4j is not installed. Please install it using pip install langchain-neo4j")
|
||||
|
||||
try:
|
||||
from rank_bm25 import BM25Okapi
|
||||
except ImportError:
|
||||
raise ImportError("rank_bm25 is not installed. Please install it using pip install rank-bm25")
|
||||
|
||||
from mem0.graphs.tools import (
|
||||
DELETE_MEMORY_STRUCT_TOOL_GRAPH,
|
||||
DELETE_MEMORY_TOOL_GRAPH,
|
||||
EXTRACT_ENTITIES_STRUCT_TOOL,
|
||||
EXTRACT_ENTITIES_TOOL,
|
||||
RELATIONS_STRUCT_TOOL,
|
||||
RELATIONS_TOOL,
|
||||
)
|
||||
from mem0.graphs.tools import (DELETE_MEMORY_STRUCT_TOOL_GRAPH,
|
||||
DELETE_MEMORY_TOOL_GRAPH,
|
||||
EXTRACT_ENTITIES_STRUCT_TOOL,
|
||||
EXTRACT_ENTITIES_TOOL, RELATIONS_STRUCT_TOOL,
|
||||
RELATIONS_TOOL)
|
||||
from mem0.graphs.utils import EXTRACT_RELATIONS_PROMPT, get_delete_messages
|
||||
from mem0.utils.factory import EmbedderFactory, LlmFactory
|
||||
|
||||
|
||||
+43
-18
@@ -34,7 +34,8 @@ class Memory(MemoryBase):
|
||||
def __init__(self, config: MemoryConfig = MemoryConfig()):
|
||||
self.config = config
|
||||
|
||||
self.custom_prompt = self.config.custom_prompt
|
||||
self.custom_fact_extraction_prompt = self.config.custom_fact_extraction_prompt
|
||||
self.custom_update_memory_prompt = self.config.custom_update_memory_prompt
|
||||
self.embedding_model = EmbedderFactory.create(self.config.embedder.provider, self.config.embedder.config)
|
||||
self.vector_store = VectorStoreFactory.create(
|
||||
self.config.vector_store.provider, self.config.vector_store.config
|
||||
@@ -57,12 +58,28 @@ class Memory(MemoryBase):
|
||||
@classmethod
|
||||
def from_config(cls, config_dict: Dict[str, Any]):
|
||||
try:
|
||||
config = cls._process_config(config_dict)
|
||||
config = MemoryConfig(**config_dict)
|
||||
except ValidationError as e:
|
||||
logger.error(f"Configuration validation error: {e}")
|
||||
raise
|
||||
return cls(config)
|
||||
|
||||
@staticmethod
|
||||
def _process_config(config_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if "graph_store" in config_dict:
|
||||
if "vector_store" not in config_dict and "embedder" in config_dict:
|
||||
config_dict["vector_store"] = {}
|
||||
config_dict["vector_store"]["config"] = {}
|
||||
config_dict["vector_store"]["config"]["embedding_model_dims"] = config_dict["embedder"]["config"][
|
||||
"embedding_dims"
|
||||
]
|
||||
try:
|
||||
return config_dict
|
||||
except ValidationError as e:
|
||||
logger.error(f"Configuration validation error: {e}")
|
||||
raise
|
||||
|
||||
def add(
|
||||
self,
|
||||
messages,
|
||||
@@ -71,6 +88,7 @@ class Memory(MemoryBase):
|
||||
run_id=None,
|
||||
metadata=None,
|
||||
filters=None,
|
||||
infer=True,
|
||||
prompt=None,
|
||||
):
|
||||
"""
|
||||
@@ -83,6 +101,7 @@ class Memory(MemoryBase):
|
||||
run_id (str, optional): ID of the run creating the memory. Defaults to None.
|
||||
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
||||
filters (dict, optional): Filters to apply to the search. Defaults to None.
|
||||
infer (bool, optional): Whether to infer the memories. Defaults to True.
|
||||
prompt (str, optional): Prompt to use for memory deduction. Defaults to None.
|
||||
|
||||
Returns:
|
||||
@@ -121,7 +140,7 @@ class Memory(MemoryBase):
|
||||
messages = parse_vision_messages(messages)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future1 = executor.submit(self._add_to_vector_store, messages, metadata, filters)
|
||||
future1 = executor.submit(self._add_to_vector_store, messages, metadata, filters, infer)
|
||||
future2 = executor.submit(self._add_to_graph, messages, filters)
|
||||
|
||||
concurrent.futures.wait([future1, future2])
|
||||
@@ -147,11 +166,20 @@ class Memory(MemoryBase):
|
||||
|
||||
return {"results": vector_store_result}
|
||||
|
||||
def _add_to_vector_store(self, messages, metadata, filters):
|
||||
def _add_to_vector_store(self, messages, metadata, filters, infer):
|
||||
if not infer:
|
||||
returned_memories = []
|
||||
for message in messages:
|
||||
if message["role"] != "system":
|
||||
message_embeddings = self.embedding_model.embed(message["content"], "add")
|
||||
memory_id = self._create_memory(message["content"], message_embeddings, metadata)
|
||||
returned_memories.append({"id": memory_id, "memory": message["content"], "event": "ADD"})
|
||||
return returned_memories
|
||||
|
||||
parsed_messages = parse_messages(messages)
|
||||
|
||||
if self.custom_prompt:
|
||||
system_prompt = self.custom_prompt
|
||||
if self.custom_fact_extraction_prompt:
|
||||
system_prompt = self.custom_fact_extraction_prompt
|
||||
user_prompt = f"Input:\n{parsed_messages}"
|
||||
else:
|
||||
system_prompt, user_prompt = get_fact_retrieval_messages(parsed_messages)
|
||||
@@ -177,7 +205,8 @@ class Memory(MemoryBase):
|
||||
messages_embeddings = self.embedding_model.embed(new_mem, "add")
|
||||
new_message_embeddings[new_mem] = messages_embeddings
|
||||
existing_memories = self.vector_store.search(
|
||||
query=messages_embeddings,
|
||||
query=new_mem,
|
||||
vectors=messages_embeddings,
|
||||
limit=5,
|
||||
filters=filters,
|
||||
)
|
||||
@@ -195,7 +224,9 @@ class Memory(MemoryBase):
|
||||
temp_uuid_mapping[str(idx)] = item["id"]
|
||||
retrieved_old_memory[idx]["id"] = str(idx)
|
||||
|
||||
function_calling_prompt = get_update_memory_messages(retrieved_old_memory, new_retrieved_facts)
|
||||
function_calling_prompt = get_update_memory_messages(
|
||||
retrieved_old_memory, new_retrieved_facts, self.custom_update_memory_prompt
|
||||
)
|
||||
|
||||
try:
|
||||
new_memories_with_actions = self.llm.generate_response(
|
||||
@@ -305,15 +336,7 @@ class Memory(MemoryBase):
|
||||
).model_dump(exclude={"score"})
|
||||
|
||||
# Add metadata if there are additional keys
|
||||
excluded_keys = {
|
||||
"user_id",
|
||||
"agent_id",
|
||||
"run_id",
|
||||
"hash",
|
||||
"data",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
excluded_keys = {"user_id", "agent_id", "run_id", "hash", "data", "created_at", "updated_at", "id"}
|
||||
additional_metadata = {k: v for k, v in memory.payload.items() if k not in excluded_keys}
|
||||
if additional_metadata:
|
||||
memory_item["metadata"] = additional_metadata
|
||||
@@ -376,6 +399,7 @@ class Memory(MemoryBase):
|
||||
"data",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"id",
|
||||
}
|
||||
all_memories = [
|
||||
{
|
||||
@@ -459,7 +483,7 @@ class Memory(MemoryBase):
|
||||
|
||||
def _search_vector_store(self, query, filters, limit):
|
||||
embeddings = self.embedding_model.embed(query, "search")
|
||||
memories = self.vector_store.search(query=embeddings, limit=limit, filters=filters)
|
||||
memories = self.vector_store.search(query=query, vectors=embeddings, limit=limit, filters=filters)
|
||||
|
||||
excluded_keys = {
|
||||
"user_id",
|
||||
@@ -469,6 +493,7 @@ class Memory(MemoryBase):
|
||||
"data",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"id",
|
||||
}
|
||||
|
||||
original_memories = [
|
||||
@@ -655,4 +680,4 @@ class Memory(MemoryBase):
|
||||
capture_event("mem0.reset", self)
|
||||
|
||||
def chat(self, query):
|
||||
raise NotImplementedError("Chat function not implemented yet.")
|
||||
raise NotImplementedError("Chat function not implemented yet.")
|
||||
|
||||
@@ -25,6 +25,7 @@ class LlmFactory:
|
||||
"gemini": "mem0.llms.gemini.GeminiLLM",
|
||||
"deepseek": "mem0.llms.deepseek.DeepSeekLLM",
|
||||
"xai": "mem0.llms.xai.XAILLM",
|
||||
"lmstudio": "mem0.llms.lmstudio.LMStudioLLM",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -47,6 +48,7 @@ class EmbedderFactory:
|
||||
"gemini": "mem0.embeddings.gemini.GoogleGenAIEmbedding",
|
||||
"vertexai": "mem0.embeddings.vertexai.VertexAIEmbedding",
|
||||
"together": "mem0.embeddings.together.TogetherEmbedding",
|
||||
"lmstudio": "mem0.embeddings.lmstudio.LMStudioEmbedding",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -67,11 +69,13 @@ class VectorStoreFactory:
|
||||
"pgvector": "mem0.vector_stores.pgvector.PGVector",
|
||||
"milvus": "mem0.vector_stores.milvus.MilvusDB",
|
||||
"azure_ai_search": "mem0.vector_stores.azure_ai_search.AzureAISearch",
|
||||
"pinecone": "mem0.vector_stores.pinecone.PineconeDB",
|
||||
"redis": "mem0.vector_stores.redis.RedisDB",
|
||||
"elasticsearch": "mem0.vector_stores.elasticsearch.ElasticsearchDB",
|
||||
"vertex_ai_vector_search": "mem0.vector_stores.vertex_ai_vector_search.GoogleMatchingEngine",
|
||||
"opensearch": "mem0.vector_stores.opensearch.OpenSearchDB",
|
||||
"supabase": "mem0.vector_stores.supabase.Supabase",
|
||||
"weaviate": "mem0.vector_stores.weaviate.Weaviate",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -12,6 +13,7 @@ try:
|
||||
from azure.search.documents import SearchClient
|
||||
from azure.search.documents.indexes import SearchIndexClient
|
||||
from azure.search.documents.indexes.models import (
|
||||
BinaryQuantizationCompression,
|
||||
HnswAlgorithmConfiguration,
|
||||
ScalarQuantizationCompression,
|
||||
SearchField,
|
||||
@@ -24,7 +26,7 @@ try:
|
||||
from azure.search.documents.models import VectorizedQuery
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"The 'azure-search-documents' library is required. Please install it using 'pip install azure-search-documents==11.5.1'."
|
||||
"The 'azure-search-documents' library is required. Please install it using 'pip install azure-search-documents==11.5.2'."
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,43 +39,86 @@ class OutputData(BaseModel):
|
||||
|
||||
|
||||
class AzureAISearch(VectorStoreBase):
|
||||
def __init__(self, service_name, collection_name, api_key, embedding_model_dims, use_compression):
|
||||
"""Initialize the Azure Cognitive Search vector store.
|
||||
def __init__(
|
||||
self,
|
||||
service_name,
|
||||
collection_name,
|
||||
api_key,
|
||||
embedding_model_dims,
|
||||
compression_type: Optional[str] = None,
|
||||
use_float16: bool = False,
|
||||
hybrid_search: bool = False,
|
||||
vector_filter_mode: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the Azure AI Search vector store.
|
||||
|
||||
Args:
|
||||
service_name (str): Azure Cognitive Search service name.
|
||||
service_name (str): Azure AI Search service name.
|
||||
collection_name (str): Index name.
|
||||
api_key (str): API key for the Azure Cognitive Search service.
|
||||
api_key (str): API key for the Azure AI Search service.
|
||||
embedding_model_dims (int): Dimension of the embedding vector.
|
||||
use_compression (bool): Use scalar quantization vector compression
|
||||
compression_type (Optional[str]): Specifies the type of quantization to use.
|
||||
Allowed values are None (no quantization), "scalar", or "binary".
|
||||
use_float16 (bool): Whether to store vectors in half precision (Edm.Half) or full precision (Edm.Single).
|
||||
(Note: This flag is preserved from the initial implementation per feedback.)
|
||||
hybrid_search (bool): Whether to use hybrid search. Default is False.
|
||||
vector_filter_mode (Optional[str]): Mode for vector filtering. Default is "preFilter".
|
||||
"""
|
||||
self.index_name = collection_name
|
||||
self.collection_name = collection_name
|
||||
self.embedding_model_dims = embedding_model_dims
|
||||
self.use_compression = use_compression
|
||||
# If compression_type is None, treat it as "none".
|
||||
self.compression_type = (compression_type or "none").lower()
|
||||
self.use_float16 = use_float16
|
||||
self.hybrid_search = hybrid_search
|
||||
self.vector_filter_mode = vector_filter_mode
|
||||
|
||||
self.search_client = SearchClient(
|
||||
endpoint=f"https://{service_name}.search.windows.net",
|
||||
index_name=self.index_name,
|
||||
credential=AzureKeyCredential(api_key),
|
||||
)
|
||||
self.index_client = SearchIndexClient(
|
||||
endpoint=f"https://{service_name}.search.windows.net", credential=AzureKeyCredential(api_key)
|
||||
endpoint=f"https://{service_name}.search.windows.net",
|
||||
credential=AzureKeyCredential(api_key),
|
||||
)
|
||||
|
||||
self.search_client._client._config.user_agent_policy.add_user_agent("mem0")
|
||||
self.index_client._client._config.user_agent_policy.add_user_agent("mem0")
|
||||
|
||||
self.create_col() # create the collection / index
|
||||
|
||||
def create_col(self):
|
||||
"""Create a new index in Azure Cognitive Search."""
|
||||
vector_dimensions = self.embedding_model_dims # Set this to the number of dimensions in your vector
|
||||
|
||||
if self.use_compression:
|
||||
"""Create a new index in Azure AI Search."""
|
||||
# Determine vector type based on use_float16 setting.
|
||||
if self.use_float16:
|
||||
vector_type = "Collection(Edm.Half)"
|
||||
compression_name = "myCompression"
|
||||
compression_configurations = [ScalarQuantizationCompression(compression_name=compression_name)]
|
||||
else:
|
||||
vector_type = "Collection(Edm.Single)"
|
||||
compression_name = None
|
||||
compression_configurations = []
|
||||
|
||||
# Configure compression settings based on the specified compression_type.
|
||||
compression_configurations = []
|
||||
compression_name = None
|
||||
if self.compression_type == "scalar":
|
||||
compression_name = "myCompression"
|
||||
# For SQ, rescoring defaults to True and oversampling defaults to 4.
|
||||
compression_configurations = [
|
||||
ScalarQuantizationCompression(
|
||||
compression_name=compression_name
|
||||
# rescoring defaults to True and oversampling defaults to 4
|
||||
)
|
||||
]
|
||||
elif self.compression_type == "binary":
|
||||
compression_name = "myCompression"
|
||||
# For BQ, rescoring defaults to True and oversampling defaults to 10.
|
||||
compression_configurations = [
|
||||
BinaryQuantizationCompression(
|
||||
compression_name=compression_name
|
||||
# rescoring defaults to True and oversampling defaults to 10
|
||||
)
|
||||
]
|
||||
# If no compression is desired, compression_configurations remains empty.
|
||||
fields = [
|
||||
SimpleField(name="id", type=SearchFieldDataType.String, key=True),
|
||||
SimpleField(name="user_id", type=SearchFieldDataType.String, filterable=True),
|
||||
@@ -83,15 +128,19 @@ class AzureAISearch(VectorStoreBase):
|
||||
name="vector",
|
||||
type=vector_type,
|
||||
searchable=True,
|
||||
vector_search_dimensions=vector_dimensions,
|
||||
vector_search_dimensions=self.embedding_model_dims,
|
||||
vector_search_profile_name="my-vector-config",
|
||||
),
|
||||
SimpleField(name="payload", type=SearchFieldDataType.String, searchable=True),
|
||||
SearchField(name="payload", type=SearchFieldDataType.String, searchable=True),
|
||||
]
|
||||
|
||||
vector_search = VectorSearch(
|
||||
profiles=[
|
||||
VectorSearchProfile(name="my-vector-config", algorithm_configuration_name="my-algorithms-config")
|
||||
VectorSearchProfile(
|
||||
name="my-vector-config",
|
||||
algorithm_configuration_name="my-algorithms-config",
|
||||
compression_name=compression_name if self.compression_type != "none" else None,
|
||||
)
|
||||
],
|
||||
algorithms=[HnswAlgorithmConfiguration(name="my-algorithms-config")],
|
||||
compressions=compression_configurations,
|
||||
@@ -101,14 +150,16 @@ class AzureAISearch(VectorStoreBase):
|
||||
|
||||
def _generate_document(self, vector, payload, id):
|
||||
document = {"id": id, "vector": vector, "payload": json.dumps(payload)}
|
||||
# Extract additional fields if they exist
|
||||
# Extract additional fields if they exist.
|
||||
for field in ["user_id", "run_id", "agent_id"]:
|
||||
if field in payload:
|
||||
document[field] = payload[field]
|
||||
return document
|
||||
|
||||
# Note: Explicit "insert" calls may later be decoupled from memory management decisions.
|
||||
def insert(self, vectors, payloads=None, ids=None):
|
||||
"""Insert vectors into the index.
|
||||
"""
|
||||
Insert vectors into the index.
|
||||
|
||||
Args:
|
||||
vectors (List[List[float]]): List of vectors to insert.
|
||||
@@ -116,43 +167,65 @@ class AzureAISearch(VectorStoreBase):
|
||||
ids (List[str], optional): List of IDs corresponding to vectors.
|
||||
"""
|
||||
logger.info(f"Inserting {len(vectors)} vectors into index {self.index_name}")
|
||||
|
||||
documents = [
|
||||
self._generate_document(vector, payload, id) for id, vector, payload in zip(ids, vectors, payloads)
|
||||
]
|
||||
self.search_client.upload_documents(documents)
|
||||
response = self.search_client.upload_documents(documents)
|
||||
for doc in response:
|
||||
if not hasattr(doc, "status_code") and doc.get("status_code") != 201:
|
||||
raise Exception(f"Insert failed for document {doc.get('id')}: {doc}")
|
||||
return response
|
||||
|
||||
def _sanitize_key(self, key: str) -> str:
|
||||
return re.sub(r"[^\w]", "", key)
|
||||
|
||||
def _build_filter_expression(self, filters):
|
||||
filter_conditions = []
|
||||
for key, value in filters.items():
|
||||
# If the value is a string, add quotes
|
||||
safe_key = self._sanitize_key(key)
|
||||
if isinstance(value, str):
|
||||
condition = f"{key} eq '{value}'"
|
||||
safe_value = value.replace("'", "''")
|
||||
condition = f"{safe_key} eq '{safe_value}'"
|
||||
else:
|
||||
condition = f"{key} eq {value}"
|
||||
condition = f"{safe_key} eq {value}"
|
||||
filter_conditions.append(condition)
|
||||
# Use 'and' to join multiple conditions
|
||||
filter_expression = " and ".join(filter_conditions)
|
||||
return filter_expression
|
||||
|
||||
def search(self, query, limit=5, filters=None):
|
||||
"""Search for similar vectors.
|
||||
def search(self, query, vectors, limit=5, filters=None):
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (List[float]): Query vectors.
|
||||
query (str): Query.
|
||||
vectors (List[float]): Query vector.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (Dict, optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
Returns:
|
||||
list: Search results.
|
||||
List[OutputData]: Search results.
|
||||
"""
|
||||
# Build filter expression
|
||||
filter_expression = None
|
||||
if filters:
|
||||
filter_expression = self._build_filter_expression(filters)
|
||||
|
||||
vector_query = VectorizedQuery(vector=query, k_nearest_neighbors=limit, fields="vector")
|
||||
search_results = self.search_client.search(vector_queries=[vector_query], filter=filter_expression, top=limit)
|
||||
vector_query = VectorizedQuery(vector=vectors, k_nearest_neighbors=limit, fields="vector")
|
||||
if self.hybrid_search:
|
||||
search_results = self.search_client.search(
|
||||
search_text=query,
|
||||
vector_queries=[vector_query],
|
||||
filter=filter_expression,
|
||||
top=limit,
|
||||
vector_filter_mode=self.vector_filter_mode,
|
||||
search_fields=["payload"],
|
||||
)
|
||||
else:
|
||||
search_results = self.search_client.search(
|
||||
vector_queries=[vector_query],
|
||||
filter=filter_expression,
|
||||
top=limit,
|
||||
vector_filter_mode=self.vector_filter_mode,
|
||||
)
|
||||
|
||||
results = []
|
||||
for result in search_results:
|
||||
@@ -161,16 +234,22 @@ class AzureAISearch(VectorStoreBase):
|
||||
return results
|
||||
|
||||
def delete(self, vector_id):
|
||||
"""Delete a vector by ID.
|
||||
"""
|
||||
Delete a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to delete.
|
||||
"""
|
||||
self.search_client.delete_documents(documents=[{"id": vector_id}])
|
||||
response = self.search_client.delete_documents(documents=[{"id": vector_id}])
|
||||
for doc in response:
|
||||
if not hasattr(doc, "status_code") and doc.get("status_code") != 200:
|
||||
raise Exception(f"Delete failed for document {vector_id}: {doc}")
|
||||
logger.info(f"Deleted document with ID '{vector_id}' from index '{self.index_name}'.")
|
||||
return response
|
||||
|
||||
def update(self, vector_id, vector=None, payload=None):
|
||||
"""Update a vector and its payload.
|
||||
"""
|
||||
Update a vector and its payload.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to update.
|
||||
@@ -185,10 +264,15 @@ class AzureAISearch(VectorStoreBase):
|
||||
document["payload"] = json_payload
|
||||
for field in ["user_id", "run_id", "agent_id"]:
|
||||
document[field] = payload.get(field)
|
||||
self.search_client.merge_or_upload_documents(documents=[document])
|
||||
response = self.search_client.merge_or_upload_documents(documents=[document])
|
||||
for doc in response:
|
||||
if not hasattr(doc, "status_code") and doc.get("status_code") != 200:
|
||||
raise Exception(f"Update failed for document {vector_id}: {doc}")
|
||||
return response
|
||||
|
||||
def get(self, vector_id) -> OutputData:
|
||||
"""Retrieve a vector by ID.
|
||||
"""
|
||||
Retrieve a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (str): ID of the vector to retrieve.
|
||||
@@ -203,32 +287,38 @@ class AzureAISearch(VectorStoreBase):
|
||||
return OutputData(id=result["id"], score=None, payload=json.loads(result["payload"]))
|
||||
|
||||
def list_cols(self) -> List[str]:
|
||||
"""List all collections (indexes).
|
||||
"""
|
||||
List all collections (indexes).
|
||||
|
||||
Returns:
|
||||
List[str]: List of index names.
|
||||
"""
|
||||
indexes = self.index_client.list_indexes()
|
||||
return [index.name for index in indexes]
|
||||
try:
|
||||
names = self.index_client.list_index_names()
|
||||
except AttributeError:
|
||||
names = [index.name for index in self.index_client.list_indexes()]
|
||||
return names
|
||||
|
||||
def delete_col(self):
|
||||
"""Delete the index."""
|
||||
self.index_client.delete_index(self.index_name)
|
||||
|
||||
def col_info(self):
|
||||
"""Get information about the index.
|
||||
"""
|
||||
Get information about the index.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Index information.
|
||||
dict: Index information.
|
||||
"""
|
||||
index = self.index_client.get_index(self.index_name)
|
||||
return {"name": index.name, "fields": index.fields}
|
||||
|
||||
def list(self, filters=None, limit=100):
|
||||
"""List all vectors in the index.
|
||||
"""
|
||||
List all vectors in the index.
|
||||
|
||||
Args:
|
||||
filters (Dict, optional): Filters to apply to the list.
|
||||
filters (dict, optional): Filters to apply to the list.
|
||||
limit (int, optional): Number of vectors to return. Defaults to 100.
|
||||
|
||||
Returns:
|
||||
@@ -243,7 +333,6 @@ class AzureAISearch(VectorStoreBase):
|
||||
for result in search_results:
|
||||
payload = json.loads(result["payload"])
|
||||
results.append(OutputData(id=result["id"], score=result["@search.score"], payload=payload))
|
||||
|
||||
return [results]
|
||||
|
||||
def __del__(self):
|
||||
|
||||
@@ -13,7 +13,7 @@ class VectorStoreBase(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def search(self, query, limit=5, filters=None):
|
||||
def search(self, query, vectors, limit=5, filters=None):
|
||||
"""Search for similar vectors."""
|
||||
pass
|
||||
|
||||
|
||||
@@ -127,19 +127,22 @@ class ChromaDB(VectorStoreBase):
|
||||
logger.info(f"Inserting {len(vectors)} vectors into collection {self.collection_name}")
|
||||
self.collection.add(ids=ids, embeddings=vectors, metadatas=payloads)
|
||||
|
||||
def search(self, query: List[list], limit: int = 5, filters: Optional[Dict] = None) -> List[OutputData]:
|
||||
def search(
|
||||
self, query: str, vectors: List[list], limit: int = 5, filters: Optional[Dict] = None
|
||||
) -> List[OutputData]:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (List[list]): Query vector.
|
||||
query (str): Query.
|
||||
vectors (List[list]): List of vectors to search.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (Optional[Dict], optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
Returns:
|
||||
List[OutputData]: Search results.
|
||||
"""
|
||||
results = self.collection.query(query_embeddings=query, where=filters, n_results=limit)
|
||||
results = self.collection.query(query_embeddings=vectors, where=filters, n_results=limit)
|
||||
final_results = self._parse_output(results)
|
||||
return final_results
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class VectorStoreConfig(BaseModel):
|
||||
"qdrant": "QdrantConfig",
|
||||
"chroma": "ChromaDbConfig",
|
||||
"pgvector": "PGVectorConfig",
|
||||
"pinecone": "PineconeConfig",
|
||||
"milvus": "MilvusDBConfig",
|
||||
"azure_ai_search": "AzureAISearchConfig",
|
||||
"redis": "RedisDBConfig",
|
||||
@@ -21,6 +22,7 @@ class VectorStoreConfig(BaseModel):
|
||||
"vertex_ai_vector_search": "GoogleMatchingEngineConfig",
|
||||
"opensearch": "OpenSearchConfig",
|
||||
"supabase": "SupabaseConfig",
|
||||
"weaviate": "WeaviateConfig",
|
||||
}
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
||||
@@ -46,6 +46,11 @@ class ElasticsearchDB(VectorStoreBase):
|
||||
if config.auto_create_index:
|
||||
self.create_index()
|
||||
|
||||
if config.custom_search_query:
|
||||
self.custom_search_query = config.custom_search_query
|
||||
else:
|
||||
self.custom_search_query = None
|
||||
|
||||
def create_index(self) -> None:
|
||||
"""Create Elasticsearch index with proper mappings if it doesn't exist"""
|
||||
index_settings = {
|
||||
@@ -116,26 +121,25 @@ class ElasticsearchDB(VectorStoreBase):
|
||||
)
|
||||
return results
|
||||
|
||||
def search(self, query: List[float], limit: int = 5, filters: Optional[Dict] = None) -> List[OutputData]:
|
||||
"""Search for similar vectors using KNN search with pre-filtering."""
|
||||
if not filters:
|
||||
# If no filters, just do KNN search
|
||||
search_query = {"knn": {"field": "vector", "query_vector": query, "k": limit, "num_candidates": limit * 2}}
|
||||
def search(
|
||||
self, query: str, vectors: List[float], limit: int = 5, filters: Optional[Dict] = None
|
||||
) -> List[OutputData]:
|
||||
"""
|
||||
Search with two options:
|
||||
1. Use custom search query if provided
|
||||
2. Use KNN search on vectors with pre-filtering if no custom search query is provided
|
||||
"""
|
||||
if self.custom_search_query:
|
||||
search_query = self.custom_search_query(vectors, limit, filters)
|
||||
else:
|
||||
# If filters exist, apply them with KNN search
|
||||
filter_conditions = []
|
||||
for key, value in filters.items():
|
||||
filter_conditions.append({"term": {f"metadata.{key}": value}})
|
||||
|
||||
search_query = {
|
||||
"knn": {
|
||||
"field": "vector",
|
||||
"query_vector": query,
|
||||
"k": limit,
|
||||
"num_candidates": limit * 2,
|
||||
"filter": {"bool": {"must": filter_conditions}},
|
||||
}
|
||||
"knn": {"field": "vector", "query_vector": vectors, "k": limit, "num_candidates": limit * 2}
|
||||
}
|
||||
if filters:
|
||||
filter_conditions = []
|
||||
for key, value in filters.items():
|
||||
filter_conditions.append({"term": {f"metadata.{key}": value}})
|
||||
search_query["knn"]["filter"] = {"bool": {"must": filter_conditions}}
|
||||
|
||||
response = self.client.search(index=self.collection_name, body=search_query)
|
||||
|
||||
|
||||
@@ -134,12 +134,13 @@ class MilvusDB(VectorStoreBase):
|
||||
|
||||
return memory
|
||||
|
||||
def search(self, query: list, limit: int = 5, filters: dict = None) -> list:
|
||||
def search(self, query: str, vectors: list, limit: int = 5, filters: dict = None) -> list:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (List[float]): Query vector.
|
||||
query (str): Query.
|
||||
vectors (List[float]): Query vector.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (Dict, optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
@@ -149,7 +150,7 @@ class MilvusDB(VectorStoreBase):
|
||||
query_filter = self._create_filter(filters) if filters else None
|
||||
hits = self.client.search(
|
||||
collection_name=self.collection_name,
|
||||
data=[query],
|
||||
data=[vectors],
|
||||
limit=limit,
|
||||
filter=query_filter,
|
||||
output_fields=["*"],
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
try:
|
||||
from opensearchpy import OpenSearch
|
||||
from opensearchpy import OpenSearch, RequestsHttpConnection
|
||||
from opensearchpy.helpers import bulk
|
||||
except ImportError:
|
||||
raise ImportError("OpenSearch requires extra dependencies. Install with `pip install opensearch-py`") from None
|
||||
@@ -28,9 +28,12 @@ class OpenSearchDB(VectorStoreBase):
|
||||
# Initialize OpenSearch client
|
||||
self.client = OpenSearch(
|
||||
hosts=[{"host": config.host, "port": config.port or 9200}],
|
||||
http_auth=(config.user, config.password) if (config.user and config.password) else None,
|
||||
http_auth=config.http_auth
|
||||
if config.http_auth
|
||||
else ((config.user, config.password) if (config.user and config.password) else None),
|
||||
use_ssl=config.use_ssl,
|
||||
verify_certs=config.verify_certs,
|
||||
connection_class=RequestsHttpConnection,
|
||||
)
|
||||
|
||||
self.collection_name = config.collection_name
|
||||
@@ -43,14 +46,17 @@ class OpenSearchDB(VectorStoreBase):
|
||||
def create_index(self) -> None:
|
||||
"""Create OpenSearch index with proper mappings if it doesn't exist."""
|
||||
index_settings = {
|
||||
# ToDo change replicas to 1
|
||||
"settings": {
|
||||
"index": {"number_of_replicas": 1, "number_of_shards": 5, "refresh_interval": "1s", "knn": True}
|
||||
},
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"text": {"type": "text"},
|
||||
"vector": {"type": "knn_vector", "dimension": self.vector_dim},
|
||||
"vector": {
|
||||
"type": "knn_vector",
|
||||
"dimension": self.vector_dim,
|
||||
"method": {"engine": "lucene", "name": "hnsw", "space_type": "cosinesimil"},
|
||||
},
|
||||
"metadata": {"type": "object", "properties": {"user_id": {"type": "keyword"}}},
|
||||
}
|
||||
},
|
||||
@@ -111,14 +117,16 @@ class OpenSearchDB(VectorStoreBase):
|
||||
results.append(OutputData(id=id_, score=1.0, payload=payloads[i]))
|
||||
return results
|
||||
|
||||
def search(self, query: List[float], limit: int = 5, filters: Optional[Dict] = None) -> List[OutputData]:
|
||||
def search(
|
||||
self, query: str, vectors: List[float], limit: int = 5, filters: Optional[Dict] = None
|
||||
) -> List[OutputData]:
|
||||
"""Search for similar vectors using OpenSearch k-NN search with pre-filtering."""
|
||||
search_query = {
|
||||
"size": limit,
|
||||
"query": {
|
||||
"knn": {
|
||||
"vector": {
|
||||
"vector": query,
|
||||
"vector": vectors,
|
||||
"k": limit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,12 +120,13 @@ class PGVector(VectorStoreBase):
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def search(self, query, limit=5, filters=None):
|
||||
def search(self, query, vectors, limit=5, filters=None):
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (List[float]): Query vector.
|
||||
query (str): Query.
|
||||
vectors (List[float]): Query vector.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (Dict, optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
@@ -150,7 +151,7 @@ class PGVector(VectorStoreBase):
|
||||
ORDER BY distance
|
||||
LIMIT %s
|
||||
""",
|
||||
(query, *filter_params, limit),
|
||||
(vectors, *filter_params, limit),
|
||||
)
|
||||
|
||||
results = self.cur.fetchall()
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
from pinecone import Pinecone, PodSpec, ServerlessSpec
|
||||
from pinecone.data.dataclasses.vector import Vector
|
||||
except ImportError:
|
||||
raise ImportError("Pinecone requires extra dependencies. Install with `pip install pinecone pinecone-text`") from None
|
||||
|
||||
from mem0.vector_stores.base import VectorStoreBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OutputData(BaseModel):
|
||||
id: Optional[str] # memory id
|
||||
score: Optional[float] # distance
|
||||
payload: Optional[Dict] # metadata
|
||||
|
||||
|
||||
class PineconeDB(VectorStoreBase):
|
||||
def __init__(
|
||||
self,
|
||||
collection_name: str,
|
||||
embedding_model_dims: int,
|
||||
client: Optional["Pinecone"],
|
||||
api_key: Optional[str],
|
||||
environment: Optional[str],
|
||||
serverless_config: Optional[Dict[str, Any]],
|
||||
pod_config: Optional[Dict[str, Any]],
|
||||
hybrid_search: bool,
|
||||
metric: str,
|
||||
batch_size: int,
|
||||
extra_params: Optional[Dict[str, Any]]
|
||||
):
|
||||
"""
|
||||
Initialize the Pinecone vector store.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the index/collection.
|
||||
embedding_model_dims (int): Dimensions of the embedding model.
|
||||
client (Pinecone, optional): Existing Pinecone client instance. Defaults to None.
|
||||
api_key (str, optional): API key for Pinecone. Defaults to None.
|
||||
environment (str, optional): Pinecone environment. Defaults to None.
|
||||
serverless_config (Dict, optional): Configuration for serverless deployment. Defaults to None.
|
||||
pod_config (Dict, optional): Configuration for pod-based deployment. Defaults to None.
|
||||
hybrid_search (bool, optional): Whether to enable hybrid search. Defaults to False.
|
||||
metric (str, optional): Distance metric for vector similarity. Defaults to "cosine".
|
||||
batch_size (int, optional): Batch size for operations. Defaults to 100.
|
||||
extra_params (Dict, optional): Additional parameters for Pinecone client. Defaults to None.
|
||||
"""
|
||||
if client:
|
||||
self.client = client
|
||||
else:
|
||||
api_key = api_key or os.environ.get("PINECONE_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
"Pinecone API key must be provided either as a parameter or as an environment variable"
|
||||
)
|
||||
|
||||
params = extra_params or {}
|
||||
self.client = Pinecone(api_key=api_key, **params)
|
||||
|
||||
self.collection_name = collection_name
|
||||
self.embedding_model_dims = embedding_model_dims
|
||||
self.environment = environment
|
||||
self.serverless_config = serverless_config
|
||||
self.pod_config = pod_config
|
||||
self.hybrid_search = hybrid_search
|
||||
self.metric = metric
|
||||
self.batch_size = batch_size
|
||||
|
||||
self.sparse_encoder = None
|
||||
if self.hybrid_search:
|
||||
try:
|
||||
from pinecone_text.sparse import BM25Encoder
|
||||
|
||||
logger.info("Initializing BM25Encoder for sparse vectors...")
|
||||
self.sparse_encoder = BM25Encoder.default()
|
||||
except ImportError:
|
||||
logger.warning("pinecone-text not installed. Hybrid search will be disabled.")
|
||||
self.hybrid_search = False
|
||||
|
||||
self.create_col(embedding_model_dims, metric)
|
||||
|
||||
def create_col(self, vector_size: int, metric: str = "cosine"):
|
||||
"""
|
||||
Create a new index/collection.
|
||||
|
||||
Args:
|
||||
vector_size (int): Size of the vectors to be stored.
|
||||
metric (str, optional): Distance metric for vector similarity. Defaults to "cosine".
|
||||
"""
|
||||
existing_indexes = self.list_cols().names()
|
||||
|
||||
if self.collection_name in existing_indexes:
|
||||
logging.debug(f"Index {self.collection_name} already exists. Skipping creation.")
|
||||
self.index = self.client.Index(self.collection_name)
|
||||
return
|
||||
|
||||
if self.serverless_config:
|
||||
spec = ServerlessSpec(**self.serverless_config)
|
||||
elif self.pod_config:
|
||||
spec = PodSpec(**self.pod_config)
|
||||
else:
|
||||
spec = ServerlessSpec(cloud="aws", region="us-west-2")
|
||||
|
||||
self.client.create_index(
|
||||
name=self.collection_name,
|
||||
dimension=vector_size,
|
||||
metric=metric,
|
||||
spec=spec,
|
||||
)
|
||||
|
||||
self.index = self.client.Index(self.collection_name)
|
||||
|
||||
def insert(
|
||||
self,
|
||||
vectors: List[List[float]],
|
||||
payloads: Optional[List[Dict]] = None,
|
||||
ids: Optional[List[Union[str, int]]] = None,
|
||||
):
|
||||
"""
|
||||
Insert vectors into an index.
|
||||
|
||||
Args:
|
||||
vectors (list): List of vectors to insert.
|
||||
payloads (list, optional): List of payloads corresponding to vectors. Defaults to None.
|
||||
ids (list, optional): List of IDs corresponding to vectors. Defaults to None.
|
||||
"""
|
||||
logger.info(f"Inserting {len(vectors)} vectors into index {self.collection_name}")
|
||||
items = []
|
||||
|
||||
for idx, vector in enumerate(vectors):
|
||||
item_id = str(ids[idx]) if ids is not None else str(idx)
|
||||
payload = payloads[idx] if payloads else {}
|
||||
|
||||
vector_record = {"id": item_id, "values": vector, "metadata": payload}
|
||||
|
||||
if self.hybrid_search and self.sparse_encoder and "text" in payload:
|
||||
sparse_vector = self.sparse_encoder.encode_documents(payload["text"])
|
||||
vector_record["sparse_values"] = sparse_vector
|
||||
|
||||
items.append(vector_record)
|
||||
|
||||
if len(items) >= self.batch_size:
|
||||
self.index.upsert(vectors=items)
|
||||
items = []
|
||||
|
||||
if items:
|
||||
self.index.upsert(vectors=items)
|
||||
|
||||
def _parse_output(self, data: Dict) -> List[OutputData]:
|
||||
"""
|
||||
Parse the output data from Pinecone search results.
|
||||
|
||||
Args:
|
||||
data (Dict): Output data from Pinecone query.
|
||||
|
||||
Returns:
|
||||
List[OutputData]: Parsed output data.
|
||||
"""
|
||||
if isinstance(data, Vector):
|
||||
result = OutputData(
|
||||
id=data.id,
|
||||
score=0.0,
|
||||
payload=data.metadata,
|
||||
)
|
||||
return result
|
||||
else:
|
||||
result = []
|
||||
for match in data:
|
||||
entry = OutputData(
|
||||
id=match.get("id"),
|
||||
score=match.get("score"),
|
||||
payload=match.get("metadata"),
|
||||
)
|
||||
result.append(entry)
|
||||
|
||||
return result
|
||||
|
||||
def _create_filter(self, filters: Optional[Dict]) -> Dict:
|
||||
"""
|
||||
Create a filter dictionary from the provided filters.
|
||||
"""
|
||||
if not filters:
|
||||
return {}
|
||||
|
||||
pinecone_filter = {}
|
||||
|
||||
for key, value in filters.items():
|
||||
if isinstance(value, dict) and "gte" in value and "lte" in value:
|
||||
pinecone_filter[key] = {"$gte": value["gte"], "$lte": value["lte"]}
|
||||
else:
|
||||
pinecone_filter[key] = {"$eq": value}
|
||||
|
||||
return pinecone_filter
|
||||
|
||||
def search(self, query: str, vectors: List[float], limit: int = 5, filters: Optional[Dict] = None) -> List[OutputData]:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (str): Query.
|
||||
vectors (list): List of vectors to search.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (dict, optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
Returns:
|
||||
list: Search results.
|
||||
"""
|
||||
filter_dict = self._create_filter(filters) if filters else None
|
||||
|
||||
query_params = {
|
||||
"vector": vectors,
|
||||
"top_k": limit,
|
||||
"include_metadata": True,
|
||||
"include_values": False,
|
||||
}
|
||||
|
||||
if filter_dict:
|
||||
query_params["filter"] = filter_dict
|
||||
|
||||
if self.hybrid_search and self.sparse_encoder and "text" in filters:
|
||||
query_text = filters.get("text")
|
||||
if query_text:
|
||||
sparse_vector = self.sparse_encoder.encode_queries(query_text)
|
||||
query_params["sparse_vector"] = sparse_vector
|
||||
|
||||
response = self.index.query(**query_params)
|
||||
|
||||
results = self._parse_output(response.matches)
|
||||
return results
|
||||
|
||||
def delete(self, vector_id: Union[str, int]):
|
||||
"""
|
||||
Delete a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (Union[str, int]): ID of the vector to delete.
|
||||
"""
|
||||
self.index.delete(ids=[str(vector_id)])
|
||||
|
||||
def update(self, vector_id: Union[str, int], vector: Optional[List[float]] = None, payload: Optional[Dict] = None):
|
||||
"""
|
||||
Update a vector and its payload.
|
||||
|
||||
Args:
|
||||
vector_id (Union[str, int]): ID of the vector to update.
|
||||
vector (list, optional): Updated vector. Defaults to None.
|
||||
payload (dict, optional): Updated payload. Defaults to None.
|
||||
"""
|
||||
item = {
|
||||
"id": str(vector_id),
|
||||
}
|
||||
|
||||
if vector is not None:
|
||||
item["values"] = vector
|
||||
|
||||
if payload is not None:
|
||||
item["metadata"] = payload
|
||||
|
||||
if self.hybrid_search and self.sparse_encoder and "text" in payload:
|
||||
sparse_vector = self.sparse_encoder.encode_documents(payload["text"])
|
||||
item["sparse_values"] = sparse_vector
|
||||
|
||||
self.index.upsert(vectors=[item])
|
||||
|
||||
def get(self, vector_id: Union[str, int]) -> OutputData:
|
||||
"""
|
||||
Retrieve a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (Union[str, int]): ID of the vector to retrieve.
|
||||
|
||||
Returns:
|
||||
dict: Retrieved vector or None if not found.
|
||||
"""
|
||||
try:
|
||||
response = self.index.fetch(ids=[str(vector_id)])
|
||||
if str(vector_id) in response.vectors:
|
||||
return self._parse_output(response.vectors[str(vector_id)])
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector {vector_id}: {e}")
|
||||
return None
|
||||
|
||||
def list_cols(self):
|
||||
"""
|
||||
List all indexes/collections.
|
||||
|
||||
Returns:
|
||||
list: List of index information.
|
||||
"""
|
||||
return self.client.list_indexes()
|
||||
|
||||
def delete_col(self):
|
||||
"""Delete an index/collection."""
|
||||
try:
|
||||
self.client.delete_index(self.collection_name)
|
||||
logger.info(f"Index {self.collection_name} deleted successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting index {self.collection_name}: {e}")
|
||||
|
||||
def col_info(self) -> Dict:
|
||||
"""
|
||||
Get information about an index/collection.
|
||||
|
||||
Returns:
|
||||
dict: Index information.
|
||||
"""
|
||||
return self.client.describe_index(self.collection_name)
|
||||
|
||||
def list(self, filters: Optional[Dict] = None, limit: int = 100) -> List[OutputData]:
|
||||
"""
|
||||
List vectors in an index with optional filtering.
|
||||
|
||||
Args:
|
||||
filters (dict, optional): Filters to apply to the list. Defaults to None.
|
||||
limit (int, optional): Number of vectors to return. Defaults to 100.
|
||||
|
||||
Returns:
|
||||
dict: List of vectors with their metadata.
|
||||
"""
|
||||
filter_dict = self._create_filter(filters) if filters else None
|
||||
|
||||
stats = self.index.describe_index_stats()
|
||||
dimension = stats.dimension
|
||||
|
||||
zero_vector = [0.0] * dimension
|
||||
|
||||
query_params = {
|
||||
"vector": zero_vector,
|
||||
"top_k": limit,
|
||||
"include_metadata": True,
|
||||
"include_values": True,
|
||||
}
|
||||
|
||||
if filter_dict:
|
||||
query_params["filter"] = filter_dict
|
||||
|
||||
try:
|
||||
response = self.index.query(**query_params)
|
||||
response = response.to_dict()
|
||||
results = self._parse_output(response["matches"])
|
||||
return [results]
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing vectors: {e}")
|
||||
return {"points": [], "next_page_token": None}
|
||||
|
||||
def count(self) -> int:
|
||||
"""
|
||||
Count number of vectors in the index.
|
||||
|
||||
Returns:
|
||||
int: Total number of vectors.
|
||||
"""
|
||||
stats = self.index.describe_index_stats()
|
||||
return stats.total_vector_count
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset the index by deleting and recreating it.
|
||||
"""
|
||||
self.delete_col()
|
||||
self.create_col(self.embedding_model_dims, self.metric)
|
||||
@@ -127,12 +127,13 @@ class Qdrant(VectorStoreBase):
|
||||
conditions.append(FieldCondition(key=key, match=MatchValue(value=value)))
|
||||
return Filter(must=conditions) if conditions else None
|
||||
|
||||
def search(self, query: list, limit: int = 5, filters: dict = None) -> list:
|
||||
def search(self, query: str, vectors: list, limit: int = 5, filters: dict = None) -> list:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (list): Query vector.
|
||||
query (str): Query.
|
||||
vectors (list): Query vector.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (dict, optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
@@ -142,7 +143,7 @@ class Qdrant(VectorStoreBase):
|
||||
query_filter = self._create_filter(filters) if filters else None
|
||||
hits = self.client.query_points(
|
||||
collection_name=self.collection_name,
|
||||
query=query,
|
||||
query=vectors,
|
||||
query_filter=query_filter,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
@@ -101,12 +101,12 @@ class RedisDB(VectorStoreBase):
|
||||
data.append(entry)
|
||||
self.index.load(data, id_field="memory_id")
|
||||
|
||||
def search(self, query: list, limit: int = 5, filters: dict = None):
|
||||
def search(self, query: str, vectors: list, limit: int = 5, filters: dict = None):
|
||||
conditions = [Tag(key) == value for key, value in filters.items() if value is not None]
|
||||
filter = reduce(lambda x, y: x & y, conditions)
|
||||
|
||||
v = VectorQuery(
|
||||
vector=np.array(query, dtype=np.float32).tobytes(),
|
||||
vector=np.array(vectors, dtype=np.float32).tobytes(),
|
||||
vector_field_name="embedding",
|
||||
return_fields=["memory_id", "hash", "agent_id", "run_id", "user_id", "memory", "metadata", "created_at"],
|
||||
filter_expression=filter,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import uuid
|
||||
from typing import List, Optional, Dict, Any
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -112,16 +112,18 @@ class Supabase(VectorStoreBase):
|
||||
payloads = [{} for _ in vectors]
|
||||
|
||||
records = [(id, vector, payload) for id, vector, payload in zip(ids, vectors, payloads)]
|
||||
print(records)
|
||||
|
||||
self.collection.upsert(records)
|
||||
|
||||
def search(self, query: List[float], limit: int = 5, filters: Optional[dict] = None) -> List[OutputData]:
|
||||
def search(
|
||||
self, query: str, vectors: List[float], limit: int = 5, filters: Optional[dict] = None
|
||||
) -> List[OutputData]:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (List[float]): Query vector
|
||||
query (str): Query.
|
||||
vectors (List[float]): Query vector.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (Dict, optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
@@ -129,11 +131,9 @@ class Supabase(VectorStoreBase):
|
||||
List[OutputData]: Search results
|
||||
"""
|
||||
filters = self._preprocess_filters(filters)
|
||||
print(filters)
|
||||
results = self.collection.query(
|
||||
data=query, limit=limit, filters=filters, include_metadata=True, include_value=True
|
||||
data=vectors, limit=limit, filters=filters, include_metadata=True, include_value=True
|
||||
)
|
||||
print(results)
|
||||
|
||||
return [OutputData(id=str(result[0]), score=float(result[1]), payload=result[2]) for result in results]
|
||||
|
||||
|
||||
@@ -32,19 +32,19 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize Google Matching Engine client."""
|
||||
logger.debug("Initializing Google Matching Engine with kwargs: %s", kwargs)
|
||||
|
||||
|
||||
# If collection_name is passed, use it as deployment_index_id if deployment_index_id is not provided
|
||||
if 'collection_name' in kwargs and 'deployment_index_id' not in kwargs:
|
||||
kwargs['deployment_index_id'] = kwargs['collection_name']
|
||||
logger.debug("Using collection_name as deployment_index_id: %s", kwargs['deployment_index_id'])
|
||||
elif 'deployment_index_id' in kwargs and 'collection_name' not in kwargs:
|
||||
kwargs['collection_name'] = kwargs['deployment_index_id']
|
||||
logger.debug("Using deployment_index_id as collection_name: %s", kwargs['collection_name'])
|
||||
|
||||
if "collection_name" in kwargs and "deployment_index_id" not in kwargs:
|
||||
kwargs["deployment_index_id"] = kwargs["collection_name"]
|
||||
logger.debug("Using collection_name as deployment_index_id: %s", kwargs["deployment_index_id"])
|
||||
elif "deployment_index_id" in kwargs and "collection_name" not in kwargs:
|
||||
kwargs["collection_name"] = kwargs["deployment_index_id"]
|
||||
logger.debug("Using deployment_index_id as collection_name: %s", kwargs["collection_name"])
|
||||
|
||||
try:
|
||||
config = GoogleMatchingEngineConfig(**kwargs)
|
||||
logger.debug("Config created: %s", config.model_dump())
|
||||
logger.debug("Config collection_name: %s", getattr(config, 'collection_name', None))
|
||||
logger.debug("Config collection_name: %s", getattr(config, "collection_name", None))
|
||||
except Exception as e:
|
||||
logger.error("Failed to validate config: %s", str(e))
|
||||
raise
|
||||
@@ -57,41 +57,37 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
self.deployment_index_id = config.deployment_index_id # The deployment-specific ID
|
||||
self.collection_name = config.collection_name
|
||||
self.vector_search_api_endpoint = config.vector_search_api_endpoint
|
||||
|
||||
|
||||
logger.debug("Using project=%s, location=%s", self.project_id, self.region)
|
||||
|
||||
|
||||
# Initialize Vertex AI with credentials if provided
|
||||
init_args = {
|
||||
"project": self.project_id,
|
||||
"location": self.region,
|
||||
}
|
||||
if hasattr(config, 'credentials_path') and config.credentials_path:
|
||||
if hasattr(config, "credentials_path") and config.credentials_path:
|
||||
logger.debug("Using credentials from: %s", config.credentials_path)
|
||||
credentials = service_account.Credentials.from_service_account_file(
|
||||
config.credentials_path
|
||||
)
|
||||
credentials = service_account.Credentials.from_service_account_file(config.credentials_path)
|
||||
init_args["credentials"] = credentials
|
||||
|
||||
|
||||
try:
|
||||
aiplatform.init(**init_args)
|
||||
logger.debug("Vertex AI initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error("Failed to initialize Vertex AI: %s", str(e))
|
||||
raise
|
||||
|
||||
|
||||
try:
|
||||
# Format the index path properly using the configured index_id
|
||||
index_path = f"projects/{self.project_number}/locations/{self.region}/indexes/{self.index_id}"
|
||||
logger.debug("Initializing index with path: %s", index_path)
|
||||
self.index = aiplatform.MatchingEngineIndex(index_name=index_path)
|
||||
logger.debug("Index initialized successfully")
|
||||
|
||||
|
||||
# Format the endpoint name properly
|
||||
endpoint_name = self.endpoint_id
|
||||
logger.debug("Initializing endpoint with name: %s", endpoint_name)
|
||||
self.index_endpoint = aiplatform.MatchingEngineIndexEndpoint(
|
||||
index_endpoint_name=endpoint_name
|
||||
)
|
||||
self.index_endpoint = aiplatform.MatchingEngineIndexEndpoint(index_endpoint_name=endpoint_name)
|
||||
logger.debug("Endpoint initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error("Failed to initialize Matching Engine components: %s", str(e))
|
||||
@@ -119,47 +115,36 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
|
||||
def _create_restriction(self, key: str, value: Any) -> aiplatform_v1.types.index.IndexDatapoint.Restriction:
|
||||
"""Create a restriction object for the Matching Engine index.
|
||||
|
||||
|
||||
Args:
|
||||
key: The namespace/key for the restriction
|
||||
value: The value to restrict on
|
||||
|
||||
|
||||
Returns:
|
||||
Restriction object for the index
|
||||
"""
|
||||
str_value = str(value) if value is not None else ""
|
||||
return aiplatform_v1.types.index.IndexDatapoint.Restriction(
|
||||
namespace=key,
|
||||
allow_list=[str_value]
|
||||
)
|
||||
return aiplatform_v1.types.index.IndexDatapoint.Restriction(namespace=key, allow_list=[str_value])
|
||||
|
||||
def _create_datapoint(
|
||||
self,
|
||||
vector_id: str,
|
||||
vector: List[float],
|
||||
payload: Optional[Dict] = None
|
||||
self, vector_id: str, vector: List[float], payload: Optional[Dict] = None
|
||||
) -> aiplatform_v1.types.index.IndexDatapoint:
|
||||
"""Create a datapoint object for the Matching Engine index.
|
||||
|
||||
|
||||
Args:
|
||||
vector_id: The ID for the datapoint
|
||||
vector: The vector to store
|
||||
payload: Optional metadata to store with the vector
|
||||
|
||||
|
||||
Returns:
|
||||
IndexDatapoint object
|
||||
"""
|
||||
restrictions = []
|
||||
if payload:
|
||||
restrictions = [
|
||||
self._create_restriction(key, value)
|
||||
for key, value in payload.items()
|
||||
]
|
||||
|
||||
restrictions = [self._create_restriction(key, value) for key, value in payload.items()]
|
||||
|
||||
return aiplatform_v1.types.index.IndexDatapoint(
|
||||
datapoint_id=vector_id,
|
||||
feature_vector=vector,
|
||||
restricts=restrictions
|
||||
datapoint_id=vector_id, feature_vector=vector, restricts=restrictions
|
||||
)
|
||||
|
||||
def insert(
|
||||
@@ -169,41 +154,41 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
ids: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
"""Insert vectors into the Matching Engine index.
|
||||
|
||||
|
||||
Args:
|
||||
vectors: List of vectors to insert
|
||||
payloads: Optional list of metadata dictionaries
|
||||
ids: Optional list of IDs for the vectors
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If vectors is empty or lengths don't match
|
||||
GoogleAPIError: If the API call fails
|
||||
"""
|
||||
if not vectors:
|
||||
raise ValueError("No vectors provided for insertion")
|
||||
|
||||
|
||||
if payloads and len(payloads) != len(vectors):
|
||||
raise ValueError(f"Number of payloads ({len(payloads)}) does not match number of vectors ({len(vectors)})")
|
||||
|
||||
|
||||
if ids and len(ids) != len(vectors):
|
||||
raise ValueError(f"Number of ids ({len(ids)}) does not match number of vectors ({len(vectors)})")
|
||||
|
||||
|
||||
logger.debug("Starting insert of %d vectors", len(vectors))
|
||||
|
||||
|
||||
try:
|
||||
datapoints = [
|
||||
self._create_datapoint(
|
||||
vector_id=ids[i] if ids else str(uuid.uuid4()),
|
||||
vector=vector,
|
||||
payload=payloads[i] if payloads and i < len(payloads) else None
|
||||
payload=payloads[i] if payloads and i < len(payloads) else None,
|
||||
)
|
||||
for i, vector in enumerate(vectors)
|
||||
]
|
||||
|
||||
|
||||
logger.debug("Created %d datapoints", len(datapoints))
|
||||
self.index.upsert_datapoints(datapoints=datapoints)
|
||||
logger.debug("Successfully inserted datapoints")
|
||||
|
||||
|
||||
except google.api_core.exceptions.GoogleAPIError as e:
|
||||
logger.error("Failed to insert vectors: %s", str(e))
|
||||
raise
|
||||
@@ -212,21 +197,22 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.error("Stack trace: %s", traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
def search(self, query: List[float], limit: int = 5, filters: Optional[Dict] = None) -> List[OutputData]:
|
||||
def search(
|
||||
self, query: str, vectors: List[float], limit: int = 5, filters: Optional[Dict] = None
|
||||
) -> List[OutputData]:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
Args:
|
||||
query (List[float]): Query vector.
|
||||
query (str): Query.
|
||||
vectors (List[float]): Query vector.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (Optional[Dict], optional): Filters to apply to the search. Defaults to None.
|
||||
Returns:
|
||||
List[OutputData]: Search results (unwrapped)
|
||||
"""
|
||||
logger.debug("Starting search")
|
||||
logger.debug("Query type: %s, length: %d", type(query), len(query))
|
||||
logger.debug("Limit: %d, Filters: %s", limit, filters)
|
||||
|
||||
|
||||
try:
|
||||
filter_namespaces = []
|
||||
if filters:
|
||||
@@ -235,53 +221,42 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.debug("Processing filter %s=%s (type=%s)", key, value, type(value))
|
||||
if isinstance(value, (str, int, float)):
|
||||
logger.debug("Adding simple filter for %s", key)
|
||||
filter_namespaces.append(
|
||||
Namespace(key, [str(value)], [])
|
||||
)
|
||||
filter_namespaces.append(Namespace(key, [str(value)], []))
|
||||
elif isinstance(value, dict):
|
||||
logger.debug("Adding complex filter for %s", key)
|
||||
includes = value.get('include', [])
|
||||
excludes = value.get('exclude', [])
|
||||
filter_namespaces.append(
|
||||
Namespace(key, includes, excludes)
|
||||
)
|
||||
|
||||
includes = value.get("include", [])
|
||||
excludes = value.get("exclude", [])
|
||||
filter_namespaces.append(Namespace(key, includes, excludes))
|
||||
|
||||
logger.debug("Final filter_namespaces: %s", filter_namespaces)
|
||||
|
||||
|
||||
response = self.index_endpoint.find_neighbors(
|
||||
deployed_index_id=self.deployment_index_id,
|
||||
queries=[query],
|
||||
queries=[vectors],
|
||||
num_neighbors=limit,
|
||||
filter=filter_namespaces if filter_namespaces else None,
|
||||
return_full_datapoint=True
|
||||
return_full_datapoint=True,
|
||||
)
|
||||
|
||||
|
||||
if not response or len(response) == 0 or len(response[0]) == 0:
|
||||
logger.debug("No results found")
|
||||
return []
|
||||
|
||||
|
||||
results = []
|
||||
for neighbor in response[0]:
|
||||
logger.debug("Processing neighbor - id: %s, distance: %s",
|
||||
neighbor.id, neighbor.distance)
|
||||
|
||||
logger.debug("Processing neighbor - id: %s, distance: %s", neighbor.id, neighbor.distance)
|
||||
|
||||
payload = {}
|
||||
if hasattr(neighbor, 'restricts'):
|
||||
if hasattr(neighbor, "restricts"):
|
||||
logger.debug("Processing restricts")
|
||||
for restrict in neighbor.restricts:
|
||||
if (hasattr(restrict, 'name') and
|
||||
hasattr(restrict, 'allow_tokens') and
|
||||
restrict.allow_tokens):
|
||||
if hasattr(restrict, "name") and hasattr(restrict, "allow_tokens") and restrict.allow_tokens:
|
||||
logger.debug("Adding %s: %s", restrict.name, restrict.allow_tokens[0])
|
||||
payload[restrict.name] = restrict.allow_tokens[0]
|
||||
|
||||
output_data = OutputData(
|
||||
id=neighbor.id,
|
||||
score=neighbor.distance,
|
||||
payload=payload
|
||||
)
|
||||
|
||||
output_data = OutputData(id=neighbor.id, score=neighbor.distance, payload=payload)
|
||||
results.append(output_data)
|
||||
|
||||
|
||||
logger.debug("Returning %d results", len(results))
|
||||
return results
|
||||
|
||||
@@ -291,7 +266,6 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.error("Stack trace: %s", traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
def delete(self, vector_id: Optional[str] = None, ids: Optional[List[str]] = None) -> bool:
|
||||
"""
|
||||
Delete vectors from the Matching Engine index.
|
||||
@@ -326,14 +300,13 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
except google.api_core.exceptions.InvalidArgument as e:
|
||||
logger.error("Invalid argument: %s", str(e))
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error occurred: %s", str(e))
|
||||
logger.error("Error type: %s", type(e))
|
||||
logger.error("Stack trace: %s", traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
def update(
|
||||
self,
|
||||
vector_id: str,
|
||||
@@ -341,42 +314,40 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
payload: Optional[Dict] = None,
|
||||
) -> bool:
|
||||
"""Update a vector and its payload.
|
||||
|
||||
|
||||
Args:
|
||||
vector_id: ID of the vector to update
|
||||
vector: Optional new vector values
|
||||
payload: Optional new metadata payload
|
||||
|
||||
|
||||
Returns:
|
||||
bool: True if update was successful
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If neither vector nor payload is provided
|
||||
GoogleAPIError: If the API call fails
|
||||
"""
|
||||
logger.debug("Starting update for vector_id: %s", vector_id)
|
||||
|
||||
|
||||
if vector is None and payload is None:
|
||||
raise ValueError("Either vector or payload must be provided for update")
|
||||
|
||||
|
||||
# First check if the vector exists
|
||||
try:
|
||||
existing = self.get(vector_id)
|
||||
if existing is None:
|
||||
logger.error("Vector ID not found: %s", vector_id)
|
||||
return False
|
||||
|
||||
|
||||
datapoint = self._create_datapoint(
|
||||
vector_id=vector_id,
|
||||
vector=vector if vector is not None else [],
|
||||
payload=payload
|
||||
vector_id=vector_id, vector=vector if vector is not None else [], payload=payload
|
||||
)
|
||||
|
||||
|
||||
logger.debug("Upserting datapoint: %s", datapoint)
|
||||
self.index.upsert_datapoints(datapoints=[datapoint])
|
||||
logger.debug("Update completed successfully")
|
||||
return True
|
||||
|
||||
|
||||
except google.api_core.exceptions.GoogleAPIError as e:
|
||||
logger.error("API error during update: %s", str(e))
|
||||
return False
|
||||
@@ -385,7 +356,6 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.error("Stack trace: %s", traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
def get(self, vector_id: str) -> Optional[OutputData]:
|
||||
"""
|
||||
Retrieve a vector by ID.
|
||||
@@ -395,24 +365,17 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
Optional[OutputData]: Retrieved vector or None if not found.
|
||||
"""
|
||||
logger.debug("Starting get for vector_id: %s", vector_id)
|
||||
|
||||
|
||||
try:
|
||||
if not self.vector_search_api_endpoint:
|
||||
raise ValueError("vector_search_api_endpoint is required for get operation")
|
||||
|
||||
vector_search_client = aiplatform_v1.MatchServiceClient(
|
||||
client_options={
|
||||
"api_endpoint": self.vector_search_api_endpoint
|
||||
},
|
||||
)
|
||||
datapoint = aiplatform_v1.IndexDatapoint(
|
||||
datapoint_id=vector_id
|
||||
client_options={"api_endpoint": self.vector_search_api_endpoint},
|
||||
)
|
||||
datapoint = aiplatform_v1.IndexDatapoint(datapoint_id=vector_id)
|
||||
|
||||
query = aiplatform_v1.FindNeighborsRequest.Query(
|
||||
datapoint=datapoint,
|
||||
neighbor_count=1
|
||||
)
|
||||
query = aiplatform_v1.FindNeighborsRequest.Query(datapoint=datapoint, neighbor_count=1)
|
||||
request = aiplatform_v1.FindNeighborsRequest(
|
||||
index_endpoint=f"projects/{self.project_number}/locations/{self.region}/indexEndpoints/{self.endpoint_id}",
|
||||
deployed_index_id=self.deployment_index_id,
|
||||
@@ -423,41 +386,36 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
try:
|
||||
response = vector_search_client.find_neighbors(request)
|
||||
logger.debug("Got response")
|
||||
|
||||
|
||||
if response and response.nearest_neighbors:
|
||||
nearest = response.nearest_neighbors[0]
|
||||
if nearest.neighbors:
|
||||
neighbor = nearest.neighbors[0]
|
||||
|
||||
|
||||
payload = {}
|
||||
if hasattr(neighbor.datapoint, 'restricts'):
|
||||
if hasattr(neighbor.datapoint, "restricts"):
|
||||
for restrict in neighbor.datapoint.restricts:
|
||||
if restrict.allow_list:
|
||||
payload[restrict.namespace] = restrict.allow_list[0]
|
||||
|
||||
return OutputData(
|
||||
id=neighbor.datapoint.datapoint_id,
|
||||
score=neighbor.distance,
|
||||
payload=payload
|
||||
)
|
||||
|
||||
|
||||
return OutputData(id=neighbor.datapoint.datapoint_id, score=neighbor.distance, payload=payload)
|
||||
|
||||
logger.debug("No results found")
|
||||
return None
|
||||
|
||||
|
||||
except google.api_core.exceptions.NotFound:
|
||||
logger.debug("Datapoint not found")
|
||||
return None
|
||||
except google.api_core.exceptions.PermissionDenied as e:
|
||||
logger.error("Permission denied: %s", str(e))
|
||||
return None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error occurred: %s", str(e))
|
||||
logger.error("Error type: %s", type(e))
|
||||
logger.error("Stack trace: %s", traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
def list_cols(self) -> List[str]:
|
||||
"""
|
||||
List all collections (indexes).
|
||||
@@ -466,7 +424,6 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
"""
|
||||
return [self.deployment_index_id]
|
||||
|
||||
|
||||
def delete_col(self):
|
||||
"""
|
||||
Delete a collection (index).
|
||||
@@ -475,7 +432,6 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.warning("Delete collection operation is not supported for Google Matching Engine")
|
||||
pass
|
||||
|
||||
|
||||
def col_info(self) -> Dict:
|
||||
"""
|
||||
Get information about a collection (index).
|
||||
@@ -486,17 +442,16 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
"index_id": self.index_id,
|
||||
"endpoint_id": self.endpoint_id,
|
||||
"project_id": self.project_id,
|
||||
"region": self.region
|
||||
"region": self.region,
|
||||
}
|
||||
|
||||
|
||||
def list(self, filters: Optional[Dict] = None, limit: Optional[int] = None) -> List[List[OutputData]]:
|
||||
"""List vectors matching the given filters.
|
||||
|
||||
|
||||
Args:
|
||||
filters: Optional filters to apply
|
||||
limit: Optional maximum number of results to return
|
||||
|
||||
|
||||
Returns:
|
||||
List[List[OutputData]]: List of matching vectors wrapped in an extra array
|
||||
to match the interface
|
||||
@@ -504,36 +459,31 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.debug("Starting list operation")
|
||||
logger.debug("Filters: %s", filters)
|
||||
logger.debug("Limit: %s", limit)
|
||||
|
||||
|
||||
try:
|
||||
# Use a zero vector for the search
|
||||
dimension = 768 # This should be configurable based on the model
|
||||
zero_vector = [0.0] * dimension
|
||||
|
||||
|
||||
# Use a large limit if none specified
|
||||
search_limit = limit if limit is not None else 10000
|
||||
|
||||
results = self.search(
|
||||
query=zero_vector,
|
||||
limit=search_limit,
|
||||
filters=filters
|
||||
)
|
||||
|
||||
|
||||
results = self.search(query=zero_vector, limit=search_limit, filters=filters)
|
||||
|
||||
logger.debug("Found %d results", len(results))
|
||||
return [results] # Wrap in extra array to match interface
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error in list operation: %s", str(e))
|
||||
logger.error("Stack trace: %s", traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
def create_col(self, name=None, vector_size=None, distance=None):
|
||||
"""
|
||||
Create a new collection. For Google Matching Engine, collections (indexes)
|
||||
Create a new collection. For Google Matching Engine, collections (indexes)
|
||||
are created through the Google Cloud Console or API separately.
|
||||
This method is a no-op since indexes are pre-created.
|
||||
|
||||
|
||||
Args:
|
||||
name: Ignored for Google Matching Engine
|
||||
vector_size: Ignored for Google Matching Engine
|
||||
@@ -543,41 +493,35 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
# This method is included only to satisfy the abstract base class
|
||||
pass
|
||||
|
||||
|
||||
def add(self, text: str, metadata: Optional[Dict] = None, user_id: Optional[str] = None) -> str:
|
||||
logger.debug("Starting add operation")
|
||||
logger.debug("Text: %s", text)
|
||||
logger.debug("Metadata: %s", metadata)
|
||||
logger.debug("User ID: %s", user_id)
|
||||
|
||||
|
||||
try:
|
||||
# Generate a unique ID for this entry
|
||||
vector_id = str(uuid.uuid4())
|
||||
|
||||
|
||||
# Create the payload with all necessary fields
|
||||
payload = {
|
||||
"data": text, # Store the text in the data field
|
||||
"user_id": user_id,
|
||||
**(metadata or {})
|
||||
**(metadata or {}),
|
||||
}
|
||||
|
||||
|
||||
# Get the embedding
|
||||
vector = self.embedder.embed_query(text)
|
||||
|
||||
|
||||
# Insert using the insert method
|
||||
self.insert(
|
||||
vectors=[vector],
|
||||
payloads=[payload],
|
||||
ids=[vector_id]
|
||||
)
|
||||
|
||||
self.insert(vectors=[vector], payloads=[payload], ids=[vector_id])
|
||||
|
||||
return vector_id
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error occurred: %s", str(e))
|
||||
raise
|
||||
|
||||
|
||||
def add_texts(
|
||||
self,
|
||||
texts: List[str],
|
||||
@@ -585,47 +529,45 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
ids: Optional[List[str]] = None,
|
||||
) -> List[str]:
|
||||
"""Add texts to the vector store.
|
||||
|
||||
|
||||
Args:
|
||||
texts: List of texts to add
|
||||
metadatas: Optional list of metadata dicts
|
||||
ids: Optional list of IDs to use
|
||||
|
||||
|
||||
Returns:
|
||||
List[str]: List of IDs of the added texts
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If texts is empty or lengths don't match
|
||||
"""
|
||||
if not texts:
|
||||
raise ValueError("No texts provided")
|
||||
|
||||
|
||||
if metadatas and len(metadatas) != len(texts):
|
||||
raise ValueError(f"Number of metadata items ({len(metadatas)}) does not match number of texts ({len(texts)})")
|
||||
|
||||
raise ValueError(
|
||||
f"Number of metadata items ({len(metadatas)}) does not match number of texts ({len(texts)})"
|
||||
)
|
||||
|
||||
if ids and len(ids) != len(texts):
|
||||
raise ValueError(f"Number of ids ({len(ids)}) does not match number of texts ({len(texts)})")
|
||||
|
||||
|
||||
logger.debug("Starting add_texts operation")
|
||||
logger.debug("Number of texts: %d", len(texts))
|
||||
logger.debug("Has metadatas: %s", metadatas is not None)
|
||||
logger.debug("Has ids: %s", ids is not None)
|
||||
|
||||
|
||||
if ids is None:
|
||||
ids = [str(uuid.uuid4()) for _ in texts]
|
||||
|
||||
|
||||
try:
|
||||
# Get embeddings
|
||||
embeddings = self.embedder.embed_documents(texts)
|
||||
|
||||
|
||||
# Add to store
|
||||
self.insert(
|
||||
vectors=embeddings,
|
||||
payloads=metadatas if metadatas else [{}] * len(texts),
|
||||
ids=ids
|
||||
)
|
||||
self.insert(vectors=embeddings, payloads=metadatas if metadatas else [{}] * len(texts), ids=ids)
|
||||
return ids
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error in add_texts: %s", str(e))
|
||||
logger.error("Stack trace: %s", traceback.format_exc())
|
||||
@@ -657,18 +599,12 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.debug("Query: %s", query)
|
||||
logger.debug("k: %d", k)
|
||||
logger.debug("Filter: %s", filter)
|
||||
|
||||
|
||||
embedding = self.embedder.embed_query(query)
|
||||
results = self.search(query=embedding, limit=k, filters=filter)
|
||||
|
||||
|
||||
docs_and_scores = [
|
||||
(
|
||||
Document(
|
||||
page_content=result.payload.get("text", ""),
|
||||
metadata=result.payload
|
||||
),
|
||||
result.score
|
||||
)
|
||||
(Document(page_content=result.payload.get("text", ""), metadata=result.payload), result.score)
|
||||
for result in results
|
||||
]
|
||||
logger.debug("Found %d results", len(docs_and_scores))
|
||||
@@ -684,4 +620,3 @@ class GoogleMatchingEngine(VectorStoreBase):
|
||||
logger.debug("Starting similarity search")
|
||||
docs_and_scores = self.similarity_search_with_score(query, k, filter)
|
||||
return [doc for doc, _ in docs_and_scores]
|
||||
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Dict, List, Mapping, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
import weaviate
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"The 'weaviate' library is required. Please install it using 'pip install weaviate-client weaviate'."
|
||||
)
|
||||
|
||||
import weaviate.classes.config as wvcc
|
||||
from weaviate.classes.init import Auth
|
||||
from weaviate.classes.query import Filter, MetadataQuery
|
||||
from weaviate.util import get_valid_uuid
|
||||
|
||||
from mem0.vector_stores.base import VectorStoreBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OutputData(BaseModel):
|
||||
id: str
|
||||
score: float
|
||||
payload: Dict
|
||||
|
||||
|
||||
class Weaviate(VectorStoreBase):
|
||||
def __init__(
|
||||
self,
|
||||
collection_name: str,
|
||||
embedding_model_dims: int,
|
||||
cluster_url: str = None,
|
||||
auth_client_secret: str = None,
|
||||
additional_headers: dict = None,
|
||||
):
|
||||
"""
|
||||
Initialize the Weaviate vector store.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection/class in Weaviate.
|
||||
embedding_model_dims (int): Dimensions of the embedding model.
|
||||
client (WeaviateClient, optional): Existing Weaviate client instance. Defaults to None.
|
||||
cluster_url (str, optional): URL for Weaviate server. Defaults to None.
|
||||
auth_config (dict, optional): Authentication configuration for Weaviate. Defaults to None.
|
||||
additional_headers (dict, optional): Additional headers for requests. Defaults to None.
|
||||
"""
|
||||
if "localhost" in cluster_url:
|
||||
self.client = weaviate.connect_to_local(headers=additional_headers)
|
||||
else:
|
||||
self.client = weaviate.connect_to_wcs(
|
||||
cluster_url=cluster_url,
|
||||
auth_credentials=Auth.api_key(auth_client_secret),
|
||||
headers=additional_headers,
|
||||
)
|
||||
|
||||
self.collection_name = collection_name
|
||||
self.create_col(embedding_model_dims)
|
||||
|
||||
def _parse_output(self, data: Dict) -> List[OutputData]:
|
||||
"""
|
||||
Parse the output data.
|
||||
|
||||
Args:
|
||||
data (Dict): Output data.
|
||||
|
||||
Returns:
|
||||
List[OutputData]: Parsed output data.
|
||||
"""
|
||||
keys = ["ids", "distances", "metadatas"]
|
||||
values = []
|
||||
|
||||
for key in keys:
|
||||
value = data.get(key, [])
|
||||
if isinstance(value, list) and value and isinstance(value[0], list):
|
||||
value = value[0]
|
||||
values.append(value)
|
||||
|
||||
ids, distances, metadatas = values
|
||||
max_length = max(len(v) for v in values if isinstance(v, list) and v is not None)
|
||||
|
||||
result = []
|
||||
for i in range(max_length):
|
||||
entry = OutputData(
|
||||
id=ids[i] if isinstance(ids, list) and ids and i < len(ids) else None,
|
||||
score=(distances[i] if isinstance(distances, list) and distances and i < len(distances) else None),
|
||||
payload=(metadatas[i] if isinstance(metadatas, list) and metadatas and i < len(metadatas) else None),
|
||||
)
|
||||
result.append(entry)
|
||||
|
||||
return result
|
||||
|
||||
def create_col(self, vector_size, distance="cosine"):
|
||||
"""
|
||||
Create a new collection with the specified schema.
|
||||
|
||||
Args:
|
||||
vector_size (int): Size of the vectors to be stored.
|
||||
distance (str, optional): Distance metric for vector similarity. Defaults to "cosine".
|
||||
"""
|
||||
if self.client.collections.exists(self.collection_name):
|
||||
logging.debug(f"Collection {self.collection_name} already exists. Skipping creation.")
|
||||
return
|
||||
|
||||
properties = [
|
||||
wvcc.Property(name="ids", data_type=wvcc.DataType.TEXT),
|
||||
wvcc.Property(name="hash", data_type=wvcc.DataType.TEXT),
|
||||
wvcc.Property(
|
||||
name="metadata",
|
||||
data_type=wvcc.DataType.TEXT,
|
||||
description="Additional metadata",
|
||||
),
|
||||
wvcc.Property(name="data", data_type=wvcc.DataType.TEXT),
|
||||
wvcc.Property(name="created_at", data_type=wvcc.DataType.TEXT),
|
||||
wvcc.Property(name="category", data_type=wvcc.DataType.TEXT),
|
||||
wvcc.Property(name="updated_at", data_type=wvcc.DataType.TEXT),
|
||||
wvcc.Property(name="user_id", data_type=wvcc.DataType.TEXT),
|
||||
wvcc.Property(name="agent_id", data_type=wvcc.DataType.TEXT),
|
||||
wvcc.Property(name="run_id", data_type=wvcc.DataType.TEXT),
|
||||
]
|
||||
|
||||
vectorizer_config = wvcc.Configure.Vectorizer.none()
|
||||
vector_index_config = wvcc.Configure.VectorIndex.hnsw()
|
||||
|
||||
self.client.collections.create(
|
||||
self.collection_name,
|
||||
vectorizer_config=vectorizer_config,
|
||||
vector_index_config=vector_index_config,
|
||||
properties=properties,
|
||||
)
|
||||
|
||||
def insert(self, vectors, payloads=None, ids=None):
|
||||
"""
|
||||
Insert vectors into a collection.
|
||||
|
||||
Args:
|
||||
vectors (list): List of vectors to insert.
|
||||
payloads (list, optional): List of payloads corresponding to vectors. Defaults to None.
|
||||
ids (list, optional): List of IDs corresponding to vectors. Defaults to None.
|
||||
"""
|
||||
logger.info(f"Inserting {len(vectors)} vectors into collection {self.collection_name}")
|
||||
with self.client.batch.fixed_size(batch_size=100) as batch:
|
||||
for idx, vector in enumerate(vectors):
|
||||
object_id = ids[idx] if ids and idx < len(ids) else str(uuid.uuid4())
|
||||
object_id = get_valid_uuid(object_id)
|
||||
|
||||
data_object = payloads[idx] if payloads and idx < len(payloads) else {}
|
||||
|
||||
# Ensure 'id' is not included in properties (it's used as the Weaviate object ID)
|
||||
if "ids" in data_object:
|
||||
del data_object["ids"]
|
||||
|
||||
batch.add_object(collection=self.collection_name, properties=data_object, uuid=object_id, vector=vector)
|
||||
|
||||
def search(
|
||||
self, query: str, vectors: List[float], limit: int = 5, filters: Optional[Dict] = None
|
||||
) -> List[OutputData]:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
"""
|
||||
collection = self.client.collections.get(str(self.collection_name))
|
||||
filter_conditions = []
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
if value and key in ["user_id", "agent_id", "run_id"]:
|
||||
filter_conditions.append(Filter.by_property(key).equal(value))
|
||||
combined_filter = Filter.all_of(filter_conditions) if filter_conditions else None
|
||||
response = collection.query.hybrid(
|
||||
query="",
|
||||
vector=vectors,
|
||||
limit=limit,
|
||||
filters=combined_filter,
|
||||
return_properties=["hash", "created_at", "updated_at", "user_id", "agent_id", "run_id", "data", "category"],
|
||||
return_metadata=MetadataQuery(score=True),
|
||||
)
|
||||
results = []
|
||||
for obj in response.objects:
|
||||
payload = obj.properties.copy()
|
||||
|
||||
for id_field in ["run_id", "agent_id", "user_id"]:
|
||||
if id_field in payload and payload[id_field] is None:
|
||||
del payload[id_field]
|
||||
|
||||
payload["id"] = str(obj.uuid).split("'")[0] # Include the id in the payload
|
||||
results.append(
|
||||
OutputData(
|
||||
id=str(obj.uuid),
|
||||
score=1
|
||||
if obj.metadata.distance is None
|
||||
else 1 - obj.metadata.distance, # Convert distance to score
|
||||
payload=payload,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
def delete(self, vector_id):
|
||||
"""
|
||||
Delete a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id: ID of the vector to delete.
|
||||
"""
|
||||
collection = self.client.collections.get(str(self.collection_name))
|
||||
collection.data.delete_by_id(vector_id)
|
||||
|
||||
def update(self, vector_id, vector=None, payload=None):
|
||||
"""
|
||||
Update a vector and its payload.
|
||||
|
||||
Args:
|
||||
vector_id: ID of the vector to update.
|
||||
vector (list, optional): Updated vector. Defaults to None.
|
||||
payload (dict, optional): Updated payload. Defaults to None.
|
||||
"""
|
||||
collection = self.client.collections.get(str(self.collection_name))
|
||||
|
||||
if payload:
|
||||
collection.data.update(uuid=vector_id, properties=payload)
|
||||
|
||||
if vector:
|
||||
existing_data = self.get(vector_id)
|
||||
if existing_data:
|
||||
existing_data = dict(existing_data)
|
||||
if "id" in existing_data:
|
||||
del existing_data["id"]
|
||||
existing_payload: Mapping[str, str] = existing_data
|
||||
collection.data.update(uuid=vector_id, properties=existing_payload, vector=vector)
|
||||
|
||||
def get(self, vector_id):
|
||||
"""
|
||||
Retrieve a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id: ID of the vector to retrieve.
|
||||
|
||||
Returns:
|
||||
dict: Retrieved vector and metadata.
|
||||
"""
|
||||
vector_id = get_valid_uuid(vector_id)
|
||||
collection = self.client.collections.get(str(self.collection_name))
|
||||
|
||||
response = collection.query.fetch_object_by_id(
|
||||
uuid=vector_id,
|
||||
return_properties=["hash", "created_at", "updated_at", "user_id", "agent_id", "run_id", "data", "category"],
|
||||
)
|
||||
# results = {}
|
||||
# print("reponse",response)
|
||||
# for obj in response.objects:
|
||||
payload = response.properties.copy()
|
||||
payload["id"] = str(response.uuid).split("'")[0]
|
||||
results = OutputData(
|
||||
id=str(response.uuid).split("'")[0],
|
||||
score=1.0,
|
||||
payload=payload,
|
||||
)
|
||||
return results
|
||||
|
||||
def list_cols(self):
|
||||
"""
|
||||
List all collections.
|
||||
|
||||
Returns:
|
||||
list: List of collection names.
|
||||
"""
|
||||
collections = self.client.collections.list_all()
|
||||
logger.debug(f"collections: {collections}")
|
||||
print(f"collections: {collections}")
|
||||
return {"collections": [{"name": col.name} for col in collections]}
|
||||
|
||||
def delete_col(self):
|
||||
"""Delete a collection."""
|
||||
self.client.collections.delete(self.collection_name)
|
||||
|
||||
def col_info(self):
|
||||
"""
|
||||
Get information about a collection.
|
||||
|
||||
Returns:
|
||||
dict: Collection information.
|
||||
"""
|
||||
schema = self.client.collections.get(self.collection_name)
|
||||
if schema:
|
||||
return schema
|
||||
return None
|
||||
|
||||
def list(self, filters=None, limit=100) -> List[OutputData]:
|
||||
"""
|
||||
List all vectors in a collection.
|
||||
"""
|
||||
collection = self.client.collections.get(self.collection_name)
|
||||
filter_conditions = []
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
if value and key in ["user_id", "agent_id", "run_id"]:
|
||||
filter_conditions.append(Filter.by_property(key).equal(value))
|
||||
combined_filter = Filter.all_of(filter_conditions) if filter_conditions else None
|
||||
response = collection.query.fetch_objects(
|
||||
limit=limit,
|
||||
filters=combined_filter,
|
||||
return_properties=["hash", "created_at", "updated_at", "user_id", "agent_id", "run_id", "data", "category"],
|
||||
)
|
||||
results = []
|
||||
for obj in response.objects:
|
||||
payload = obj.properties.copy()
|
||||
payload["id"] = str(obj.uuid).split("'")[0]
|
||||
results.append(OutputData(id=str(obj.uuid).split("'")[0], score=1.0, payload=payload))
|
||||
return [results]
|
||||
Generated
+332
-212
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@@ -7,7 +7,7 @@ description = "Happy Eyeballs for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1"},
|
||||
{file = "aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"},
|
||||
@@ -20,7 +20,7 @@ description = "Async http client/server framework (asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "aiohttp-3.11.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4fe27dbbeec445e6e1291e61d61eb212ee9fed6e47998b27de71d70d3e8777d"},
|
||||
{file = "aiohttp-3.11.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e64ca2dbea28807f8484c13f684a2f761e69ba2640ec49dacd342763cc265ef"},
|
||||
@@ -116,7 +116,7 @@ propcache = ">=0.2.0"
|
||||
yarl = ">=1.17.0,<2.0"
|
||||
|
||||
[package.extras]
|
||||
speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
|
||||
speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
@@ -125,7 +125,7 @@ description = "aiosignal: a list of registered asynchronous callbacks"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
|
||||
{file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
|
||||
@@ -166,7 +166,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
|
||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
|
||||
[[package]]
|
||||
@@ -189,19 +189,69 @@ description = "Classes Without Boilerplate"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"},
|
||||
{file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||
cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||
dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
||||
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
|
||||
|
||||
[[package]]
|
||||
name = "azure-common"
|
||||
version = "1.1.28"
|
||||
description = "Microsoft Azure Client Library for Python (Common)"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3"},
|
||||
{file = "azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-core"
|
||||
version = "1.32.0"
|
||||
description = "Microsoft Azure Core Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "azure_core-1.32.0-py3-none-any.whl", hash = "sha256:eac191a0efb23bfa83fddf321b27b122b4ec847befa3091fa736a5c32c50d7b4"},
|
||||
{file = "azure_core-1.32.0.tar.gz", hash = "sha256:22b3c35d6b2dae14990f6c1be2912bf23ffe50b220e708a28ab1bb92b1c730e5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
requests = ">=2.21.0"
|
||||
six = ">=1.11.0"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[package.extras]
|
||||
aio = ["aiohttp (>=3.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "azure-search-documents"
|
||||
version = "11.5.2"
|
||||
description = "Microsoft Azure Cognitive Search Client Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "azure_search_documents-11.5.2-py3-none-any.whl", hash = "sha256:c949d011008a4b0bcee3db91132741b4e4d50ddb3f7e2f48944d949d4b413b11"},
|
||||
{file = "azure_search_documents-11.5.2.tar.gz", hash = "sha256:98977dd1fa4978d3b7d8891a0856b3becb6f02cc07ff2e1ea40b9c7254ada315"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
azure-common = ">=1.1"
|
||||
azure-core = ">=1.28.0"
|
||||
isodate = ">=0.6.0"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
@@ -423,23 +473,6 @@ files = [
|
||||
]
|
||||
markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\"", test = "sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "dataclasses-json"
|
||||
version = "0.6.7"
|
||||
description = "Easily serialize dataclasses to and from JSON."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"},
|
||||
{file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
marshmallow = ">=3.18.0,<4.0.0"
|
||||
typing-inspect = ">=0.4.0,<1"
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
@@ -475,7 +508,7 @@ description = "A list-like structure which implements collections.abc.MutableSeq
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"},
|
||||
{file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"},
|
||||
@@ -571,6 +604,47 @@ files = [
|
||||
{file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsspec"
|
||||
version = "2024.12.0"
|
||||
description = "File-system specification"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "fsspec-2024.12.0-py3-none-any.whl", hash = "sha256:b520aed47ad9804237ff878b504267a3b0b441e97508bd6d2d8774e3db85cee2"},
|
||||
{file = "fsspec-2024.12.0.tar.gz", hash = "sha256:670700c977ed2fb51e0d9f9253177ed20cbde4a3e5c0283cc5385b5870c8533f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
abfs = ["adlfs"]
|
||||
adl = ["adlfs"]
|
||||
arrow = ["pyarrow (>=1)"]
|
||||
dask = ["dask", "distributed"]
|
||||
dev = ["pre-commit", "ruff"]
|
||||
doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"]
|
||||
dropbox = ["dropbox", "dropboxdrivefs", "requests"]
|
||||
full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"]
|
||||
fuse = ["fusepy"]
|
||||
gcs = ["gcsfs"]
|
||||
git = ["pygit2"]
|
||||
github = ["requests"]
|
||||
gs = ["gcsfs"]
|
||||
gui = ["panel"]
|
||||
hdfs = ["pyarrow (>=1)"]
|
||||
http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"]
|
||||
libarchive = ["libarchive-c"]
|
||||
oci = ["ocifs"]
|
||||
s3 = ["s3fs"]
|
||||
sftp = ["paramiko"]
|
||||
smb = ["smbprotocol"]
|
||||
ssh = ["paramiko"]
|
||||
test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"]
|
||||
test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask-expr", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"]
|
||||
test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"]
|
||||
tqdm = ["tqdm"]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.1.1"
|
||||
@@ -879,25 +953,12 @@ httpcore = "==1.*"
|
||||
idna = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
description = "Consume Server-Sent Event (SSE) messages with HTTPX."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
|
||||
{file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyperframe"
|
||||
version = "6.1.0"
|
||||
@@ -937,6 +998,18 @@ files = [
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isodate"
|
||||
version = "0.7.2"
|
||||
description = "An ISO 8601 date/time/duration parser and formatter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"},
|
||||
{file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.13.2"
|
||||
@@ -1038,6 +1111,19 @@ files = [
|
||||
{file = "jiter-0.8.2.tar.gz", hash = "sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json-repair"
|
||||
version = "0.30.3"
|
||||
description = "A package to repair broken json strings"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "json_repair-0.30.3-py3-none-any.whl", hash = "sha256:63bb588162b0958ae93d85356ecbe54c06b8c33f8a4834f93fa2719ea669804e"},
|
||||
{file = "json_repair-0.30.3.tar.gz", hash = "sha256:0ac56e7ae9253ee9c507a7e1a3a26799c9b0bbe5e2bec1b2cc5053e90d5b05e3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonpatch"
|
||||
version = "1.33"
|
||||
@@ -1074,7 +1160,7 @@ description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "langchain-0.3.19-py3-none-any.whl", hash = "sha256:1e16d97db9106640b7de4c69f8f5ed22eeda56b45b9241279e83f111640eff16"},
|
||||
{file = "langchain-0.3.19.tar.gz", hash = "sha256:b96f8a445f01d15d522129ffe77cc89c8468dbd65830d153a676de8f6b899e7b"},
|
||||
@@ -1114,34 +1200,44 @@ together = ["langchain-together"]
|
||||
xai = ["langchain-xai"]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-community"
|
||||
version = "0.3.18"
|
||||
description = "Community contributed LangChain integrations."
|
||||
name = "langchain"
|
||||
version = "0.3.20"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "python_version >= \"3.10\" and python_version < \"3.12\" and extra == \"graph\""
|
||||
files = [
|
||||
{file = "langchain_community-0.3.18-py3-none-any.whl", hash = "sha256:0d4a70144a1750045c4f726f9a43379ed2484178f76e4b8295bcef3a7fdf41d5"},
|
||||
{file = "langchain_community-0.3.18.tar.gz", hash = "sha256:fa2889a8f0b2d22b5c306fd1b070c0970e1f11b604bf55fad2f4a1d0bf68a077"},
|
||||
{file = "langchain-0.3.20-py3-none-any.whl", hash = "sha256:273287f8e61ffdf7e811cf8799e6a71e9381325b8625fd6618900faba79cfdd0"},
|
||||
{file = "langchain-0.3.20.tar.gz", hash = "sha256:edcc3241703e1f6557ef5a5c35cd56f9ccc25ff12e38b4829c66d94971737a93"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.8.3,<4.0.0"
|
||||
dataclasses-json = ">=0.5.7,<0.7"
|
||||
httpx-sse = ">=0.4.0,<1.0.0"
|
||||
langchain = ">=0.3.19,<1.0.0"
|
||||
langchain-core = ">=0.3.37,<1.0.0"
|
||||
langsmith = ">=0.1.125,<0.4"
|
||||
numpy = [
|
||||
{version = ">=1.26.4,<2", markers = "python_version < \"3.12\""},
|
||||
{version = ">=1.26.2,<3", markers = "python_version >= \"3.12\""},
|
||||
]
|
||||
pydantic-settings = ">=2.4.0,<3.0.0"
|
||||
async-timeout = {version = ">=4.0.0,<5.0.0", markers = "python_version < \"3.11\""}
|
||||
langchain-core = ">=0.3.41,<1.0.0"
|
||||
langchain-text-splitters = ">=0.3.6,<1.0.0"
|
||||
langsmith = ">=0.1.17,<0.4"
|
||||
pydantic = ">=2.7.4,<3.0.0"
|
||||
PyYAML = ">=5.3"
|
||||
requests = ">=2,<3"
|
||||
SQLAlchemy = ">=1.4,<3"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10"
|
||||
|
||||
[package.extras]
|
||||
anthropic = ["langchain-anthropic"]
|
||||
aws = ["langchain-aws"]
|
||||
cohere = ["langchain-cohere"]
|
||||
community = ["langchain-community"]
|
||||
deepseek = ["langchain-deepseek"]
|
||||
fireworks = ["langchain-fireworks"]
|
||||
google-genai = ["langchain-google-genai"]
|
||||
google-vertexai = ["langchain-google-vertexai"]
|
||||
groq = ["langchain-groq"]
|
||||
huggingface = ["langchain-huggingface"]
|
||||
mistralai = ["langchain-mistralai"]
|
||||
ollama = ["langchain-ollama"]
|
||||
openai = ["langchain-openai"]
|
||||
together = ["langchain-together"]
|
||||
xai = ["langchain-xai"]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
@@ -1150,7 +1246,7 @@ description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "langchain_core-0.3.40-py3-none-any.whl", hash = "sha256:9f31358741f10a13db8531e8288b8a5ae91904018c5c2e6f739d6645a98fca03"},
|
||||
{file = "langchain_core-0.3.40.tar.gz", hash = "sha256:893a238b38491967c804662c1ec7c3e6ebaf223d1125331249c3cf3862ff2746"},
|
||||
@@ -1168,6 +1264,47 @@ PyYAML = ">=5.3"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
|
||||
typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.45"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.10\" and python_version < \"3.12\" and extra == \"graph\""
|
||||
files = [
|
||||
{file = "langchain_core-0.3.45-py3-none-any.whl", hash = "sha256:fe560d644c102c3f5dcfb44eb5295e26d22deab259fdd084f6b1b55a0350b77c"},
|
||||
{file = "langchain_core-0.3.45.tar.gz", hash = "sha256:a39b8446495d1ea97311aa726478c0a13ef1d77cb7644350bad6d9d3c0141a0c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
jsonpatch = ">=1.33,<2.0"
|
||||
langsmith = ">=0.1.125,<0.4"
|
||||
packaging = ">=23.2,<25"
|
||||
pydantic = {version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""}
|
||||
PyYAML = ">=5.3"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
|
||||
typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-neo4j"
|
||||
version = "0.4.0"
|
||||
description = "An integration package connecting Neo4j and LangChain"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "langchain_neo4j-0.4.0-py3-none-any.whl", hash = "sha256:2760b5757e7a402884cf3419830217651df97fe4f44b3fec6c96b14b6d7fd18e"},
|
||||
{file = "langchain_neo4j-0.4.0.tar.gz", hash = "sha256:3f059a66411cec1062a2b8c44953a70d0fff9e123e9fb1d6b3f17a0bef6d6114"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
langchain = ">=0.3.7,<0.4.0"
|
||||
langchain-core = ">=0.3.8,<0.4.0"
|
||||
neo4j = ">=5.25.0,<6.0.0"
|
||||
neo4j-graphrag = ">=1.5.0,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-text-splitters"
|
||||
version = "0.3.6"
|
||||
@@ -1213,27 +1350,6 @@ zstandard = ">=0.23.0,<0.24.0"
|
||||
langsmith-pyo3 = ["langsmith-pyo3 (>=0.1.0rc2,<0.2.0)"]
|
||||
pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "3.26.1"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"},
|
||||
{file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=17.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"]
|
||||
docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"]
|
||||
tests = ["pytest", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
name = "monotonic"
|
||||
version = "1.6"
|
||||
@@ -1253,7 +1369,7 @@ description = "multidict implementation"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"},
|
||||
{file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"},
|
||||
@@ -1352,19 +1468,6 @@ files = [
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neo4j"
|
||||
version = "5.28.1"
|
||||
@@ -1386,6 +1489,42 @@ numpy = ["numpy (>=1.7.0,<3.0.0)"]
|
||||
pandas = ["numpy (>=1.7.0,<3.0.0)", "pandas (>=1.1.0,<3.0.0)"]
|
||||
pyarrow = ["pyarrow (>=1.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "neo4j-graphrag"
|
||||
version = "1.5.0"
|
||||
description = "Python package to allow easy integration to Neo4j's GraphRAG features"
|
||||
optional = false
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "neo4j_graphrag-1.5.0-py3-none-any.whl", hash = "sha256:d778be8476aa758ff10043b373c287ada0abe90bcfb9c34e8cb4d2c4bbd20240"},
|
||||
{file = "neo4j_graphrag-1.5.0.tar.gz", hash = "sha256:5367c08f128e83ca7028f2112f6e0b69541400adee40cdaf6e30c3563036acb3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
fsspec = ">=2024.9.0,<2025.0.0"
|
||||
json-repair = ">=0.30.2,<0.31.0"
|
||||
neo4j = ">=5.17.0,<6.0.0"
|
||||
pydantic = ">=2.6.3,<3.0.0"
|
||||
pypdf = ">=4.3.1,<5.0.0"
|
||||
pyyaml = ">=6.0.2,<7.0.0"
|
||||
types-pyyaml = ">=6.0.12.20240917,<7.0.0.0"
|
||||
|
||||
[package.extras]
|
||||
anthropic = ["anthropic (>=0.36.0,<0.37.0)"]
|
||||
cohere = ["cohere (>=5.9.0,<6.0.0)"]
|
||||
experimental = ["langchain-text-splitters (>=0.3.0,<0.4.0)", "llama-index (>=0.10.55,<0.11.0)", "pygraphviz (>=1.0.0,<2.0.0) ; python_version < \"3.10\"", "pygraphviz (>=1.13.0,<2.0.0) ; python_version >= \"3.10\" and python_full_version < \"4.0.0\""]
|
||||
google = ["google-cloud-aiplatform (>=1.66.0,<2.0.0)"]
|
||||
kg-creation-tools = ["pygraphviz (>=1.0.0,<2.0.0) ; python_version < \"3.10\"", "pygraphviz (>=1.13.0,<2.0.0) ; python_version >= \"3.10\" and python_full_version < \"4.0.0\""]
|
||||
mistralai = ["mistralai (>=1.0.3,<2.0.0)"]
|
||||
ollama = ["ollama (>=0.4.4,<0.5.0)"]
|
||||
openai = ["openai (>=1.51.1,<2.0.0)"]
|
||||
pinecone = ["pinecone-client (>=4.1.0,<5.0.0)"]
|
||||
qdrant = ["qdrant-client (>=1.11.3,<2.0.0)"]
|
||||
sentence-transformers = ["sentence-transformers (>=3.0.0,<4.0.0)"]
|
||||
weaviate = ["weaviate-client (>=4.6.1,<5.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "1.26.4"
|
||||
@@ -1393,7 +1532,7 @@ description = "Fundamental package for array computing in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version < \"3.12\""
|
||||
markers = "python_version < \"3.10\""
|
||||
files = [
|
||||
{file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
|
||||
{file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
|
||||
@@ -1435,68 +1574,68 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.3"
|
||||
version = "2.2.4"
|
||||
description = "Fundamental package for array computing in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.12\""
|
||||
markers = "python_version >= \"3.10\""
|
||||
files = [
|
||||
{file = "numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094"},
|
||||
{file = "numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5"},
|
||||
{file = "numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe"},
|
||||
{file = "numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693"},
|
||||
{file = "numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef"},
|
||||
{file = "numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082"},
|
||||
{file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d"},
|
||||
{file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9"},
|
||||
{file = "numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e"},
|
||||
{file = "numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4"},
|
||||
{file = "numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:a84eda42bd12edc36eb5b53bbcc9b406820d3353f1994b6cfe453a33ff101775"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:4ba5054787e89c59c593a4169830ab362ac2bee8a969249dc56e5d7d20ff8df9"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-win32.whl", hash = "sha256:a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a"},
|
||||
{file = "numpy-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9e0a277bb2eb5d8a7407e14688b85fd8ad628ee4e0c7930415687b6564207a4"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eeea959168ea555e556b8188da5fa7831e21d91ce031e95ce23747b7609f8a4"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bd3ad3b0a40e713fc68f99ecfd07124195333f1e689387c180813f0e94309d6f"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cf28633d64294969c019c6df4ff37f5698e8326db68cc2b66576a51fad634880"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-win32.whl", hash = "sha256:ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09"},
|
||||
{file = "numpy-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00"},
|
||||
{file = "numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c"},
|
||||
{file = "numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286"},
|
||||
{file = "numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d"},
|
||||
{file = "numpy-2.2.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7051ee569db5fbac144335e0f3b9c2337e0c8d5c9fee015f259a5bd70772b7e8"},
|
||||
{file = "numpy-2.2.4-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ab2939cd5bec30a7430cbdb2287b63151b77cf9624de0532d629c9a1c59b1d5c"},
|
||||
{file = "numpy-2.2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d"},
|
||||
{file = "numpy-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d"},
|
||||
{file = "numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1697,7 +1836,7 @@ description = "Accelerated property cache"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "propcache-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa44f64c37cc30c9f05932c740a8b40ce359f51882c70883cc95feac842da4d"},
|
||||
{file = "propcache-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2383a17385d9800b6eb5855c2f05ee550f803878f344f58b6e194de08b96352c"},
|
||||
@@ -1930,7 +2069,7 @@ typing-extensions = ">=4.12.2"
|
||||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
timezone = ["tzdata"]
|
||||
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
@@ -2046,26 +2185,27 @@ files = [
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.8.1"
|
||||
description = "Settings management using Pydantic"
|
||||
name = "pypdf"
|
||||
version = "4.3.1"
|
||||
description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"},
|
||||
{file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"},
|
||||
{file = "pypdf-4.3.1-py3-none-any.whl", hash = "sha256:64b31da97eda0771ef22edb1bfecd5deee4b72c3d1736b7df2689805076d6418"},
|
||||
{file = "pypdf-4.3.1.tar.gz", hash = "sha256:b2f37fe9a3030aa97ca86067a56ba3f9d3565f9a791b305c7355d8392c30d91b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=2.7.0"
|
||||
python-dotenv = ">=0.21.0"
|
||||
typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
|
||||
toml = ["tomli (>=2.0.1)"]
|
||||
yaml = ["pyyaml (>=6.0.1)"]
|
||||
crypto = ["PyCryptodome ; python_version == \"3.6\"", "cryptography ; python_version >= \"3.7\""]
|
||||
dev = ["black", "flit", "pip-tools", "pre-commit (<2.18.0)", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"]
|
||||
docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"]
|
||||
full = ["Pillow (>=8.0.0)", "PyCryptodome ; python_version == \"3.6\"", "cryptography ; python_version >= \"3.7\""]
|
||||
image = ["Pillow (>=8.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
@@ -2105,22 +2245,6 @@ files = [
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
||||
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2024.2"
|
||||
@@ -2245,7 +2369,7 @@ httpx = {version = ">=0.20.0", extras = ["http2"]}
|
||||
numpy = [
|
||||
{version = ">=1.21", markers = "python_version >= \"3.10\" and python_version < \"3.12\""},
|
||||
{version = ">=1.21,<2.1.0", markers = "python_version < \"3.10\""},
|
||||
{version = ">=1.26", markers = "python_version >= \"3.12\" and python_version < \"3.13\""},
|
||||
{version = ">=1.26", markers = "python_version == \"3.12\""},
|
||||
{version = ">=2.1.0", markers = "python_version >= \"3.13\""},
|
||||
]
|
||||
portalocker = ">=2.7.0,<3.0.0"
|
||||
@@ -2354,13 +2478,13 @@ files = [
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
|
||||
core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
|
||||
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
|
||||
type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
|
||||
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
@@ -2564,6 +2688,19 @@ notebook = ["ipywidgets (>=6)"]
|
||||
slack = ["slack-sdk"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[[package]]
|
||||
name = "types-pyyaml"
|
||||
version = "6.0.12.20241230"
|
||||
description = "Typing stubs for PyYAML"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"},
|
||||
{file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
@@ -2576,23 +2713,6 @@ files = [
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspect"
|
||||
version = "0.9.0"
|
||||
description = "Runtime inspection utilities for typing module."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
files = [
|
||||
{file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"},
|
||||
{file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mypy-extensions = ">=0.3.0"
|
||||
typing-extensions = ">=3.7.4"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.3.0"
|
||||
@@ -2606,7 +2726,7 @@ files = [
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
@@ -2618,7 +2738,7 @@ description = "Yet another URL library"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"graph\""
|
||||
markers = "(python_version < \"3.10\" or python_version >= \"3.12\") and extra == \"graph\""
|
||||
files = [
|
||||
{file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"},
|
||||
{file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"},
|
||||
@@ -2824,9 +2944,9 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\
|
||||
cffi = ["cffi (>=1.11)"]
|
||||
|
||||
[extras]
|
||||
graph = ["langchain-community", "neo4j", "rank-bm25"]
|
||||
graph = ["langchain-neo4j", "neo4j", "rank-bm25"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9,<4.0"
|
||||
content-hash = "3d2febfdf2d1318fc91fce5640e2f9cfb3bf964cc23a797e4f01df81dc7175da"
|
||||
content-hash = "f7bee9b294566e32c580fef1940cbc6fcb7fd6ca2e695f23c719d134af4c3204"
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "mem0ai"
|
||||
version = "0.1.67"
|
||||
version = "0.1.74"
|
||||
description = "Long-term memory for AI Agents"
|
||||
authors = ["Mem0 <founders@mem0.ai>"]
|
||||
exclude = [
|
||||
@@ -22,13 +22,14 @@ openai = "^1.33.0"
|
||||
posthog = "^3.5.0"
|
||||
pytz = "^2024.1"
|
||||
sqlalchemy = "^2.0.31"
|
||||
langchain-community = "^0.3.1"
|
||||
langchain-neo4j = "^0.4.0"
|
||||
neo4j = "^5.23.1"
|
||||
rank-bm25 = "^0.2.2"
|
||||
azure-search-documents = "^11.5.0"
|
||||
psycopg2-binary = "^2.9.10"
|
||||
|
||||
[tool.poetry.extras]
|
||||
graph = ["langchain-community", "neo4j", "rank-bm25"]
|
||||
graph = ["langchain-neo4j", "neo4j", "rank-bm25"]
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
pytest = "^8.2.2"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from mem0.configs import prompts
|
||||
|
||||
|
||||
def test_get_update_memory_messages():
|
||||
retrieved_old_memory_dict = [{"id": "1", "text": "old memory 1"}]
|
||||
response_content = ["new fact"]
|
||||
custom_update_memory_prompt = "custom prompt determining memory update"
|
||||
|
||||
## When custom update memory prompt is provided
|
||||
##
|
||||
result = prompts.get_update_memory_messages(retrieved_old_memory_dict, response_content, custom_update_memory_prompt)
|
||||
assert result.startswith(custom_update_memory_prompt)
|
||||
|
||||
## When custom update memory prompt is not provided
|
||||
##
|
||||
result = prompts.get_update_memory_messages(retrieved_old_memory_dict, response_content, None)
|
||||
assert result.startswith(prompts.DEFAULT_UPDATE_MEMORY_PROMPT)
|
||||
@@ -0,0 +1,41 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from mem0.embeddings.lmstudio import LMStudioEmbedding
|
||||
from mem0.configs.embeddings.base import BaseEmbedderConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_lm_studio_client():
|
||||
with patch("mem0.embeddings.lmstudio.Client") as mock_lm_studio:
|
||||
mock_client = Mock()
|
||||
mock_client.list.return_value = {"models": [{"name": "nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf"}]}
|
||||
mock_lm_studio.return_value = mock_client
|
||||
yield mock_client
|
||||
|
||||
|
||||
def test_embed_text(mock_lm_studio_client):
|
||||
config = BaseEmbedderConfig(model="nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf", embedding_dims=512)
|
||||
embedder = LMStudioEmbedding(config)
|
||||
|
||||
mock_response = {"embedding": [0.1, 0.2, 0.3, 0.4, 0.5]}
|
||||
mock_lm_studio_client.embeddings.return_value = mock_response
|
||||
|
||||
text = "Sample text to embed."
|
||||
embedding = embedder.embed(text)
|
||||
|
||||
mock_lm_studio_client.embeddings.assert_called_once_with(model="nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf", prompt=text)
|
||||
|
||||
assert embedding == [0.1, 0.2, 0.3, 0.4, 0.5]
|
||||
|
||||
|
||||
def test_ensure_model_exists(mock_lm_studio_client):
|
||||
config = BaseEmbedderConfig(model="nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf", embedding_dims=512)
|
||||
embedder = LMStudioEmbedding(config)
|
||||
|
||||
mock_lm_studio_client.pull.assert_not_called()
|
||||
|
||||
mock_lm_studio_client.list.return_value = {"models": []}
|
||||
|
||||
embedder._ensure_model_exists()
|
||||
|
||||
mock_lm_studio_client.pull.assert_called_once_with("nomic-embed-text")
|
||||
@@ -0,0 +1,34 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0.configs.llms.base import BaseLlmConfig
|
||||
from mem0.llms.lmstudio import LMStudioLLM
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_lm_studio_client():
|
||||
with patch("mem0.llms.lmstudio.Client") as mock_lm_studio:
|
||||
mock_client = Mock()
|
||||
mock_client.list.return_value = {"models": [{"name": "lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf"}]}
|
||||
mock_lm_studio.return_value = mock_client
|
||||
yield mock_client
|
||||
|
||||
|
||||
def test_generate_response_without_tools(mock_lm_studio_client):
|
||||
config = BaseLlmConfig(model="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf", temperature=0.7, max_tokens=100, top_p=1.0)
|
||||
llm = LMStudioLLM(config)
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello, how are you?"},
|
||||
]
|
||||
|
||||
mock_response = {"message": {"content": "I'm doing well, thank you for asking!"}}
|
||||
mock_lm_studio_client.chat.return_value = mock_response
|
||||
|
||||
response = llm.generate_response(messages)
|
||||
|
||||
mock_lm_studio_client.chat.assert_called_once_with(
|
||||
model="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf", messages=messages, options={"temperature": 0.7, "num_predict": 100, "top_p": 1.0}
|
||||
)
|
||||
assert response == "I'm doing well, thank you for asking!"
|
||||
+50
-1
@@ -30,6 +30,25 @@ def memory_instance():
|
||||
config = MemoryConfig(version="v1.1")
|
||||
config.graph_store.config = {"some_config": "value"}
|
||||
return Memory(config)
|
||||
|
||||
@pytest.fixture
|
||||
def memory_custom_instance():
|
||||
with patch("mem0.utils.factory.EmbedderFactory") as mock_embedder, patch(
|
||||
"mem0.utils.factory.VectorStoreFactory"
|
||||
) as mock_vector_store, patch("mem0.utils.factory.LlmFactory") as mock_llm, patch(
|
||||
"mem0.memory.telemetry.capture_event"
|
||||
), patch("mem0.memory.graph_memory.MemoryGraph"):
|
||||
mock_embedder.create.return_value = Mock()
|
||||
mock_vector_store.create.return_value = Mock()
|
||||
mock_llm.create.return_value = Mock()
|
||||
|
||||
config = MemoryConfig(
|
||||
version="v1.1",
|
||||
custom_fact_extraction_prompt="custom prompt extracting memory",
|
||||
custom_update_memory_prompt="custom prompt determining memory update"
|
||||
)
|
||||
config.graph_store.config = {"some_config": "value"}
|
||||
return Memory(config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version, enable_graph", [("v1.0", False), ("v1.1", True)])
|
||||
@@ -51,7 +70,7 @@ def test_add(memory_instance, version, enable_graph):
|
||||
assert result["results"] == [{"memory": "Test memory", "event": "ADD"}]
|
||||
|
||||
memory_instance._add_to_vector_store.assert_called_once_with(
|
||||
[{"role": "user", "content": "Test message"}], {"user_id": "test_user"}, {"user_id": "test_user"}
|
||||
[{"role": "user", "content": "Test message"}], {"user_id": "test_user"}, {"user_id": "test_user"}, True
|
||||
)
|
||||
|
||||
# Remove the conditional assertion for _add_to_graph
|
||||
@@ -239,3 +258,33 @@ def test_get_all(memory_instance, version, enable_graph, expected_result):
|
||||
memory_instance.graph.get_all.assert_called_once_with({"user_id": "test_user"}, 100)
|
||||
else:
|
||||
memory_instance.graph.get_all.assert_not_called()
|
||||
|
||||
|
||||
def test_custom_prompts(memory_custom_instance):
|
||||
messages = [{"role": "user", "content": "Test message"}]
|
||||
memory_custom_instance.llm.generate_response = Mock()
|
||||
|
||||
with patch("mem0.memory.main.parse_messages", return_value="Test message") as mock_parse_messages:
|
||||
with patch("mem0.memory.main.get_update_memory_messages", return_value="custom update memory prompt") as mock_get_update_memory_messages:
|
||||
memory_custom_instance.add(messages=messages, user_id="test_user")
|
||||
|
||||
## custom prompt
|
||||
##
|
||||
mock_parse_messages.assert_called_once_with(messages)
|
||||
|
||||
memory_custom_instance.llm.generate_response.assert_any_call(
|
||||
messages=[
|
||||
{"role": "system", "content": memory_custom_instance.config.custom_fact_extraction_prompt},
|
||||
{"role": "user", "content": f"Input:\n{mock_parse_messages.return_value}"},
|
||||
],
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
|
||||
## custom update memory prompt
|
||||
##
|
||||
mock_get_update_memory_messages.assert_called_once_with([],[],memory_custom_instance.config.custom_update_memory_prompt)
|
||||
|
||||
memory_custom_instance.llm.generate_response.assert_any_call(
|
||||
messages=[{"role": "user", "content": mock_get_update_memory_messages.return_value}],
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
@@ -0,0 +1,542 @@
|
||||
import json
|
||||
from unittest.mock import Mock, patch, MagicMock, call
|
||||
import pytest
|
||||
from azure.core.exceptions import ResourceNotFoundError, HttpResponseError
|
||||
|
||||
# Import the AzureAISearch class and related models
|
||||
from mem0.vector_stores.azure_ai_search import AzureAISearch, OutputData
|
||||
from mem0.configs.vector_stores.azure_ai_search import AzureAISearchConfig
|
||||
|
||||
|
||||
# Fixture to patch SearchClient and SearchIndexClient and create an instance of AzureAISearch.
|
||||
@pytest.fixture
|
||||
def mock_clients():
|
||||
with patch("mem0.vector_stores.azure_ai_search.SearchClient") as MockSearchClient, \
|
||||
patch("mem0.vector_stores.azure_ai_search.SearchIndexClient") as MockIndexClient, \
|
||||
patch("mem0.vector_stores.azure_ai_search.AzureKeyCredential") as MockAzureKeyCredential:
|
||||
# Create mocked instances for search and index clients.
|
||||
mock_search_client = MockSearchClient.return_value
|
||||
mock_index_client = MockIndexClient.return_value
|
||||
|
||||
# Mock the client._client._config.user_agent_policy.add_user_agent
|
||||
mock_search_client._client = MagicMock()
|
||||
mock_search_client._client._config.user_agent_policy.add_user_agent = Mock()
|
||||
mock_index_client._client = MagicMock()
|
||||
mock_index_client._client._config.user_agent_policy.add_user_agent = Mock()
|
||||
|
||||
# Stub required methods on search_client.
|
||||
mock_search_client.upload_documents = Mock()
|
||||
mock_search_client.upload_documents.return_value = [{"status": True, "id": "doc1"}]
|
||||
mock_search_client.search = Mock()
|
||||
mock_search_client.delete_documents = Mock()
|
||||
mock_search_client.delete_documents.return_value = [{"status": True, "id": "doc1"}]
|
||||
mock_search_client.merge_or_upload_documents = Mock()
|
||||
mock_search_client.merge_or_upload_documents.return_value = [{"status": True, "id": "doc1"}]
|
||||
mock_search_client.get_document = Mock()
|
||||
mock_search_client.close = Mock()
|
||||
|
||||
# Stub required methods on index_client.
|
||||
mock_index_client.create_or_update_index = Mock()
|
||||
mock_index_client.list_indexes = Mock()
|
||||
mock_index_client.list_index_names = Mock(return_value=["test-index"])
|
||||
mock_index_client.delete_index = Mock()
|
||||
# For col_info() we assume get_index returns an object with name and fields attributes.
|
||||
fake_index = Mock()
|
||||
fake_index.name = "test-index"
|
||||
fake_index.fields = ["id", "vector", "payload", "user_id", "run_id", "agent_id"]
|
||||
mock_index_client.get_index = Mock(return_value=fake_index)
|
||||
mock_index_client.close = Mock()
|
||||
|
||||
yield mock_search_client, mock_index_client, MockAzureKeyCredential
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def azure_ai_search_instance(mock_clients):
|
||||
mock_search_client, mock_index_client, _ = mock_clients
|
||||
# Create an instance with dummy parameters.
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="test-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=3,
|
||||
compression_type="binary", # testing binary quantization option
|
||||
use_float16=True
|
||||
)
|
||||
# Return instance and clients for verification.
|
||||
return instance, mock_search_client, mock_index_client
|
||||
|
||||
|
||||
# --- Tests for AzureAISearchConfig ---
|
||||
|
||||
def test_config_validation_valid():
|
||||
"""Test valid configurations are accepted."""
|
||||
# Test minimal configuration
|
||||
config = AzureAISearchConfig(
|
||||
service_name="test-service",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768
|
||||
)
|
||||
assert config.collection_name == "mem0" # Default value
|
||||
assert config.service_name == "test-service"
|
||||
assert config.api_key == "test-api-key"
|
||||
assert config.embedding_model_dims == 768
|
||||
assert config.compression_type is None
|
||||
assert config.use_float16 is False
|
||||
|
||||
# Test with all optional parameters
|
||||
config = AzureAISearchConfig(
|
||||
collection_name="custom-index",
|
||||
service_name="test-service",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=1536,
|
||||
compression_type="scalar",
|
||||
use_float16=True
|
||||
)
|
||||
assert config.collection_name == "custom-index"
|
||||
assert config.compression_type == "scalar"
|
||||
assert config.use_float16 is True
|
||||
|
||||
|
||||
def test_config_validation_invalid_compression_type():
|
||||
"""Test that invalid compression types are rejected."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
AzureAISearchConfig(
|
||||
service_name="test-service",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
compression_type="invalid-type" # Not a valid option
|
||||
)
|
||||
assert "Invalid compression_type" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_config_validation_deprecated_use_compression():
|
||||
"""Test that using the deprecated use_compression parameter raises an error."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
AzureAISearchConfig(
|
||||
service_name="test-service",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
use_compression=True # Deprecated parameter
|
||||
)
|
||||
# Fix: Use a partial string match instead of exact match
|
||||
assert "use_compression" in str(exc_info.value)
|
||||
assert "no longer supported" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_config_validation_extra_fields():
|
||||
"""Test that extra fields are rejected."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
AzureAISearchConfig(
|
||||
service_name="test-service",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
unknown_parameter="value" # Extra field
|
||||
)
|
||||
assert "Extra fields not allowed" in str(exc_info.value)
|
||||
assert "unknown_parameter" in str(exc_info.value)
|
||||
|
||||
|
||||
# --- Tests for AzureAISearch initialization ---
|
||||
|
||||
def test_initialization(mock_clients):
|
||||
"""Test AzureAISearch initialization with different parameters."""
|
||||
mock_search_client, mock_index_client, mock_azure_key_credential = mock_clients
|
||||
|
||||
# Test with minimal parameters
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="test-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768
|
||||
)
|
||||
|
||||
# Verify initialization parameters
|
||||
assert instance.index_name == "test-index"
|
||||
assert instance.collection_name == "test-index"
|
||||
assert instance.embedding_model_dims == 768
|
||||
assert instance.compression_type == "none" # Default when None is passed
|
||||
assert instance.use_float16 is False
|
||||
|
||||
# Verify client creation
|
||||
mock_azure_key_credential.assert_called_with("test-api-key")
|
||||
assert "mem0" in mock_search_client._client._config.user_agent_policy.add_user_agent.call_args[0]
|
||||
assert "mem0" in mock_index_client._client._config.user_agent_policy.add_user_agent.call_args[0]
|
||||
|
||||
# Verify index creation was called
|
||||
mock_index_client.create_or_update_index.assert_called_once()
|
||||
|
||||
|
||||
def test_initialization_with_compression_types(mock_clients):
|
||||
"""Test initialization with different compression types."""
|
||||
mock_search_client, mock_index_client, _ = mock_clients
|
||||
|
||||
# Test with scalar compression
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="scalar-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
compression_type="scalar"
|
||||
)
|
||||
assert instance.compression_type == "scalar"
|
||||
|
||||
# Capture the index creation call
|
||||
args, _ = mock_index_client.create_or_update_index.call_args_list[-1]
|
||||
index = args[0]
|
||||
# Verify scalar compression was configured
|
||||
assert hasattr(index.vector_search, 'compressions')
|
||||
assert len(index.vector_search.compressions) > 0
|
||||
assert "ScalarQuantizationCompression" in str(type(index.vector_search.compressions[0]))
|
||||
|
||||
# Test with binary compression
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="binary-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
compression_type="binary"
|
||||
)
|
||||
assert instance.compression_type == "binary"
|
||||
|
||||
# Capture the index creation call
|
||||
args, _ = mock_index_client.create_or_update_index.call_args_list[-1]
|
||||
index = args[0]
|
||||
# Verify binary compression was configured
|
||||
assert hasattr(index.vector_search, 'compressions')
|
||||
assert len(index.vector_search.compressions) > 0
|
||||
assert "BinaryQuantizationCompression" in str(type(index.vector_search.compressions[0]))
|
||||
|
||||
# Test with no compression
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="no-compression-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
compression_type=None
|
||||
)
|
||||
assert instance.compression_type == "none"
|
||||
|
||||
# Capture the index creation call
|
||||
args, _ = mock_index_client.create_or_update_index.call_args_list[-1]
|
||||
index = args[0]
|
||||
# Verify no compression was configured
|
||||
assert hasattr(index.vector_search, 'compressions')
|
||||
assert len(index.vector_search.compressions) == 0
|
||||
|
||||
|
||||
def test_initialization_with_float_precision(mock_clients):
|
||||
"""Test initialization with different float precision settings."""
|
||||
mock_search_client, mock_index_client, _ = mock_clients
|
||||
|
||||
# Test with half precision (float16)
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="float16-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
use_float16=True
|
||||
)
|
||||
assert instance.use_float16 is True
|
||||
|
||||
# Capture the index creation call
|
||||
args, _ = mock_index_client.create_or_update_index.call_args_list[-1]
|
||||
index = args[0]
|
||||
# Find the vector field and check its type
|
||||
vector_field = next((f for f in index.fields if f.name == "vector"), None)
|
||||
assert vector_field is not None
|
||||
assert "Edm.Half" in vector_field.type
|
||||
|
||||
# Test with full precision (float32)
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="float32-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
use_float16=False
|
||||
)
|
||||
assert instance.use_float16 is False
|
||||
|
||||
# Capture the index creation call
|
||||
args, _ = mock_index_client.create_or_update_index.call_args_list[-1]
|
||||
index = args[0]
|
||||
# Find the vector field and check its type
|
||||
vector_field = next((f for f in index.fields if f.name == "vector"), None)
|
||||
assert vector_field is not None
|
||||
assert "Edm.Single" in vector_field.type
|
||||
|
||||
|
||||
# --- Tests for create_col method ---
|
||||
|
||||
def test_create_col(azure_ai_search_instance):
|
||||
"""Test the create_col method creates an index with the correct configuration."""
|
||||
instance, _, mock_index_client = azure_ai_search_instance
|
||||
|
||||
# create_col is called during initialization, so we check the call that was already made
|
||||
mock_index_client.create_or_update_index.assert_called_once()
|
||||
|
||||
# Verify the index configuration
|
||||
args, _ = mock_index_client.create_or_update_index.call_args
|
||||
index = args[0]
|
||||
|
||||
# Check basic properties
|
||||
assert index.name == "test-index"
|
||||
assert len(index.fields) == 6 # id, user_id, run_id, agent_id, vector, payload
|
||||
|
||||
# Check that required fields are present
|
||||
field_names = [f.name for f in index.fields]
|
||||
assert "id" in field_names
|
||||
assert "vector" in field_names
|
||||
assert "payload" in field_names
|
||||
assert "user_id" in field_names
|
||||
assert "run_id" in field_names
|
||||
assert "agent_id" in field_names
|
||||
|
||||
# Check that id is the key field
|
||||
id_field = next(f for f in index.fields if f.name == "id")
|
||||
assert id_field.key is True
|
||||
|
||||
# Check vector search configuration
|
||||
assert index.vector_search is not None
|
||||
assert len(index.vector_search.profiles) == 1
|
||||
assert index.vector_search.profiles[0].name == "my-vector-config"
|
||||
assert index.vector_search.profiles[0].algorithm_configuration_name == "my-algorithms-config"
|
||||
|
||||
# Check algorithms
|
||||
assert len(index.vector_search.algorithms) == 1
|
||||
assert index.vector_search.algorithms[0].name == "my-algorithms-config"
|
||||
assert "HnswAlgorithmConfiguration" in str(type(index.vector_search.algorithms[0]))
|
||||
|
||||
# With binary compression and float16, we should have compression configuration
|
||||
assert len(index.vector_search.compressions) == 1
|
||||
assert index.vector_search.compressions[0].compression_name == "myCompression"
|
||||
assert "BinaryQuantizationCompression" in str(type(index.vector_search.compressions[0]))
|
||||
|
||||
|
||||
def test_create_col_scalar_compression(mock_clients):
|
||||
"""Test creating a collection with scalar compression."""
|
||||
mock_search_client, mock_index_client, _ = mock_clients
|
||||
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="scalar-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
compression_type="scalar"
|
||||
)
|
||||
|
||||
# Verify the index configuration
|
||||
args, _ = mock_index_client.create_or_update_index.call_args
|
||||
index = args[0]
|
||||
|
||||
# Check compression configuration
|
||||
assert len(index.vector_search.compressions) == 1
|
||||
assert index.vector_search.compressions[0].compression_name == "myCompression"
|
||||
assert "ScalarQuantizationCompression" in str(type(index.vector_search.compressions[0]))
|
||||
|
||||
# Check profile references compression
|
||||
assert index.vector_search.profiles[0].compression_name == "myCompression"
|
||||
|
||||
|
||||
def test_create_col_no_compression(mock_clients):
|
||||
"""Test creating a collection with no compression."""
|
||||
mock_search_client, mock_index_client, _ = mock_clients
|
||||
|
||||
instance = AzureAISearch(
|
||||
service_name="test-service",
|
||||
collection_name="no-compression-index",
|
||||
api_key="test-api-key",
|
||||
embedding_model_dims=768,
|
||||
compression_type=None
|
||||
)
|
||||
|
||||
# Verify the index configuration
|
||||
args, _ = mock_index_client.create_or_update_index.call_args
|
||||
index = args[0]
|
||||
|
||||
# Check compression configuration - should be empty
|
||||
assert len(index.vector_search.compressions) == 0
|
||||
|
||||
# Check profile doesn't reference compression
|
||||
assert index.vector_search.profiles[0].compression_name is None
|
||||
|
||||
|
||||
# --- Tests for insert method ---
|
||||
|
||||
def test_insert_single(azure_ai_search_instance):
|
||||
"""Test inserting a single vector."""
|
||||
instance, mock_search_client, _ = azure_ai_search_instance
|
||||
vectors = [[0.1, 0.2, 0.3]]
|
||||
payloads = [{"user_id": "user1", "run_id": "run1", "agent_id": "agent1"}]
|
||||
ids = ["doc1"]
|
||||
|
||||
instance.insert(vectors, payloads, ids)
|
||||
|
||||
# Verify upload_documents was called correctly
|
||||
mock_search_client.upload_documents.assert_called_once()
|
||||
args, _ = mock_search_client.upload_documents.call_args
|
||||
documents = args[0]
|
||||
|
||||
# Verify document structure
|
||||
assert len(documents) == 1
|
||||
assert documents[0]["id"] == "doc1"
|
||||
assert documents[0]["vector"] == [0.1, 0.2, 0.3]
|
||||
assert documents[0]["payload"] == json.dumps(payloads[0])
|
||||
assert documents[0]["user_id"] == "user1"
|
||||
assert documents[0]["run_id"] == "run1"
|
||||
assert documents[0]["agent_id"] == "agent1"
|
||||
|
||||
|
||||
def test_insert_multiple(azure_ai_search_instance):
|
||||
"""Test inserting multiple vectors in one call."""
|
||||
instance, mock_search_client, _ = azure_ai_search_instance
|
||||
|
||||
# Create multiple vectors
|
||||
num_docs = 3
|
||||
vectors = [[float(i)/10, float(i+1)/10, float(i+2)/10] for i in range(num_docs)]
|
||||
payloads = [{"user_id": f"user{i}", "content": f"Test content {i}"} for i in range(num_docs)]
|
||||
ids = [f"doc{i}" for i in range(num_docs)]
|
||||
|
||||
# Configure mock to return success for all documents
|
||||
mock_search_client.upload_documents.return_value = [
|
||||
{"status": True, "id": id_val} for id_val in ids
|
||||
]
|
||||
|
||||
# Insert the documents
|
||||
instance.insert(vectors, payloads, ids)
|
||||
|
||||
# Verify upload_documents was called with correct documents
|
||||
mock_search_client.upload_documents.assert_called_once()
|
||||
args, _ = mock_search_client.upload_documents.call_args
|
||||
documents = args[0]
|
||||
|
||||
# Verify all documents were included
|
||||
assert len(documents) == num_docs
|
||||
|
||||
# Check first document
|
||||
assert documents[0]["id"] == "doc0"
|
||||
assert documents[0]["vector"] == [0.0, 0.1, 0.2]
|
||||
assert documents[0]["payload"] == json.dumps(payloads[0])
|
||||
assert documents[0]["user_id"] == "user0"
|
||||
|
||||
# Check last document
|
||||
assert documents[2]["id"] == "doc2"
|
||||
assert documents[2]["vector"] == [0.2, 0.3, 0.4]
|
||||
assert documents[2]["payload"] == json.dumps(payloads[2])
|
||||
assert documents[2]["user_id"] == "user2"
|
||||
|
||||
|
||||
def test_insert_with_error(azure_ai_search_instance):
|
||||
"""Test insert when Azure returns an error for one or more documents."""
|
||||
instance, mock_search_client, _ = azure_ai_search_instance
|
||||
|
||||
# Configure mock to return an error for one document
|
||||
mock_search_client.upload_documents.return_value = [
|
||||
{"status": False, "id": "doc1", "errorMessage": "Azure error"}
|
||||
]
|
||||
|
||||
vectors = [[0.1, 0.2, 0.3]]
|
||||
payloads = [{"user_id": "user1"}]
|
||||
ids = ["doc1"]
|
||||
|
||||
# Insert should raise an exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
instance.insert(vectors, payloads, ids)
|
||||
|
||||
assert "Insert failed for document doc1" in str(exc_info.value)
|
||||
|
||||
# Configure mock to return mixed success/failure for multiple documents
|
||||
mock_search_client.upload_documents.return_value = [
|
||||
{"status": True, "id": "doc1"},
|
||||
{"status": False, "id": "doc2", "errorMessage": "Azure error"}
|
||||
]
|
||||
|
||||
vectors = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
|
||||
payloads = [{"user_id": "user1"}, {"user_id": "user2"}]
|
||||
ids = ["doc1", "doc2"]
|
||||
|
||||
# Insert should raise an exception
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
instance.insert(vectors, payloads, ids)
|
||||
|
||||
assert "Insert failed for document doc2" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_insert_with_missing_payload_fields(azure_ai_search_instance):
|
||||
"""Test inserting with payloads missing some of the expected fields."""
|
||||
instance, mock_search_client, _ = azure_ai_search_instance
|
||||
vectors = [[0.1, 0.2, 0.3]]
|
||||
payloads = [{"content": "Some content without user_id, run_id, or agent_id"}]
|
||||
ids = ["doc1"]
|
||||
|
||||
instance.insert(vectors, payloads, ids)
|
||||
|
||||
# Verify upload_documents was called correctly
|
||||
mock_search_client.upload_documents.assert_called_once()
|
||||
args, _ = mock_search_client.upload_documents.call_args
|
||||
documents = args[0]
|
||||
|
||||
# Verify document has payload but not the extra fields
|
||||
assert len(documents) == 1
|
||||
assert documents[0]["id"] == "doc1"
|
||||
assert documents[0]["vector"] == [0.1, 0.2, 0.3]
|
||||
assert documents[0]["payload"] == json.dumps(payloads[0])
|
||||
assert "user_id" not in documents[0]
|
||||
assert "run_id" not in documents[0]
|
||||
assert "agent_id" not in documents[0]
|
||||
|
||||
|
||||
def test_insert_with_http_error(azure_ai_search_instance):
|
||||
"""Test insert when Azure client throws an HTTP error."""
|
||||
instance, mock_search_client, _ = azure_ai_search_instance
|
||||
|
||||
# Configure mock to raise an HttpResponseError
|
||||
mock_search_client.upload_documents.side_effect = HttpResponseError("Azure service error")
|
||||
|
||||
vectors = [[0.1, 0.2, 0.3]]
|
||||
payloads = [{"user_id": "user1"}]
|
||||
ids = ["doc1"]
|
||||
|
||||
# Insert should propagate the HTTP error
|
||||
with pytest.raises(HttpResponseError) as exc_info:
|
||||
instance.insert(vectors, payloads, ids)
|
||||
|
||||
assert "Azure service error" in str(exc_info.value)
|
||||
|
||||
|
||||
# --- Tests for search method ---
|
||||
|
||||
def test_search_basic(azure_ai_search_instance):
|
||||
"""Test basic vector search without filters."""
|
||||
instance, mock_search_client, _ = azure_ai_search_instance
|
||||
|
||||
# Configure mock to return search results
|
||||
mock_search_client.search.return_value = [
|
||||
{
|
||||
"id": "doc1",
|
||||
"@search.score": 0.95,
|
||||
"payload": json.dumps({"content": "Test content"})
|
||||
}
|
||||
]
|
||||
|
||||
# Search with a vector
|
||||
query_vector = [0.1, 0.2, 0.3]
|
||||
results = instance.search(query_vector, limit=5)
|
||||
|
||||
# Verify search was called correctly
|
||||
mock_search_client.search.assert_called_once()
|
||||
_, kwargs = mock_search_client.search.call_args
|
||||
|
||||
# Check parameters
|
||||
assert len(kwargs["vector_queries"]) == 1
|
||||
assert kwargs["vector_queries"][0].vector == query_vector
|
||||
assert kwargs["vector_queries"][0].k_nearest_neighbors == 5
|
||||
assert kwargs["vector_queries"][0].fields == "vector"
|
||||
assert kwargs["filter"] is None # No filters
|
||||
assert kwargs["top"] == 5
|
||||
assert kwargs["vector_filter_mode"] == "preFilter" # Default mode
|
||||
|
||||
# Check results
|
||||
assert len(results) == 1
|
||||
assert results[0].id == "doc1"
|
||||
assert results[0].score == 0.95
|
||||
assert results[0].payload == {"content": "Test content"}
|
||||
@@ -35,12 +35,11 @@ def test_search_vectors(chromadb_instance, mock_chromadb_client):
|
||||
}
|
||||
chromadb_instance.collection.query.return_value = mock_result
|
||||
|
||||
query = [[0.1, 0.2, 0.3]]
|
||||
results = chromadb_instance.search(query=query, limit=2)
|
||||
vectors = [[0.1, 0.2, 0.3]]
|
||||
results = chromadb_instance.search(query="", vectors=vectors, limit=2)
|
||||
|
||||
chromadb_instance.collection.query.assert_called_once_with(query_embeddings=query, where=None, n_results=2)
|
||||
chromadb_instance.collection.query.assert_called_once_with(query_embeddings=vectors, where=None, n_results=2)
|
||||
|
||||
print(results, type(results))
|
||||
assert len(results) == 2
|
||||
assert results[0].id == "id1"
|
||||
assert results[0].score == 0.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import dotenv
|
||||
|
||||
@@ -196,8 +196,8 @@ class TestElasticsearchDB(unittest.TestCase):
|
||||
self.client_mock.search.return_value = mock_response
|
||||
|
||||
# Perform search
|
||||
query_vector = [0.1] * 1536
|
||||
results = self.es_db.search(query=query_vector, limit=5)
|
||||
vectors = [[0.1] * 1536]
|
||||
results = self.es_db.search(query="", vectors=vectors, limit=5)
|
||||
|
||||
# Verify search call
|
||||
self.client_mock.search.assert_called_once()
|
||||
@@ -210,7 +210,7 @@ class TestElasticsearchDB(unittest.TestCase):
|
||||
# Verify KNN query structure
|
||||
self.assertIn("knn", body)
|
||||
self.assertEqual(body["knn"]["field"], "vector")
|
||||
self.assertEqual(body["knn"]["query_vector"], query_vector)
|
||||
self.assertEqual(body["knn"]["query_vector"], vectors)
|
||||
self.assertEqual(body["knn"]["k"], 5)
|
||||
self.assertEqual(body["knn"]["num_candidates"], 10)
|
||||
|
||||
@@ -220,6 +220,23 @@ class TestElasticsearchDB(unittest.TestCase):
|
||||
self.assertEqual(results[0].score, 0.8)
|
||||
self.assertEqual(results[0].payload, {"key1": "value1"})
|
||||
|
||||
def test_custom_search_query(self):
|
||||
# Mock custom search query
|
||||
self.es_db.custom_search_query = Mock()
|
||||
self.es_db.custom_search_query.return_value = {"custom_key": "custom_value"}
|
||||
|
||||
# Perform search
|
||||
vectors = [[0.1] * 1536]
|
||||
limit = 5
|
||||
filters = {"key1": "value1"}
|
||||
self.es_db.search(query="", vectors=vectors, limit=limit, filters=filters)
|
||||
|
||||
# Verify custom search query function was called
|
||||
self.es_db.custom_search_query.assert_called_once_with(vectors, limit, filters)
|
||||
|
||||
# Verify custom search query was used
|
||||
self.client_mock.search.assert_called_once_with(index=self.es_db.collection_name, body={"custom_key": "custom_value"})
|
||||
|
||||
def test_get(self):
|
||||
# Mock get response with correct structure
|
||||
mock_response = {
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
||||
import dotenv
|
||||
|
||||
try:
|
||||
from opensearchpy import OpenSearch
|
||||
from opensearchpy import OpenSearch, AWSV4SignerAuth
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"OpenSearch requires extra dependencies. Install with `pip install opensearch-py`"
|
||||
@@ -126,15 +126,15 @@ class TestOpenSearchDB(unittest.TestCase):
|
||||
def test_search(self):
|
||||
mock_response = {"hits": {"hits": [{"_id": "id1", "_score": 0.8, "_source": {"vector": [0.1] * 1536, "metadata": {"key1": "value1"}}}]}}
|
||||
self.client_mock.search.return_value = mock_response
|
||||
query_vector = [0.1] * 1536
|
||||
results = self.os_db.search(query=query_vector, limit=5)
|
||||
vectors = [[0.1] * 1536]
|
||||
results = self.os_db.search(query="", vectors=vectors, limit=5)
|
||||
self.client_mock.search.assert_called_once()
|
||||
search_args = self.client_mock.search.call_args[1]
|
||||
self.assertEqual(search_args["index"], "test_collection")
|
||||
body = search_args["body"]
|
||||
self.assertIn("knn", body["query"])
|
||||
self.assertIn("vector", body["query"]["knn"])
|
||||
self.assertEqual(body["query"]["knn"]["vector"]["vector"], query_vector)
|
||||
self.assertEqual(body["query"]["knn"]["vector"]["vector"], vectors)
|
||||
self.assertEqual(body["query"]["knn"]["vector"]["k"], 5)
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0].id, "id1")
|
||||
@@ -148,3 +148,29 @@ class TestOpenSearchDB(unittest.TestCase):
|
||||
def test_delete_col(self):
|
||||
self.os_db.delete_col()
|
||||
self.client_mock.indices.delete.assert_called_once_with(index="test_collection")
|
||||
|
||||
|
||||
def test_init_with_http_auth(self):
|
||||
mock_credentials = MagicMock()
|
||||
mock_signer = AWSV4SignerAuth(mock_credentials, "us-east-1", "es")
|
||||
|
||||
with patch('mem0.vector_stores.opensearch.OpenSearch') as mock_opensearch:
|
||||
test_db = OpenSearchDB(
|
||||
host="localhost",
|
||||
port=9200,
|
||||
collection_name="test_collection",
|
||||
embedding_model_dims=1536,
|
||||
http_auth=mock_signer,
|
||||
verify_certs=True,
|
||||
use_ssl=True,
|
||||
auto_create_index=False
|
||||
)
|
||||
|
||||
# Verify OpenSearch was initialized with correct params
|
||||
mock_opensearch.assert_called_once_with(
|
||||
hosts=[{"host": "localhost", "port": 9200}],
|
||||
http_auth=mock_signer,
|
||||
use_ssl=True,
|
||||
verify_certs=True,
|
||||
connection_class=unittest.mock.ANY
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0.vector_stores.pinecone import PineconeDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pinecone_client():
|
||||
client = MagicMock()
|
||||
client.Index.return_value = MagicMock()
|
||||
client.list_indexes.return_value.names.return_value = []
|
||||
return client
|
||||
|
||||
@pytest.fixture
|
||||
def pinecone_db(mock_pinecone_client):
|
||||
return PineconeDB(
|
||||
collection_name="test_index",
|
||||
embedding_model_dims=128,
|
||||
client=mock_pinecone_client,
|
||||
api_key="fake_api_key",
|
||||
environment="us-west1-gcp",
|
||||
serverless_config=None,
|
||||
pod_config=None,
|
||||
hybrid_search=False,
|
||||
metric="cosine",
|
||||
batch_size=100,
|
||||
extra_params=None
|
||||
)
|
||||
|
||||
def test_create_col_existing_index(mock_pinecone_client):
|
||||
# Set up the mock before creating the PineconeDB object
|
||||
mock_pinecone_client.list_indexes.return_value.names.return_value = ["test_index"]
|
||||
|
||||
pinecone_db = PineconeDB(
|
||||
collection_name="test_index",
|
||||
embedding_model_dims=128,
|
||||
client=mock_pinecone_client,
|
||||
api_key="fake_api_key",
|
||||
environment="us-west1-gcp",
|
||||
serverless_config=None,
|
||||
pod_config=None,
|
||||
hybrid_search=False,
|
||||
metric="cosine",
|
||||
batch_size=100,
|
||||
extra_params=None
|
||||
)
|
||||
|
||||
# Reset the mock to verify it wasn't called during the test
|
||||
mock_pinecone_client.create_index.reset_mock()
|
||||
|
||||
pinecone_db.create_col(128, "cosine")
|
||||
|
||||
mock_pinecone_client.create_index.assert_not_called()
|
||||
|
||||
def test_create_col_new_index(pinecone_db, mock_pinecone_client):
|
||||
mock_pinecone_client.list_indexes.return_value.names.return_value = []
|
||||
pinecone_db.create_col(128, "cosine")
|
||||
mock_pinecone_client.create_index.assert_called()
|
||||
|
||||
def test_insert_vectors(pinecone_db):
|
||||
vectors = [[0.1] * 128, [0.2] * 128]
|
||||
payloads = [{"name": "vector1"}, {"name": "vector2"}]
|
||||
ids = ["id1", "id2"]
|
||||
pinecone_db.insert(vectors, payloads, ids)
|
||||
pinecone_db.index.upsert.assert_called()
|
||||
|
||||
def test_search_vectors(pinecone_db):
|
||||
pinecone_db.index.query.return_value.matches = [{"id": "id1", "score": 0.9, "metadata": {"name": "vector1"}}]
|
||||
results = pinecone_db.search([0.1] * 128, limit=1)
|
||||
assert len(results) == 1
|
||||
assert results[0].id == "id1"
|
||||
assert results[0].score == 0.9
|
||||
|
||||
def test_update_vector(pinecone_db):
|
||||
pinecone_db.update("id1", vector=[0.5] * 128, payload={"name": "updated"})
|
||||
pinecone_db.index.upsert.assert_called()
|
||||
|
||||
def test_get_vector_found(pinecone_db):
|
||||
# Looking at the _parse_output method, it expects a Vector object
|
||||
# or a list of dictionaries, not a dictionary with an 'id' field
|
||||
|
||||
# Create a mock Vector object
|
||||
from pinecone.data.dataclasses.vector import Vector
|
||||
mock_vector = Vector(
|
||||
id="id1",
|
||||
values=[0.1] * 128,
|
||||
metadata={"name": "vector1"}
|
||||
)
|
||||
|
||||
# Mock the fetch method to return the mock response object
|
||||
mock_response = MagicMock()
|
||||
mock_response.vectors = {"id1": mock_vector}
|
||||
pinecone_db.index.fetch.return_value = mock_response
|
||||
|
||||
result = pinecone_db.get("id1")
|
||||
assert result is not None
|
||||
assert result.id == "id1"
|
||||
assert result.payload == {"name": "vector1"}
|
||||
|
||||
def test_delete_vector(pinecone_db):
|
||||
pinecone_db.delete("id1")
|
||||
pinecone_db.index.delete.assert_called_with(ids=["id1"])
|
||||
|
||||
def test_get_vector_not_found(pinecone_db):
|
||||
pinecone_db.index.fetch.return_value.vectors = {}
|
||||
result = pinecone_db.get("id1")
|
||||
assert result is None
|
||||
|
||||
def test_list_cols(pinecone_db):
|
||||
pinecone_db.list_cols()
|
||||
pinecone_db.client.list_indexes.assert_called()
|
||||
|
||||
def test_delete_col(pinecone_db):
|
||||
pinecone_db.delete_col()
|
||||
pinecone_db.client.delete_index.assert_called_with("test_index")
|
||||
|
||||
def test_col_info(pinecone_db):
|
||||
pinecone_db.col_info()
|
||||
pinecone_db.client.describe_index.assert_called_with("test_index")
|
||||
@@ -50,15 +50,15 @@ class TestQdrant(unittest.TestCase):
|
||||
self.assertEqual(points[0].payload, payloads[0])
|
||||
|
||||
def test_search(self):
|
||||
query_vector = [0.1, 0.2]
|
||||
vectors = [[0.1, 0.2]]
|
||||
mock_point = MagicMock(id=str(uuid.uuid4()), score=0.95, payload={"key": "value"})
|
||||
self.client_mock.query_points.return_value = MagicMock(points=[mock_point])
|
||||
|
||||
results = self.qdrant.search(query=query_vector, limit=1)
|
||||
results = self.qdrant.search(query="", vectors=vectors, limit=1)
|
||||
|
||||
self.client_mock.query_points.assert_called_once_with(
|
||||
collection_name="test_collection",
|
||||
query=query_vector,
|
||||
query=vectors,
|
||||
query_filter=None,
|
||||
limit=1,
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user