Compare commits

..
21 Commits
Author SHA1 Message Date
Dev KhantandGitHub 0e0be18ecc Fix azure ai vector store (#2396) 2025-03-18 14:13:19 +05:30
Wonbin KimandGitHub 66d3f9b93c Support Custom Prompt for Memory Action Decision (#2371) 2025-03-18 10:43:01 +05:30
Wonbin KimandGitHub b8f40f728f Support Custom Search Query for Elasticsearch (#2372) 2025-03-18 10:34:34 +05:30
Prateek ChhikaraandGitHub 00a2ea9ff0 Added export instructions to docs (#2394) 2025-03-17 17:41:56 -07:00
Prateek ChhikaraandGitHub 9545836469 Added docs for add-v2 (#2381) 2025-03-17 15:39:17 -07:00
Saket AryanandGitHub 3acd9e20da Fix Redis Search (#2392) 2025-03-17 15:30:40 -07:00
Dev KhantandGitHub d48ecd52ef update poetry lock file (#2391) 2025-03-18 01:11:05 +05:30
Saket AryanandGitHub 2fbea7705b Add Intercom to Docs (#2390) 2025-03-17 12:37:56 -07:00
d7a26bd0c3 Add infer param and version bump (#2389)
Co-authored-by: Deshraj Yadav <deshrajdry@gmail.com>
2025-03-18 01:04:58 +05:30
Farzad SunavalaandGitHub e25dc4b504 bugfix: update Azure AI Search Config (#2380) 2025-03-17 22:19:46 +05:30
Parshva DaftariandGitHub dab3349990 Neo4j embeddings error (#2377) 2025-03-17 21:57:23 +05:30
Saket AryanandGitHub 6db87e8d07 Make DEMO UI Responsive (#2382) 2025-03-14 20:00:28 -07:00
Saket AryanandGitHub faf811ee2d Added Custom Categories in Mem0-TS (#2370) 2025-03-14 22:07:46 +05:30
Anusha YellaandGitHub ee80a43810 Remove tools from LLMs (#2363) 2025-03-14 17:42:48 +05:30
Dev KhantandGitHub 4be426f762 version bump -> 0.1.68 (#2369) 2025-03-12 21:22:49 +05:30
Farzad SunavalaandGitHub ba9c61938b feat: enhance Azure AI Search Integration with Binary Quantization, Pre/Post Filter Options, and user agent header (#2354) 2025-03-12 21:20:25 +05:30
Parshva DaftariandGitHub 65f826e064 Fix langchain neo4j deprecation warning (#2350) 2025-03-12 15:30:45 +05:30
Saket AryanandGitHub b43363cdf3 OpenAI Inbuilt Tools (#2362) 2025-03-11 15:33:49 -07:00
Prateek ChhikaraandGitHub 89e786a88e Added agentic tool in docs (#2361) 2025-03-11 13:36:20 -07:00
Saket AryanandGitHub 2d5062bd40 Updated Demo (#2360) 2025-03-12 01:49:08 +05:30
Parshva DaftariandGitHub b89628322d WeaviateDB Integration (#2339) 2025-03-11 00:12:17 +05:30
71 changed files with 3891 additions and 1721 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ install:
install_all:
poetry install
poetry run pip install groq together boto3 litellm ollama chromadb sentence_transformers vertexai \
poetry run pip install groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
google-generativeai elasticsearch opensearch-py vecs
# Format code with ruff
@@ -1,12 +1,14 @@
[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
### Usage
[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
```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 +17,8 @@ config = {
"service_name": "ai-search-test",
"api_key": "*****",
"collection_name": "mem0",
"embedding_model_dims": 1536 ,
"use_compression": False
"embedding_model_dims": 1536,
"compression_type": "none"
}
}
}
@@ -25,20 +27,46 @@ 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
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
}
}
}
```
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 |
## 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` |
## 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
- **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,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` |
+1
View File
@@ -25,6 +25,7 @@ See the list of supported vector databases below.
<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
+11 -3
View File
@@ -47,6 +47,7 @@
"pages": [
"features/platform-overview",
"features/advanced-retrieval",
"features/contextual-add",
"features/multimodal-support",
"features/selective-memory",
"features/custom-categories",
@@ -71,7 +72,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"
]
@@ -130,7 +132,8 @@
"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 +188,9 @@
"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"
]
}
]
@@ -333,6 +338,9 @@
"posthog": {
"apiKey": "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX",
"apiHost": "https://mango.mem0.ai"
},
"intercom": {
"appId": "jjv2r0tt"
}
}
}
+226
View File
@@ -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)
+312
View File
@@ -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)
+8
View File
@@ -58,4 +58,12 @@ 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>
</CardGroup>
+205
View File
@@ -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" />
@@ -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 |
+24 -4
View File
@@ -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"
```
+3
View File
@@ -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>
+7 -7
View File
@@ -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"
+9 -3
View File
@@ -85,14 +85,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 +372,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 +410,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>
+12 -6
View File
@@ -932,27 +932,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"
@@ -4809,6 +4809,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
}
}
},
+45 -41
View File
@@ -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"
}'
```
@@ -1004,17 +1008,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,7 +1031,7 @@ 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,
@@ -1038,7 +1042,7 @@ 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,
@@ -1049,7 +1053,7 @@ 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,
@@ -1072,7 +1076,7 @@ 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,
@@ -1083,7 +1087,7 @@ 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,
@@ -1094,7 +1098,7 @@ 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,
@@ -1133,7 +1137,7 @@ 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,
@@ -1152,55 +1156,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,7 +1217,7 @@ 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,
@@ -1224,7 +1228,7 @@ 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,
@@ -1585,7 +1589,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",
+15 -10
View File
@@ -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>
);
};
+83
View File
@@ -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"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.1.1",
"version": "2.1.5",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
+4
View File
@@ -205,6 +205,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/`,
+3
View File
@@ -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 {
+1 -1
View File
@@ -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";
+48 -20
View File
@@ -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];
+2 -2
View File
@@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional, Union
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 +110,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}")
+6 -2
View File
@@ -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,
)
+165 -145
View File
@@ -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.
"""
"""
+35 -9
View File
@@ -1,27 +1,53 @@
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)"
)
@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
# 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 = {
"arbitrary_types_allowed": True,
}
}
+6 -1
View File
@@ -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,10 @@ 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
+42
View File
@@ -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,
}
+18 -15
View File
@@ -4,14 +4,26 @@ from typing import Dict, List, Optional
try:
import anthropic
except ImportError:
raise ImportError("The 'anthropic' library is required. Please install it using 'pip install anthropic'.")
raise ImportError(
"The 'anthropic' library is required. Please install it using 'pip install anthropic'."
)
from mem0.configs.llms.base import BaseLlmConfig
from mem0.llms.base import LLMBase
class AnthropicLLM(LLMBase):
"""
A class for interacting with Anthropic's Claude models using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the AnthropicLLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
if not self.config.model:
@@ -23,23 +35,17 @@ class AnthropicLLM(LLMBase):
def generate_response(
self,
messages: List[Dict[str, str]],
response_format=None,
tools: Optional[List[Dict]] = None,
tool_choice: str = "auto",
):
) -> str:
"""
Generate a response based on the given messages using Anthropic.
Generates a response using Anthropic's Claude model based on the provided messages.
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.
Returns:
str: The generated response.
str: The generated response from the model.
"""
# Separate system message from other messages
# Extract system message separately
system_message = ""
filtered_messages = []
for message in messages:
@@ -56,9 +62,6 @@ class AnthropicLLM(LLMBase):
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
if tools: # TODO: Remove tools if no issues found with new memory addition logic
params["tools"] = tools
params["tool_choice"] = tool_choice
response = self.client.messages.create(**params)
return response.content[0].text
+50 -126
View File
@@ -4,14 +4,26 @@ from typing import Any, Dict, List, Optional
try:
import boto3
except ImportError:
raise ImportError("The 'boto3' library is required. Please install it using 'pip install boto3'.")
raise ImportError(
"The 'boto3' library is required. Please install it using 'pip install boto3'."
)
from mem0.configs.llms.base import BaseLlmConfig
from mem0.llms.base import LLMBase
class AWSBedrockLLM(LLMBase):
"""
A wrapper for AWS Bedrock's language models, integrating them with the LLMBase class.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the AWS Bedrock LLM with the provided configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration object for the model.
"""
super().__init__(config)
if not self.config.model:
@@ -25,49 +37,29 @@ class AWSBedrockLLM(LLMBase):
def _format_messages(self, messages: List[Dict[str, str]]) -> str:
"""
Formats a list of messages into the required prompt structure for the model.
Formats a list of messages into a structured prompt for the model.
Args:
messages (List[Dict[str, str]]): A list of dictionaries where each dictionary represents a message.
Each dictionary contains 'role' and 'content' keys.
messages (List[Dict[str, str]]): A list of dictionaries containing 'role' and 'content'.
Returns:
str: A formatted string combining all messages, structured with roles capitalized and separated by newlines.
"""
formatted_messages = []
for message in messages:
role = message["role"].capitalize()
content = message["content"]
formatted_messages.append(f"\n\n{role}: {content}")
formatted_messages = [
f"\n\n{msg['role'].capitalize()}: {msg['content']}" for msg in messages
]
return "".join(formatted_messages) + "\n\nAssistant:"
def _parse_response(self, response, tools) -> str:
def _parse_response(self, response) -> str:
"""
Process the response based on whether tools are used or not.
Extracts the generated response from the API response.
Args:
response: The raw response from API.
tools: The list of tools provided in the request.
response: The raw response from the AWS Bedrock API.
Returns:
str or dict: The processed response.
str: The generated response text.
"""
if tools:
processed_response = {"tool_calls": []}
if response["output"]["message"]["content"]:
for item in response["output"]["message"]["content"]:
if "toolUse" in item:
processed_response["tool_calls"].append(
{
"name": item["toolUse"]["name"],
"arguments": item["toolUse"]["input"],
}
)
return processed_response
response_body = json.loads(response["body"].read().decode())
return response_body.get("completion", "")
@@ -76,22 +68,21 @@ class AWSBedrockLLM(LLMBase):
provider: str,
model: str,
prompt: str,
model_kwargs: Optional[Dict[str, Any]] = {},
model_kwargs: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Prepares the input dictionary for the specified provider's model by mapping and renaming
keys in the input based on the provider's requirements.
Prepares the input dictionary for the specified provider's model.
Args:
provider (str): The name of the service provider (e.g., "meta", "ai21", "mistral", "cohere", "amazon").
model (str): The name or identifier of the model being used.
prompt (str): The text prompt to be processed by the model.
model_kwargs (Dict[str, Any]): Additional keyword arguments specific to the model's requirements.
provider (str): The model provider (e.g., "meta", "ai21", "mistral", "cohere", "amazon").
model (str): The model identifier.
prompt (str): The input prompt.
model_kwargs (Optional[Dict[str, Any]]): Additional model parameters.
Returns:
Dict[str, Any]: The prepared input dictionary with the correct keys and values for the specified provider.
Dict[str, Any]: The prepared input dictionary.
"""
model_kwargs = model_kwargs or {}
input_body = {"prompt": prompt, **model_kwargs}
provider_mappings = {
@@ -119,102 +110,35 @@ class AWSBedrockLLM(LLMBase):
},
}
input_body["textGenerationConfig"] = {
k: v for k, v in input_body["textGenerationConfig"].items() if v is not None
k: v
for k, v in input_body["textGenerationConfig"].items()
if v is not None
}
return input_body
def _convert_tool_format(self, original_tools):
def generate_response(self, messages: List[Dict[str, str]]) -> str:
"""
Converts a list of tools from their original format to a new standardized format.
Generates a response using AWS Bedrock based on the provided messages.
Args:
original_tools (list): A list of dictionaries representing the original tools, each containing a 'type' key and corresponding details.
messages (List[Dict[str, str]]): List of message dictionaries containing 'role' and 'content'.
Returns:
list: A list of dictionaries representing the tools in the new standardized format.
str: The generated response text.
"""
new_tools = []
prompt = self._format_messages(messages)
provider = self.config.model.split(".")[0]
input_body = self._prepare_input(
provider, self.config.model, prompt, self.model_kwargs
)
body = json.dumps(input_body)
for tool in original_tools:
if tool["type"] == "function":
function = tool["function"]
new_tool = {
"toolSpec": {
"name": function["name"],
"description": function["description"],
"inputSchema": {
"json": {
"type": "object",
"properties": {},
"required": function["parameters"].get("required", []),
}
},
}
}
response = self.client.invoke_model(
body=body,
modelId=self.config.model,
accept="application/json",
contentType="application/json",
)
for prop, details in function["parameters"].get("properties", {}).items():
new_tool["toolSpec"]["inputSchema"]["json"]["properties"][prop] = {
"type": details.get("type", "string"),
"description": details.get("description", ""),
}
new_tools.append(new_tool)
return new_tools
def generate_response(
self,
messages: List[Dict[str, str]],
response_format=None,
tools: Optional[List[Dict]] = None,
tool_choice: str = "auto",
):
"""
Generate a response based on the given messages using AWS Bedrock.
Args:
messages (list): List of message dicts containing 'role' and 'content'.
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.
"""
if tools:
# Use converse method when tools are provided
messages = [
{
"role": "user",
"content": [{"text": message["content"]} for message in messages],
}
]
inference_config = {
"temperature": self.model_kwargs["temperature"],
"maxTokens": self.model_kwargs["max_tokens_to_sample"],
"topP": self.model_kwargs["top_p"],
}
tools_config = {"tools": self._convert_tool_format(tools)}
response = self.client.converse(
modelId=self.config.model,
messages=messages,
inferenceConfig=inference_config,
toolConfig=tools_config,
)
else:
# Use invoke_model method when no tools are provided
prompt = self._format_messages(messages)
provider = self.model.split(".")[0]
input_body = self._prepare_input(provider, self.config.model, prompt, **self.model_kwargs)
body = json.dumps(input_body)
response = self.client.invoke_model(
body=body,
modelId=self.model,
accept="application/json",
contentType="application/json",
)
return self._parse_response(response, tools)
return self._parse_response(response)
+31 -50
View File
@@ -9,17 +9,35 @@ from mem0.llms.base import LLMBase
class AzureOpenAILLM(LLMBase):
"""
A class for interacting with Azure OpenAI models using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the AzureOpenAILLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
# Model name should match the custom deployment name chosen for it.
# Ensure model name is set; it should match the Azure OpenAI deployment name.
if not self.config.model:
self.config.model = "gpt-4o"
api_key = self.config.azure_kwargs.api_key or os.getenv("LLM_AZURE_OPENAI_API_KEY")
azure_deployment = self.config.azure_kwargs.azure_deployment or os.getenv("LLM_AZURE_DEPLOYMENT")
azure_endpoint = self.config.azure_kwargs.azure_endpoint or os.getenv("LLM_AZURE_ENDPOINT")
api_version = self.config.azure_kwargs.api_version or os.getenv("LLM_AZURE_API_VERSION")
api_key = self.config.azure_kwargs.api_key or os.getenv(
"LLM_AZURE_OPENAI_API_KEY"
)
azure_deployment = self.config.azure_kwargs.azure_deployment or os.getenv(
"LLM_AZURE_DEPLOYMENT"
)
azure_endpoint = self.config.azure_kwargs.azure_endpoint or os.getenv(
"LLM_AZURE_ENDPOINT"
)
api_version = self.config.azure_kwargs.api_version or os.getenv(
"LLM_AZURE_API_VERSION"
)
default_headers = self.config.azure_kwargs.default_headers
self.client = AzureOpenAI(
@@ -31,54 +49,20 @@ class AzureOpenAILLM(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.
Generates a response using Azure OpenAI based on the provided messages.
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.
str: The generated response from the model.
"""
params = {
"model": self.config.model,
@@ -87,11 +71,8 @@ class AzureOpenAILLM(LLMBase):
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
if response_format:
params["response_format"] = response_format
if tools: # TODO: Remove tools if no issues found with new memory addition logic
params["tools"] = tools
params["tool_choice"] = tool_choice
response = self.client.chat.completions.create(**params)
return self._parse_response(response, tools)
return response.choices[0].message.content
+32 -50
View File
@@ -9,20 +9,38 @@ from mem0.llms.base import LLMBase
class AzureOpenAIStructuredLLM(LLMBase):
"""
A class for interacting with Azure OpenAI models using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the AzureOpenAIStructuredLLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
# Model name should match the custom deployment name chosen for it.
# Ensure model name is set; it should match the Azure OpenAI deployment name.
if not self.config.model:
self.config.model = "gpt-4o-2024-08-06"
api_key = os.getenv("LLM_AZURE_OPENAI_API_KEY") or self.config.azure_kwargs.api_key
azure_deployment = os.getenv("LLM_AZURE_DEPLOYMENT") or self.config.azure_kwargs.azure_deployment
azure_endpoint = os.getenv("LLM_AZURE_ENDPOINT") or self.config.azure_kwargs.azure_endpoint
api_version = os.getenv("LLM_AZURE_API_VERSION") or self.config.azure_kwargs.api_version
api_key = (
os.getenv("LLM_AZURE_OPENAI_API_KEY") or self.config.azure_kwargs.api_key
)
azure_deployment = (
os.getenv("LLM_AZURE_DEPLOYMENT")
or self.config.azure_kwargs.azure_deployment
)
azure_endpoint = (
os.getenv("LLM_AZURE_ENDPOINT") or self.config.azure_kwargs.azure_endpoint
)
api_version = (
os.getenv("LLM_AZURE_API_VERSION") or self.config.azure_kwargs.api_version
)
default_headers = self.config.azure_kwargs.default_headers
# Can display a warning if API version is of model and api-version
self.client = AzureOpenAI(
azure_deployment=azure_deployment,
azure_endpoint=azure_endpoint,
@@ -32,54 +50,20 @@ 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.
Generates a response using Azure OpenAI based on the provided messages.
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.
str: The generated response from the model.
"""
params = {
"model": self.config.model,
@@ -88,11 +72,9 @@ class AzureOpenAIStructuredLLM(LLMBase):
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
if response_format:
params["response_format"] = response_format
if tools:
params["tools"] = tools
params["tool_choice"] = tool_choice
response = self.client.chat.completions.create(**params)
return self._parse_response(response, tools)
return response.choices[0].message.content
+6 -2
View File
@@ -4,8 +4,12 @@ from pydantic import BaseModel, Field, field_validator
class LlmConfig(BaseModel):
provider: str = Field(description="Provider of the LLM (e.g., 'ollama', 'openai')", default="openai")
config: Optional[dict] = Field(description="Configuration for the specific LLM", default={})
provider: str = Field(
description="Provider of the LLM (e.g., 'ollama', 'openai')", default="openai"
)
config: Optional[dict] = Field(
description="Configuration for the specific LLM", default={}
)
@field_validator("config")
def validate_config(cls, v, values):
+20 -47
View File
@@ -9,64 +9,42 @@ from mem0.llms.base import LLMBase
class DeepSeekLLM(LLMBase):
"""
A class for interacting with DeepSeek's language models using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the DeepSeekLLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
if not self.config.model:
self.config.model = "deepseek-chat"
api_key = self.config.api_key or os.getenv("DEEPSEEK_API_KEY")
base_url = self.config.deepseek_base_url or os.getenv("DEEPSEEK_API_BASE") or "https://api.deepseek.com"
base_url = (
self.config.deepseek_base_url
or os.getenv("DEEPSEEK_API_BASE")
or "https://api.deepseek.com"
)
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: 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",
):
) -> str:
"""
Generate a response based on the given messages using DeepSeek.
Generates a response using DeepSeek based on the provided messages.
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.
Returns:
str: The generated response.
str: The generated response from the model.
"""
params = {
"model": self.config.model,
@@ -75,10 +53,5 @@ class DeepSeekLLM(LLMBase):
"max_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
if tools:
params["tools"] = tools
params["tool_choice"] = tool_choice
response = self.client.chat.completions.create(**params)
return self._parse_response(response, tools)
return response.choices[0].message.content
+28 -102
View File
@@ -3,8 +3,7 @@ from typing import Dict, List, Optional
try:
import google.generativeai as genai
from google.generativeai import GenerativeModel, protos
from google.generativeai.types import content_types
from google.generativeai import GenerativeModel
except ImportError:
raise ImportError(
"The 'google-generativeai' library is required. Please install it using 'pip install google-generativeai'."
@@ -15,7 +14,17 @@ from mem0.llms.base import LLMBase
class GeminiLLM(LLMBase):
"""
A wrapper for Google's Gemini language model, integrating it with the LLMBase class.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the Gemini LLM with the provided configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration object for the model.
"""
super().__init__(config)
if not self.config.model:
@@ -25,51 +34,25 @@ class GeminiLLM(LLMBase):
genai.configure(api_key=api_key)
self.client = GenerativeModel(model_name=self.config.model)
def _parse_response(self, response, tools):
def _reformat_messages(
self, messages: List[Dict[str, str]]
) -> List[Dict[str, str]]:
"""
Process the response based on whether tools are used or not.
Reformats messages to match the Gemini API's expected structure.
Args:
response: The raw response from API.
tools: The list of tools provided in the request.
messages (List[Dict[str, str]]): A list of messages with 'role' and 'content' keys.
Returns:
str or dict: The processed response.
"""
if tools:
processed_response = {
"content": (content if (content := response.candidates[0].content.parts[0].text) else None),
"tool_calls": [],
}
for part in response.candidates[0].content.parts:
if fn := part.function_call:
if isinstance(fn, protos.FunctionCall):
fn_call = type(fn).to_dict(fn)
processed_response["tool_calls"].append({"name": fn_call["name"], "arguments": fn_call["args"]})
continue
processed_response["tool_calls"].append({"name": fn.name, "arguments": fn.args})
return processed_response
else:
return response.candidates[0].content.parts[0].text
def _reformat_messages(self, messages: List[Dict[str, str]]):
"""
Reformat messages for Gemini.
Args:
messages: The list of messages provided in the request.
Returns:
list: The list of messages in the required format.
List[Dict[str, str]]: Reformatted messages in the required format.
"""
new_messages = []
for message in messages:
if message["role"] == "system":
content = "THIS IS A SYSTEM PROMPT. YOU MUST OBEY THIS: " + message["content"]
content = (
"THIS IS A SYSTEM PROMPT. YOU MUST OBEY THIS: " + message["content"]
)
else:
content = message["content"]
@@ -82,90 +65,33 @@ class GeminiLLM(LLMBase):
return new_messages
def _reformat_tools(self, tools: Optional[List[Dict]]):
"""
Reformat tools for Gemini.
Args:
tools: The list of tools provided in the request.
Returns:
list: The list of tools in the required format.
"""
def remove_additional_properties(data):
"""Recursively removes 'additionalProperties' from nested dictionaries."""
if isinstance(data, dict):
filtered_dict = {
key: remove_additional_properties(value)
for key, value in data.items()
if not (key == "additionalProperties")
}
return filtered_dict
else:
return data
new_tools = []
if tools:
for tool in tools:
func = tool["function"].copy()
new_tools.append({"function_declarations": [remove_additional_properties(func)]})
# TODO: temporarily ignore it to pass tests, will come back to update according to standards later.
# return content_types.to_function_library(new_tools)
return new_tools
else:
return None
def generate_response(
self,
messages: List[Dict[str, str]],
response_format=None,
tools: Optional[List[Dict]] = None,
tool_choice: str = "auto",
):
self, messages: List[Dict[str, str]], response_format: Optional[Dict] = None
) -> str:
"""
Generate a response based on the given messages using Gemini.
Generates a response from Gemini based on the given conversation history.
Args:
messages (list): List of message dicts containing 'role' and 'content'.
response_format (str or object, optional): Format for 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]]): List of message dictionaries containing 'role' and 'content'.
response_format (Optional[Dict]): Specifies the response format (e.g., JSON schema).
Returns:
str: The generated response.
str: The generated response as text.
"""
params = {
"temperature": self.config.temperature,
"max_output_tokens": self.config.max_tokens,
"top_p": self.config.top_p,
}
if response_format is not None and response_format["type"] == "json_object":
if response_format and response_format.get("type") == "json_object":
params["response_mime_type"] = "application/json"
if "schema" in response_format:
params["response_schema"] = response_format["schema"]
if tool_choice:
tool_config = content_types.to_tool_config(
{
"function_calling_config": {
"mode": tool_choice,
"allowed_function_names": (
[tool["function"]["name"] for tool in tools] if tool_choice == "any" else None
),
}
}
)
response = self.client.generate_content(
contents=self._reformat_messages(messages),
tools=self._reformat_tools(tools),
generation_config=genai.GenerationConfig(**params),
tool_config=tool_config,
)
return self._parse_response(response, tools)
return response.candidates[0].content.parts[0].text
+20 -46
View File
@@ -5,14 +5,26 @@ from typing import Dict, List, Optional
try:
from groq import Groq
except ImportError:
raise ImportError("The 'groq' library is required. Please install it using 'pip install groq'.")
raise ImportError(
"The 'groq' library is required. Please install it using 'pip install groq'."
)
from mem0.configs.llms.base import BaseLlmConfig
from mem0.llms.base import LLMBase
class GroqLLM(LLMBase):
"""
A class for interacting with Groq's language models using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the GroqLLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
if not self.config.model:
@@ -21,54 +33,20 @@ class GroqLLM(LLMBase):
api_key = self.config.api_key or os.getenv("GROQ_API_KEY")
self.client = Groq(api_key=api_key)
def _parse_response(self, response, tools):
"""
Process the response based on whether tools are used or not.
Args:
response: The raw response from API.
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 Groq.
Generates a response using Groq based on the provided messages.
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.
str: The generated response from the model.
"""
params = {
"model": self.config.model,
@@ -79,9 +57,5 @@ class GroqLLM(LLMBase):
}
if response_format:
params["response_format"] = response_format
if tools:
params["tools"] = tools
params["tool_choice"] = tool_choice
response = self.client.chat.completions.create(**params)
return self._parse_response(response, tools)
return response.choices[0].message.content
+23 -46
View File
@@ -4,70 +4,50 @@ from typing import Dict, List, Optional
try:
import litellm
except ImportError:
raise ImportError("The 'litellm' library is required. Please install it using 'pip install litellm'.")
raise ImportError(
"The 'litellm' library is required. Please install it using 'pip install litellm'."
)
from mem0.configs.llms.base import BaseLlmConfig
from mem0.llms.base import LLMBase
class LiteLLM(LLMBase):
"""
A class for interacting with LiteLLM's language models using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the LiteLLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
if not self.config.model:
self.config.model = "gpt-4o-mini"
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 Litellm.
Generates a response using LiteLLM based on the provided messages.
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.
str: The generated response from the model.
"""
if not litellm.supports_function_calling(self.config.model):
raise ValueError(f"Model '{self.config.model}' in litellm does not support function calling.")
raise ValueError(
f"Model '{self.config.model}' in LiteLLM does not support function calling."
)
params = {
"model": self.config.model,
@@ -78,9 +58,6 @@ class LiteLLM(LLMBase):
}
if response_format:
params["response_format"] = response_format
if tools: # TODO: Remove tools if no issues found with new memory addition logic
params["tools"] = tools
params["tool_choice"] = tool_choice
response = litellm.completion(**params)
return self._parse_response(response, tools)
return response.choices[0].message.content
+22 -46
View File
@@ -3,77 +3,56 @@ from typing import Dict, List, Optional
try:
from ollama import Client
except ImportError:
raise ImportError("The 'ollama' library is required. Please install it using 'pip install ollama'.")
raise ImportError(
"The 'ollama' library is required. Please install it using 'pip install ollama'."
)
from mem0.configs.llms.base import BaseLlmConfig
from mem0.llms.base import LLMBase
class OllamaLLM(LLMBase):
"""
A class for interacting with Ollama's language models using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the OllamaLLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
if not self.config.model:
self.config.model = "llama3.1:70b"
self.client = Client(host=self.config.ollama_base_url)
self._ensure_model_exists()
def _ensure_model_exists(self):
"""
Ensure the specified model exists locally. If not, pull it from Ollama.
Ensures the specified model exists locally. If not, pulls it from Ollama.
"""
local_models = self.client.list()["models"]
if not any(model.get("name") == self.config.model for model in local_models):
self.client.pull(self.config.model)
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["message"]["content"],
"tool_calls": [],
}
if response["message"].get("tool_calls"):
for tool_call in response["message"]["tool_calls"]:
processed_response["tool_calls"].append(
{
"name": tool_call["function"]["name"],
"arguments": tool_call["function"]["arguments"],
}
)
return processed_response
else:
return response["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.
Generates a response using Ollama based on the provided messages.
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.
str: The generated response from the model.
"""
params = {
"model": self.config.model,
@@ -87,8 +66,5 @@ class OllamaLLM(LLMBase):
if response_format:
params["format"] = "json"
if tools:
params["tools"] = tools
response = self.client.chat(**params)
return self._parse_response(response, tools)
return response["message"]["content"]
+22 -45
View File
@@ -9,7 +9,17 @@ from mem0.llms.base import LLMBase
class OpenAILLM(LLMBase):
"""
A class to interact with OpenAI or OpenRouter APIs for generating responses using LLMs.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the OpenAILLM instance.
Args:
config (Optional[BaseLlmConfig]): Configuration for the LLM, including model, API key, and base URLs.
"""
super().__init__(config)
if not self.config.model:
@@ -24,57 +34,27 @@ class OpenAILLM(LLMBase):
)
else:
api_key = self.config.api_key or os.getenv("OPENAI_API_KEY")
base_url = self.config.openai_base_url or os.getenv("OPENAI_API_BASE") or "https://api.openai.com/v1"
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: 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 OpenAI.
Generates a response based on the provided messages using OpenAI or OpenRouter.
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 message dictionaries containing 'role' and 'content'.
response_format (Optional[str]): The format of the response. Defaults to None.
Returns:
str: The generated response.
str: The generated response from the model.
"""
params = {
"model": self.config.model,
@@ -102,9 +82,6 @@ class OpenAILLM(LLMBase):
if response_format:
params["response_format"] = response_format
if tools: # TODO: Remove tools if no issues found with new memory addition logic
params["tools"] = tools
params["tool_choice"] = tool_choice
response = self.client.chat.completions.create(**params)
return self._parse_response(response, tools)
return response.choices[0].message.content
+23 -48
View File
@@ -9,66 +9,45 @@ from mem0.llms.base import LLMBase
class OpenAIStructuredLLM(LLMBase):
"""
A class for interacting with OpenAI's structured language models using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the OpenAIStructuredLLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
if not self.config.model:
self.config.model = "gpt-4o-2024-08-06"
api_key = self.config.api_key or os.getenv("OPENAI_API_KEY")
base_url = self.config.openai_base_url or os.getenv("OPENAI_API_BASE") or "https://api.openai.com/v1"
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.
Generates a response using OpenAI based on the provided messages.
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.
str: The generated response from the model.
"""
params = {
"model": self.config.model,
@@ -78,10 +57,6 @@ class OpenAIStructuredLLM(LLMBase):
if response_format:
params["response_format"] = response_format
if tools:
params["tools"] = tools
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
+20 -45
View File
@@ -5,14 +5,26 @@ from typing import Dict, List, Optional
try:
from together import Together
except ImportError:
raise ImportError("The 'together' library is required. Please install it using 'pip install together'.")
raise ImportError(
"The 'together' library is required. Please install it using 'pip install together'."
)
from mem0.configs.llms.base import BaseLlmConfig
from mem0.llms.base import LLMBase
class TogetherLLM(LLMBase):
"""
A class for interacting with the TogetherAI language model using the specified configuration.
"""
def __init__(self, config: Optional[BaseLlmConfig] = None):
"""
Initializes the TogetherLLM instance with the given configuration.
Args:
config (Optional[BaseLlmConfig]): Configuration settings for the language model.
"""
super().__init__(config)
if not self.config.model:
@@ -21,54 +33,20 @@ class TogetherLLM(LLMBase):
api_key = self.config.api_key or os.getenv("TOGETHER_API_KEY")
self.client = Together(api_key=api_key)
def _parse_response(self, response, tools):
"""
Process the response based on whether tools are used or not.
Args:
response: The raw response from API.
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 TogetherAI.
Generates a response using TogetherAI based on the provided messages.
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.
str: The generated response from the model.
"""
params = {
"model": self.config.model,
@@ -79,9 +57,6 @@ class TogetherLLM(LLMBase):
}
if response_format:
params["response_format"] = response_format
if tools: # TODO: Remove tools if no issues found with new memory addition logic
params["tools"] = tools
params["tool_choice"] = tool_choice
response = self.client.chat.completions.create(**params)
return self._parse_response(response, tools)
return response.choices[0].message.content
+5 -1
View File
@@ -15,7 +15,11 @@ class XAILLM(LLMBase):
self.config.model = "grok-2-latest"
api_key = self.config.api_key or os.getenv("XAI_API_KEY")
base_url = self.config.xai_base_url or os.getenv("XAI_API_BASE") or "https://api.x.ai/v1"
base_url = (
self.config.xai_base_url
or os.getenv("XAI_API_BASE")
or "https://api.x.ai/v1"
)
self.client = OpenAI(api_key=api_key, base_url=base_url)
def generate_response(self, messages: List[Dict[str, str]], response_format=None):
+7 -10
View File
@@ -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
+37 -16
View File
@@ -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,27 @@ 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 +87,7 @@ class Memory(MemoryBase):
run_id=None,
metadata=None,
filters=None,
infer=True,
prompt=None,
):
"""
@@ -83,6 +100,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 +139,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 +165,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)
@@ -195,7 +222,7 @@ 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 +332,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 +395,7 @@ class Memory(MemoryBase):
"data",
"created_at",
"updated_at",
"id",
}
all_memories = [
{
@@ -469,6 +489,7 @@ class Memory(MemoryBase):
"data",
"created_at",
"updated_at",
"id",
}
original_memories = [
@@ -655,4 +676,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.")
+1
View File
@@ -72,6 +72,7 @@ class VectorStoreFactory:
"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
+139 -51
View File
@@ -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,82 @@ 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,
):
"""
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.)
"""
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.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),
@@ -82,8 +123,8 @@ class AzureAISearch(VectorStoreBase):
SearchField(
name="vector",
type=vector_type,
searchable=True,
vector_search_dimensions=vector_dimensions,
searchable=True,
vector_search_dimensions=self.embedding_model_dims,
vector_search_profile_name="my-vector-config",
),
SimpleField(name="payload", type=SearchFieldDataType.String, searchable=True),
@@ -91,7 +132,11 @@ class AzureAISearch(VectorStoreBase):
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 +146,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,61 +163,84 @@ 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._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.
"""
Search for similar vectors.
Args:
query (List[float]): Query vectors.
query (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=query, k_nearest_neighbors=limit, fields="vector"
)
search_results = self.search_client.search(
vector_queries=[vector_query],
filter=filter_expression,
top=limit
)
results = []
for result in search_results:
payload = json.loads(result["payload"])
results.append(OutputData(id=result["id"], score=result["@search.score"], payload=payload))
results.append(
OutputData(
id=result["id"], score=result["@search.score"], payload=payload
)
)
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 +255,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.
@@ -200,35 +275,43 @@ class AzureAISearch(VectorStoreBase):
result = self.search_client.get_document(key=vector_id)
except ResourceNotFoundError:
return None
return OutputData(id=result["id"], score=None, payload=json.loads(result["payload"]))
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:
@@ -238,12 +321,17 @@ class AzureAISearch(VectorStoreBase):
if filters:
filter_expression = self._build_filter_expression(filters)
search_results = self.search_client.search(search_text="*", filter=filter_expression, top=limit)
search_results = self.search_client.search(
search_text="*", filter=filter_expression, top=limit
)
results = []
for result in search_results:
payload = json.loads(result["payload"])
results.append(OutputData(id=result["id"], score=result["@search.score"], payload=payload))
results.append(
OutputData(
id=result["id"], score=result["@search.score"], payload=payload
)
)
return [results]
def __del__(self):
+1
View File
@@ -21,6 +21,7 @@ class VectorStoreConfig(BaseModel):
"vertex_ai_vector_search": "GoogleMatchingEngineConfig",
"opensearch": "OpenSearchConfig",
"supabase": "SupabaseConfig",
"weaviate": "WeaviateConfig",
}
@model_validator(mode="after")
+18 -18
View File
@@ -45,6 +45,11 @@ class ElasticsearchDB(VectorStoreBase):
# Create index only if auto_create_index is True
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"""
@@ -117,25 +122,20 @@ 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}}
"""
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(query, 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}},
}
}
search_query = {"knn": {"field": "vector", "query_vector": query, "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["filter"] = {"bool": {"must": filter_conditions}}
response = self.client.search(index=self.collection_name, body=search_query)
+1 -1
View File
@@ -1,6 +1,6 @@
import logging
import uuid
from typing import List, Optional, Dict, Any
from typing import List, Optional
from pydantic import BaseModel
+307
View File
@@ -0,0 +1,307 @@
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: 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=query,
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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mem0ai"
version = "0.1.67"
version = "0.1.70"
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"
+17
View File
@@ -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)
+11 -53
View File
@@ -20,8 +20,10 @@ def mock_openai_client():
yield mock_client
def test_generate_response_without_tools(mock_openai_client):
config = BaseLlmConfig(model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, top_p=TOP_P)
def test_generate_response(mock_openai_client):
config = BaseLlmConfig(
model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, top_p=TOP_P
)
llm = AzureOpenAILLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
@@ -29,67 +31,21 @@ def test_generate_response_without_tools(mock_openai_client):
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="I'm doing well, thank you for asking!"))]
mock_response.choices = [
Mock(message=Mock(content="I'm doing well, thank you for asking!"))
]
mock_openai_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages)
mock_openai_client.chat.completions.create.assert_called_once_with(
model=MODEL, messages=messages, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, top_p=TOP_P
)
assert response == "I'm doing well, thank you for asking!"
def test_generate_response_with_tools(mock_openai_client):
config = BaseLlmConfig(model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, top_p=TOP_P)
llm = AzureOpenAILLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Add a new memory: Today is a sunny day."},
]
tools = [
{
"type": "function",
"function": {
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {"data": {"type": "string", "description": "Data to add to memory"}},
"required": ["data"],
},
},
}
]
mock_response = Mock()
mock_message = Mock()
mock_message.content = "I've added the memory for you."
mock_tool_call = Mock()
mock_tool_call.function.name = "add_memory"
mock_tool_call.function.arguments = '{"data": "Today is a sunny day."}'
mock_message.tool_calls = [mock_tool_call]
mock_response.choices = [Mock(message=mock_message)]
mock_openai_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages, tools=tools)
mock_openai_client.chat.completions.create.assert_called_once_with(
model=MODEL,
messages=messages,
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS,
top_p=TOP_P,
tools=tools,
tool_choice="auto",
)
assert response["content"] == "I've added the memory for you."
assert len(response["tool_calls"]) == 1
assert response["tool_calls"][0]["name"] == "add_memory"
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
assert response == "I'm doing well, thank you for asking!"
@pytest.mark.parametrize(
@@ -128,4 +84,6 @@ def test_generate_with_http_proxies(default_headers):
api_version=None,
default_headers=default_headers,
)
mock_http_client.assert_called_once_with(proxies="http://testproxy.mem0.net:8000")
mock_http_client.assert_called_once_with(
proxies="http://testproxy.mem0.net:8000"
)
+32 -64
View File
@@ -16,33 +16,47 @@ def mock_deepseek_client():
def test_deepseek_llm_base_url():
# case1: default config with deepseek official base url
config = BaseLlmConfig(model="deepseek-chat", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key")
config = BaseLlmConfig(
model="deepseek-chat",
temperature=0.7,
max_tokens=100,
top_p=1.0,
api_key="api_key",
)
llm = DeepSeekLLM(config)
assert str(llm.client.base_url) == "https://api.deepseek.com"
# case2: with env variable DEEPSEEK_API_BASE
provider_base_url = "https://api.provider.com/v1/"
os.environ["DEEPSEEK_API_BASE"] = provider_base_url
config = BaseLlmConfig(model="deepseek-chat", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key")
config = BaseLlmConfig(
model="deepseek-chat",
temperature=0.7,
max_tokens=100,
top_p=1.0,
api_key="api_key",
)
llm = DeepSeekLLM(config)
assert str(llm.client.base_url) == provider_base_url
# case3: with config.deepseek_base_url
config_base_url = "https://api.config.com/v1/"
config = BaseLlmConfig(
model="deepseek-chat",
temperature=0.7,
max_tokens=100,
top_p=1.0,
api_key="api_key",
deepseek_base_url=config_base_url
model="deepseek-chat",
temperature=0.7,
max_tokens=100,
top_p=1.0,
api_key="api_key",
deepseek_base_url=config_base_url,
)
llm = DeepSeekLLM(config)
assert str(llm.client.base_url) == config_base_url
def test_generate_response_without_tools(mock_deepseek_client):
config = BaseLlmConfig(model="deepseek-chat", temperature=0.7, max_tokens=100, top_p=1.0)
def test_generate_response(mock_deepseek_client):
config = BaseLlmConfig(
model="deepseek-chat", temperature=0.7, max_tokens=100, top_p=1.0
)
llm = DeepSeekLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
@@ -50,64 +64,18 @@ def test_generate_response_without_tools(mock_deepseek_client):
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="I'm doing well, thank you for asking!"))]
mock_response.choices = [
Mock(message=Mock(content="I'm doing well, thank you for asking!"))
]
mock_deepseek_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages)
mock_deepseek_client.chat.completions.create.assert_called_once_with(
model="deepseek-chat", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0
model="deepseek-chat",
messages=messages,
temperature=0.7,
max_tokens=100,
top_p=1.0,
)
assert response == "I'm doing well, thank you for asking!"
def test_generate_response_with_tools(mock_deepseek_client):
config = BaseLlmConfig(model="deepseek-chat", temperature=0.7, max_tokens=100, top_p=1.0)
llm = DeepSeekLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Add a new memory: Today is a sunny day."},
]
tools = [
{
"type": "function",
"function": {
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {"data": {"type": "string", "description": "Data to add to memory"}},
"required": ["data"],
},
},
}
]
mock_response = Mock()
mock_message = Mock()
mock_message.content = "I've added the memory for you."
mock_tool_call = Mock()
mock_tool_call.function.name = "add_memory"
mock_tool_call.function.arguments = '{"data": "Today is a sunny day."}'
mock_message.tool_calls = [mock_tool_call]
mock_response.choices = [Mock(message=mock_message)]
mock_deepseek_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages, tools=tools)
mock_deepseek_client.chat.completions.create.assert_called_once_with(
model="deepseek-chat",
messages=messages,
temperature=0.7,
max_tokens=100,
top_p=1.0,
tools=tools,
tool_choice="auto"
)
assert response["content"] == "I've added the memory for you."
assert len(response["tool_calls"]) == 1
assert response["tool_calls"][0]["name"] == "add_memory"
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
+9 -79
View File
@@ -17,7 +17,9 @@ def mock_gemini_client():
def test_generate_response_without_tools(mock_gemini_client: Mock):
config = BaseLlmConfig(model="gemini-1.5-flash-latest", temperature=0.7, max_tokens=100, top_p=1.0)
config = BaseLlmConfig(
model="gemini-1.5-flash-latest", temperature=0.7, max_tokens=100, top_p=1.0
)
llm = GeminiLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
@@ -34,86 +36,14 @@ def test_generate_response_without_tools(mock_gemini_client: Mock):
mock_gemini_client.generate_content.assert_called_once_with(
contents=[
{"parts": "THIS IS A SYSTEM PROMPT. YOU MUST OBEY THIS: You are a helpful assistant.", "role": "user"},
{
"parts": "THIS IS A SYSTEM PROMPT. YOU MUST OBEY THIS: You are a helpful assistant.",
"role": "user",
},
{"parts": "Hello, how are you?", "role": "user"},
],
generation_config=GenerationConfig(temperature=0.7, max_output_tokens=100, top_p=1.0),
tools=None,
tool_config=content_types.to_tool_config(
{"function_calling_config": {"mode": "auto", "allowed_function_names": None}}
generation_config=GenerationConfig(
temperature=0.7, max_output_tokens=100, top_p=1.0
),
)
assert response == "I'm doing well, thank you for asking!"
def test_generate_response_with_tools(mock_gemini_client: Mock):
config = BaseLlmConfig(model="gemini-1.5-flash-latest", temperature=0.7, max_tokens=100, top_p=1.0)
llm = GeminiLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Add a new memory: Today is a sunny day."},
]
tools = [
{
"type": "function",
"function": {
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {"data": {"type": "string", "description": "Data to add to memory"}},
"required": ["data"],
},
},
}
]
mock_tool_call = Mock()
mock_tool_call.name = "add_memory"
mock_tool_call.args = {"data": "Today is a sunny day."}
mock_part = Mock()
mock_part.function_call = mock_tool_call
mock_part.text = "I've added the memory for you."
mock_content = Mock()
mock_content.parts = [mock_part]
mock_message = Mock()
mock_message.content = mock_content
mock_response = Mock(candidates=[mock_message])
mock_gemini_client.generate_content.return_value = mock_response
response = llm.generate_response(messages, tools=tools)
mock_gemini_client.generate_content.assert_called_once_with(
contents=[
{"parts": "THIS IS A SYSTEM PROMPT. YOU MUST OBEY THIS: You are a helpful assistant.", "role": "user"},
{"parts": "Add a new memory: Today is a sunny day.", "role": "user"},
],
generation_config=GenerationConfig(temperature=0.7, max_output_tokens=100, top_p=1.0),
tools=[
{
"function_declarations": [
{
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {"data": {"type": "string", "description": "Data to add to memory"}},
"required": ["data"],
},
}
]
}
],
tool_config=content_types.to_tool_config(
{"function_calling_config": {"mode": "auto", "allowed_function_names": None}}
),
)
assert response["content"] == "I've added the memory for you."
assert len(response["tool_calls"]) == 1
assert response["tool_calls"][0]["name"] == "add_memory"
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
+8 -52
View File
@@ -14,8 +14,10 @@ def mock_groq_client():
yield mock_client
def test_generate_response_without_tools(mock_groq_client):
config = BaseLlmConfig(model="llama3-70b-8192", temperature=0.7, max_tokens=100, top_p=1.0)
def test_generate_response(mock_groq_client):
config = BaseLlmConfig(
model="llama3-70b-8192", temperature=0.7, max_tokens=100, top_p=1.0
)
llm = GroqLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
@@ -23,64 +25,18 @@ def test_generate_response_without_tools(mock_groq_client):
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="I'm doing well, thank you for asking!"))]
mock_response.choices = [
Mock(message=Mock(content="I'm doing well, thank you for asking!"))
]
mock_groq_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages)
mock_groq_client.chat.completions.create.assert_called_once_with(
model="llama3-70b-8192", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0
)
assert response == "I'm doing well, thank you for asking!"
def test_generate_response_with_tools(mock_groq_client):
config = BaseLlmConfig(model="llama3-70b-8192", temperature=0.7, max_tokens=100, top_p=1.0)
llm = GroqLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Add a new memory: Today is a sunny day."},
]
tools = [
{
"type": "function",
"function": {
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {"data": {"type": "string", "description": "Data to add to memory"}},
"required": ["data"],
},
},
}
]
mock_response = Mock()
mock_message = Mock()
mock_message.content = "I've added the memory for you."
mock_tool_call = Mock()
mock_tool_call.function.name = "add_memory"
mock_tool_call.function.arguments = '{"data": "Today is a sunny day."}'
mock_message.tool_calls = [mock_tool_call]
mock_response.choices = [Mock(message=mock_message)]
mock_groq_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages, tools=tools)
mock_groq_client.chat.completions.create.assert_called_once_with(
model="llama3-70b-8192",
messages=messages,
temperature=0.7,
max_tokens=100,
top_p=1.0,
tools=tools,
tool_choice="auto",
)
assert response["content"] == "I've added the memory for you."
assert len(response["tool_calls"]) == 1
assert response["tool_calls"][0]["name"] == "add_memory"
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
assert response == "I'm doing well, thank you for asking!"
+11 -51
View File
@@ -13,17 +13,22 @@ def mock_litellm():
def test_generate_response_with_unsupported_model(mock_litellm):
config = BaseLlmConfig(model="unsupported-model", temperature=0.7, max_tokens=100, top_p=1)
config = BaseLlmConfig(
model="unsupported-model", temperature=0.7, max_tokens=100, top_p=1
)
llm = litellm.LiteLLM(config)
messages = [{"role": "user", "content": "Hello"}]
mock_litellm.supports_function_calling.return_value = False
with pytest.raises(ValueError, match="Model 'unsupported-model' in litellm does not support function calling."):
with pytest.raises(
ValueError,
match="Model 'unsupported-model' in LiteLLM does not support function calling.",
):
llm.generate_response(messages)
def test_generate_response_without_tools(mock_litellm):
def test_generate_response(mock_litellm):
config = BaseLlmConfig(model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1)
llm = litellm.LiteLLM(config)
messages = [
@@ -32,7 +37,9 @@ def test_generate_response_without_tools(mock_litellm):
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="I'm doing well, thank you for asking!"))]
mock_response.choices = [
Mock(message=Mock(content="I'm doing well, thank you for asking!"))
]
mock_litellm.completion.return_value = mock_response
mock_litellm.supports_function_calling.return_value = True
@@ -42,50 +49,3 @@ def test_generate_response_without_tools(mock_litellm):
model="gpt-4o", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0
)
assert response == "I'm doing well, thank you for asking!"
def test_generate_response_with_tools(mock_litellm):
config = BaseLlmConfig(model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1)
llm = litellm.LiteLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Add a new memory: Today is a sunny day."},
]
tools = [
{
"type": "function",
"function": {
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {"data": {"type": "string", "description": "Data to add to memory"}},
"required": ["data"],
},
},
}
]
mock_response = Mock()
mock_message = Mock()
mock_message.content = "I've added the memory for you."
mock_tool_call = Mock()
mock_tool_call.function.name = "add_memory"
mock_tool_call.function.arguments = '{"data": "Today is a sunny day."}'
mock_message.tool_calls = [mock_tool_call]
mock_response.choices = [Mock(message=mock_message)]
mock_litellm.completion.return_value = mock_response
mock_litellm.supports_function_calling.return_value = True
response = llm.generate_response(messages, tools=tools)
mock_litellm.completion.assert_called_once_with(
model="gpt-4o", messages=messages, temperature=0.7, max_tokens=100, top_p=1, tools=tools, tool_choice="auto"
)
assert response["content"] == "I've added the memory for you."
assert len(response["tool_calls"]) == 1
assert response["tool_calls"][0]["name"] == "add_memory"
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
+16 -51
View File
@@ -16,7 +16,9 @@ def mock_openai_client():
def test_openai_llm_base_url():
# case1: default config: with openai official base url
config = BaseLlmConfig(model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key")
config = BaseLlmConfig(
model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key"
)
llm = OpenAILLM(config)
# Note: openai client will parse the raw base_url into a URL object, which will have a trailing slash
assert str(llm.client.base_url) == "https://api.openai.com/v1/"
@@ -24,7 +26,9 @@ def test_openai_llm_base_url():
# case2: with env variable OPENAI_API_BASE
provider_base_url = "https://api.provider.com/v1"
os.environ["OPENAI_API_BASE"] = provider_base_url
config = BaseLlmConfig(model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key")
config = BaseLlmConfig(
model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key"
)
llm = OpenAILLM(config)
# Note: openai client will parse the raw base_url into a URL object, which will have a trailing slash
assert str(llm.client.base_url) == provider_base_url + "/"
@@ -32,14 +36,19 @@ def test_openai_llm_base_url():
# case3: with config.openai_base_url
config_base_url = "https://api.config.com/v1"
config = BaseLlmConfig(
model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1.0, api_key="api_key", openai_base_url=config_base_url
model="gpt-4o",
temperature=0.7,
max_tokens=100,
top_p=1.0,
api_key="api_key",
openai_base_url=config_base_url,
)
llm = OpenAILLM(config)
# Note: openai client will parse the raw base_url into a URL object, which will have a trailing slash
assert str(llm.client.base_url) == config_base_url + "/"
def test_generate_response_without_tools(mock_openai_client):
def test_generate_response(mock_openai_client):
config = BaseLlmConfig(model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1.0)
llm = OpenAILLM(config)
messages = [
@@ -48,7 +57,9 @@ def test_generate_response_without_tools(mock_openai_client):
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="I'm doing well, thank you for asking!"))]
mock_response.choices = [
Mock(message=Mock(content="I'm doing well, thank you for asking!"))
]
mock_openai_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages)
@@ -57,49 +68,3 @@ def test_generate_response_without_tools(mock_openai_client):
model="gpt-4o", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0
)
assert response == "I'm doing well, thank you for asking!"
def test_generate_response_with_tools(mock_openai_client):
config = BaseLlmConfig(model="gpt-4o", temperature=0.7, max_tokens=100, top_p=1.0)
llm = OpenAILLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Add a new memory: Today is a sunny day."},
]
tools = [
{
"type": "function",
"function": {
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {"data": {"type": "string", "description": "Data to add to memory"}},
"required": ["data"],
},
},
}
]
mock_response = Mock()
mock_message = Mock()
mock_message.content = "I've added the memory for you."
mock_tool_call = Mock()
mock_tool_call.function.name = "add_memory"
mock_tool_call.function.arguments = '{"data": "Today is a sunny day."}'
mock_message.tool_calls = [mock_tool_call]
mock_response.choices = [Mock(message=mock_message)]
mock_openai_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages, tools=tools)
mock_openai_client.chat.completions.create.assert_called_once_with(
model="gpt-4o", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0, tools=tools, tool_choice="auto"
)
assert response["content"] == "I've added the memory for you."
assert len(response["tool_calls"]) == 1
assert response["tool_calls"][0]["name"] == "add_memory"
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
+11 -52
View File
@@ -14,8 +14,13 @@ def mock_together_client():
yield mock_client
def test_generate_response_without_tools(mock_together_client):
config = BaseLlmConfig(model="mistralai/Mixtral-8x7B-Instruct-v0.1", temperature=0.7, max_tokens=100, top_p=1.0)
def test_generate_response(mock_together_client):
config = BaseLlmConfig(
model="mistralai/Mixtral-8x7B-Instruct-v0.1",
temperature=0.7,
max_tokens=100,
top_p=1.0,
)
llm = TogetherLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
@@ -23,64 +28,18 @@ def test_generate_response_without_tools(mock_together_client):
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="I'm doing well, thank you for asking!"))]
mock_response.choices = [
Mock(message=Mock(content="I'm doing well, thank you for asking!"))
]
mock_together_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages)
mock_together_client.chat.completions.create.assert_called_once_with(
model="mistralai/Mixtral-8x7B-Instruct-v0.1", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0
)
assert response == "I'm doing well, thank you for asking!"
def test_generate_response_with_tools(mock_together_client):
config = BaseLlmConfig(model="mistralai/Mixtral-8x7B-Instruct-v0.1", temperature=0.7, max_tokens=100, top_p=1.0)
llm = TogetherLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Add a new memory: Today is a sunny day."},
]
tools = [
{
"type": "function",
"function": {
"name": "add_memory",
"description": "Add a memory",
"parameters": {
"type": "object",
"properties": {"data": {"type": "string", "description": "Data to add to memory"}},
"required": ["data"],
},
},
}
]
mock_response = Mock()
mock_message = Mock()
mock_message.content = "I've added the memory for you."
mock_tool_call = Mock()
mock_tool_call.function.name = "add_memory"
mock_tool_call.function.arguments = '{"data": "Today is a sunny day."}'
mock_message.tool_calls = [mock_tool_call]
mock_response.choices = [Mock(message=mock_message)]
mock_together_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages, tools=tools)
mock_together_client.chat.completions.create.assert_called_once_with(
model="mistralai/Mixtral-8x7B-Instruct-v0.1",
messages=messages,
temperature=0.7,
max_tokens=100,
top_p=1.0,
tools=tools,
tool_choice="auto",
)
assert response["content"] == "I've added the memory for you."
assert len(response["tool_calls"]) == 1
assert response["tool_calls"][0]["name"] == "add_memory"
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
assert response == "I'm doing well, thank you for asking!"
+50 -1
View File
@@ -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"},
)
+542
View File
@@ -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"}
+18 -1
View File
@@ -1,6 +1,6 @@
import os
import unittest
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, Mock, patch
import dotenv
@@ -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
query_vector = [0.1] * 1536
limit = 5
filters = {"key1": "value1"}
self.es_db.search(query=query_vector, limit=limit, filters=filters)
# Verify custom search query function was called
self.es_db.custom_search_query.assert_called_once_with(query_vector, 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 = {
+220
View File
@@ -0,0 +1,220 @@
import os
import uuid
import httpx
import unittest
from unittest.mock import MagicMock, patch
import dotenv
import weaviate
from weaviate.classes.query import MetadataQuery, Filter
from weaviate.exceptions import UnexpectedStatusCodeException
from mem0.vector_stores.weaviate import Weaviate, OutputData
class TestWeaviateDB(unittest.TestCase):
@classmethod
def setUpClass(cls):
dotenv.load_dotenv()
cls.original_env = {
'WEAVIATE_CLUSTER_URL': os.getenv('WEAVIATE_CLUSTER_URL', 'http://localhost:8080'),
'WEAVIATE_API_KEY': os.getenv('WEAVIATE_API_KEY', 'test_api_key'),
}
os.environ['WEAVIATE_CLUSTER_URL'] = 'http://localhost:8080'
os.environ['WEAVIATE_API_KEY'] = 'test_api_key'
def setUp(self):
self.client_mock = MagicMock(spec=weaviate.WeaviateClient)
self.client_mock.collections = MagicMock()
self.client_mock.collections.exists.return_value = False
self.client_mock.collections.create.return_value = None
self.client_mock.collections.delete.return_value = None
patcher = patch('mem0.vector_stores.weaviate.weaviate.connect_to_local', return_value=self.client_mock)
self.mock_weaviate = patcher.start()
self.addCleanup(patcher.stop)
self.weaviate_db = Weaviate(
collection_name="test_collection",
embedding_model_dims=1536,
cluster_url=os.getenv('WEAVIATE_CLUSTER_URL'),
auth_client_secret=os.getenv('WEAVIATE_API_KEY'),
additional_headers={"X-OpenAI-Api-Key": "test_key"},
)
self.client_mock.reset_mock()
@classmethod
def tearDownClass(cls):
for key, value in cls.original_env.items():
if value is not None:
os.environ[key] = value
else:
os.environ.pop(key, None)
def tearDown(self):
self.client_mock.reset_mock()
def test_create_col(self):
self.client_mock.collections.exists.return_value = False
self.weaviate_db.create_col(vector_size=1536)
self.client_mock.collections.create.assert_called_once()
self.client_mock.reset_mock()
self.client_mock.collections.exists.return_value = True
self.weaviate_db.create_col(vector_size=1536)
self.client_mock.collections.create.assert_not_called()
def test_insert(self):
self.client_mock.batch = MagicMock()
self.client_mock.batch.fixed_size.return_value.__enter__.return_value = MagicMock()
self.client_mock.collections.get.return_value.data.insert_many.return_value = {
"results": [{"id": "id1"}, {"id": "id2"}]
}
vectors = [[0.1] * 1536, [0.2] * 1536]
payloads = [{"key1": "value1"}, {"key2": "value2"}]
ids = [str(uuid.uuid4()), str(uuid.uuid4())]
results = self.weaviate_db.insert(vectors=vectors, payloads=payloads, ids=ids)
def test_get(self):
valid_uuid = str(uuid.uuid4())
mock_response = MagicMock()
mock_response.properties = {
"hash": "abc123",
"created_at": "2025-03-08T12:00:00Z",
"updated_at": "2025-03-08T13:00:00Z",
"user_id": "user_123",
"agent_id": "agent_456",
"run_id": "run_789",
"data": {"key": "value"},
"category": "test",
}
mock_response.uuid = valid_uuid
self.client_mock.collections.get.return_value.query.fetch_object_by_id.return_value = mock_response
result = self.weaviate_db.get(vector_id=valid_uuid)
assert result.id == valid_uuid
expected_payload = mock_response.properties.copy()
expected_payload["id"] = valid_uuid
assert result.payload == expected_payload
def test_get_not_found(self):
mock_response = httpx.Response(status_code=404, json={"error": "Not found"})
self.client_mock.collections.get.return_value.data.get_by_id.side_effect = UnexpectedStatusCodeException(
"Not found", mock_response
)
def test_search(self):
mock_objects = [
{
"uuid": "id1",
"properties": {"key1": "value1"},
"metadata": {"distance": 0.2}
}
]
mock_response = MagicMock()
mock_response.objects = []
for obj in mock_objects:
mock_obj = MagicMock()
mock_obj.uuid = obj["uuid"]
mock_obj.properties = obj["properties"]
mock_obj.metadata = MagicMock()
mock_obj.metadata.distance = obj["metadata"]["distance"]
mock_response.objects.append(mock_obj)
mock_hybrid = MagicMock()
self.client_mock.collections.get.return_value.query.hybrid = mock_hybrid
mock_hybrid.return_value = mock_response
query_vector = [0.1] * 1536
results = self.weaviate_db.search(query=query_vector, limit=5)
mock_hybrid.assert_called_once()
self.assertEqual(len(results), 1)
self.assertEqual(results[0].id, "id1")
self.assertEqual(results[0].score, 0.8)
def test_delete(self):
self.weaviate_db.delete(vector_id="id1")
self.client_mock.collections.get.return_value.data.delete_by_id.assert_called_once_with("id1")
def test_list(self):
mock_objects = []
mock_obj1 = MagicMock()
mock_obj1.uuid = "id1"
mock_obj1.properties = {"key1": "value1"}
mock_objects.append(mock_obj1)
mock_obj2 = MagicMock()
mock_obj2.uuid = "id2"
mock_obj2.properties = {"key2": "value2"}
mock_objects.append(mock_obj2)
mock_response = MagicMock()
mock_response.objects = mock_objects
mock_fetch = MagicMock()
self.client_mock.collections.get.return_value.query.fetch_objects = mock_fetch
mock_fetch.return_value = mock_response
results = self.weaviate_db.list(limit=10)
mock_fetch.assert_called_once()
# Verify results
self.assertEqual(len(results), 1)
self.assertEqual(len(results[0]), 2)
self.assertEqual(results[0][0].id, "id1")
self.assertEqual(results[0][0].payload["key1"], "value1")
self.assertEqual(results[0][1].id, "id2")
self.assertEqual(results[0][1].payload["key2"], "value2")
def test_list_cols(self):
mock_collection1 = MagicMock()
mock_collection1.name = "collection1"
mock_collection2 = MagicMock()
mock_collection2.name = "collection2"
self.client_mock.collections.list_all.return_value = [mock_collection1, mock_collection2]
result = self.weaviate_db.list_cols()
expected = {"collections": [{"name": "collection1"}, {"name": "collection2"}]}
assert result == expected
self.client_mock.collections.list_all.assert_called_once()
def test_delete_col(self):
self.weaviate_db.delete_col()
self.client_mock.collections.delete.assert_called_once_with("test_collection")
if __name__ == '__main__':
unittest.main()