Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dec9ace88 | ||
|
|
e1dc27276b | ||
|
|
9a12ea7b3c | ||
|
|
e10a509645 | ||
|
|
fe3f10adb8 | ||
|
|
53c91fb107 | ||
|
|
ecc596b11f | ||
|
|
be37fca1bb |
@@ -9,6 +9,9 @@ The v2 get memories API is powerful and flexible, allowing for more precise memo
|
||||
- `lte`: Less than or equal to
|
||||
- `gt`: Greater than
|
||||
- `lt`: Less than
|
||||
- `ne`: Not equal to
|
||||
- `icontains`: Case-insensitive containment check
|
||||
- `*`: Wildcard character that matches everything
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
@@ -41,3 +44,22 @@ memories = m.get_all(
|
||||
]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup>
|
||||
```python Wildcard Example
|
||||
# Using wildcard to get all memories for a specific user across all run_ids
|
||||
memories = m.get_all(
|
||||
filters={
|
||||
"AND": [
|
||||
{
|
||||
"user_id": "alex"
|
||||
},
|
||||
{
|
||||
"run_id": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
version="v2"
|
||||
)
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -11,6 +11,7 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret
|
||||
- `lt`: Less than
|
||||
- `ne`: Not equal to
|
||||
- `icontains`: Case-insensitive containment check
|
||||
- `*`: Wildcard character that matches everything
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
@@ -49,3 +50,23 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup>
|
||||
```python Wildcard Example
|
||||
# Using wildcard to match all run_ids for a specific user
|
||||
all_memories = m.search(
|
||||
query="What are Alice's hobbies?",
|
||||
version="v2",
|
||||
filters={
|
||||
"AND": [
|
||||
{
|
||||
"user_id": "alice"
|
||||
},
|
||||
{
|
||||
"run_id": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -288,6 +288,14 @@ mode: "wide"
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2025-06-06" description="v2.1.30">
|
||||
**New Features:**
|
||||
- **OSS:** Added Cloudflare support
|
||||
|
||||
**Improvements:**
|
||||
- **OSS:** Fixed baseURL param in LLM Config.
|
||||
</Update>
|
||||
|
||||
<Update label="2025-05-30" description="v2.1.29">
|
||||
**Improvements:**
|
||||
- **Client:** Added Async Mode Param for `add` method.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
[Cloudflare Vectorize](https://developers.cloudflare.com/vectorize/) is a vector database offering from Cloudflare, allowing you to build AI-powered applications with vector embeddings.
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeGroup>
|
||||
```typescript TypeScript
|
||||
import { Memory } from 'mem0ai/oss';
|
||||
|
||||
const config = {
|
||||
vectorStore: {
|
||||
provider: 'vectorize',
|
||||
config: {
|
||||
indexName: 'my-memory-index',
|
||||
accountId: 'your-cloudflare-account-id',
|
||||
apiKey: 'your-cloudflare-api-key',
|
||||
dimension: 1536, // Optional: defaults to 1536
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const memory = new Memory(config);
|
||||
const messages = [
|
||||
{"role": "user", "content": "I'm looking for a good book to read."},
|
||||
{"role": "assistant", "content": "Sure, what genre are you interested in?"},
|
||||
{"role": "user", "content": "I enjoy fantasy novels with strong world-building."},
|
||||
{"role": "assistant", "content": "Great! I'll keep that in mind for future recommendations."}
|
||||
]
|
||||
await memory.add(messages, { userId: "bob", metadata: { interest: "books" } });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Config
|
||||
|
||||
Let's see the available parameters for the `vectorize` config:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="TypeScript">
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `indexName` | The name of the Vectorize index | `None` (Required) |
|
||||
| `accountId` | Your Cloudflare account ID | `None` (Required) |
|
||||
| `apiKey` | Your Cloudflare API token | `None` (Required) |
|
||||
| `dimension` | Dimensions of the embedding model | `1536` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -13,7 +13,7 @@ Mem0 includes built-in support for various popular databases. Memory can utilize
|
||||
See the list of supported vector databases below.
|
||||
|
||||
<Note>
|
||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis and in-memory vector database.
|
||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis,Vectorize and in-memory vector database.
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={3}>
|
||||
|
||||
@@ -38,22 +38,75 @@ We suggest you follow the instructions below to set up OpenMemory on your local
|
||||
Getting started with OpenMemory is straight forward and takes just a few minutes to set up on your local machine. Follow these steps:
|
||||
|
||||
### Getting started
|
||||
First clone the repository and then follow the instructions:
|
||||
|
||||
|
||||
### 1. First clone the repository and then follow the instructions:
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/mem0ai/mem0.git
|
||||
cd mem0/openmemory
|
||||
|
||||
# Create the backend .env file with your OpenAI key
|
||||
make env
|
||||
|
||||
# Build the Docker images
|
||||
make build
|
||||
|
||||
# Start all services (API server, vector database, and MCP server components)
|
||||
make up
|
||||
```
|
||||
|
||||
### 2. Set Up Environment Variables
|
||||
|
||||
Before running the project, you need to configure environment variables for both the API and the UI.
|
||||
|
||||
You can do this in one of the following ways:
|
||||
|
||||
- **Manually**:
|
||||
Create a `.env` file in each of the following directories:
|
||||
- `/api/.env`
|
||||
- `/ui/.env`
|
||||
|
||||
- **Using `.env.example` files**:
|
||||
Copy and rename the example files:
|
||||
|
||||
```bash
|
||||
cp api/.env.example api/.env
|
||||
cp ui/.env.example ui/.env
|
||||
```
|
||||
|
||||
- **Using Makefile** (if supported):
|
||||
Run:
|
||||
|
||||
```bash
|
||||
make env
|
||||
```
|
||||
- #### Example `/api/.env`
|
||||
|
||||
``` bash
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
USER=<user-id> # The User Id you want to associate the memories with
|
||||
```
|
||||
- #### Example `/ui/.env`
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8765
|
||||
NEXT_PUBLIC_USER_ID=<user-id> # Same as the user id for environment variable in api
|
||||
```
|
||||
|
||||
### 3. Build and Run the Project
|
||||
You can run the project using the following two commands:
|
||||
```bash
|
||||
make build # builds the mcp server and ui
|
||||
make up # runs openmemory mcp server and ui
|
||||
```
|
||||
|
||||
After running these commands, you will have:
|
||||
- OpenMemory MCP server running at: http://localhost:8765 (API documentation available at http://localhost:8765/docs)
|
||||
- OpenMemory UI running at: http://localhost:3000
|
||||
|
||||
#### UI not working on http://localhost:3000?
|
||||
|
||||
If the UI does not start properly on http://localhost:3000, try running it manually:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
|
||||
You can configure the MCP client using the following command (replace username with your username):
|
||||
|
||||
```bash
|
||||
|
||||
@@ -458,7 +458,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
||||
|
||||
#### Search using custom filters
|
||||
|
||||
Our advanced search allows you to set custom search filters. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and text. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains). For more details, see [V2 Search Memories](/api-reference/memory/v2-search-memories).
|
||||
Our advanced search allows you to set custom search filters. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and text. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains, *). The wildcard character (*) matches everything for a specific field. For more details, see [V2 Search Memories](/api-reference/memory/v2-search-memories).
|
||||
|
||||
Here you need to define `version` as `v2` in the search method.
|
||||
|
||||
@@ -765,6 +765,81 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Example 5: Search using wildcard filters
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
query = "What do you know about me?"
|
||||
filters = {
|
||||
"AND": [
|
||||
{
|
||||
"user_id": "alex"
|
||||
},
|
||||
{
|
||||
"run_id": "*" # Matches all run_ids
|
||||
}
|
||||
]
|
||||
}
|
||||
client.search(query, version="v2", filters=filters)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
const query = "What do you know about me?";
|
||||
const filters = {
|
||||
"AND": [
|
||||
{
|
||||
"user_id": "alex"
|
||||
},
|
||||
{
|
||||
"run_id": "*" // Matches all run_ids
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
client.search(query, { version: "v2", filters })
|
||||
.then(results => console.log(results))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
|
||||
-H "Authorization: Token your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "What do you know about me?",
|
||||
"filters": {
|
||||
"AND": [
|
||||
{
|
||||
"user_id": "alex"
|
||||
},
|
||||
{
|
||||
"run_id": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
```json Output
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "7f165f7e-b411-4afe-b7e5-35789b72c4a5",
|
||||
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
|
||||
"user_id": "alex",
|
||||
"run_id": "session-1",
|
||||
"metadata": null,
|
||||
"categories": ["food_preferences"],
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-20T01:30:36.275141-07:00",
|
||||
"updated_at": "2024-07-20T01:30:36.275172-07:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### 4.3 Get All Users
|
||||
|
||||
@@ -1138,7 +1213,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&keywords=to play&page
|
||||
|
||||
#### Get all memories using custom filters
|
||||
|
||||
Our advanced retrieval allows you to set custom filters when fetching memories. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and keywords. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains). For more details, see [v2 Get Memories](/api-reference/memory/v2-get-memories).
|
||||
Our advanced retrieval allows you to set custom filters when fetching memories. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and keywords. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains, *). The wildcard character (*) matches everything for a specific field. For more details, see [v2 Get Memories](/api-reference/memory/v2-get-memories).
|
||||
|
||||
Here you need to define `version` as `v2` in the get_all method.
|
||||
|
||||
@@ -1497,6 +1572,119 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Example 4: Get all memories using wildcard filters
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
filters = {
|
||||
"AND": [
|
||||
{
|
||||
"user_id": "alex"
|
||||
},
|
||||
{
|
||||
"run_id": "*" # Matches all run_ids
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Default (No Pagination)
|
||||
client.get_all(version="v2", filters=filters)
|
||||
|
||||
# Pagination (You can also use the page and page_size parameters)
|
||||
client.get_all(version="v2", filters=filters, page=1, page_size=50)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
const filters = {
|
||||
"AND": [
|
||||
{
|
||||
"user_id": "alex"
|
||||
},
|
||||
{
|
||||
"run_id": "*" // Matches all run_ids
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Default (No Pagination)
|
||||
client.getAll({ version: "v2", filters })
|
||||
.then(memories => console.log(memories))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// Pagination (You can also use the page and page_size parameters)
|
||||
client.getAll({ version: "v2", filters, page: 1, page_size: 50 })
|
||||
.then(memories => console.log(memories))
|
||||
.catch(error => console.error(error));
|
||||
```
|
||||
|
||||
```bash cURL
|
||||
# Default (No Pagination)
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?version=v2" \
|
||||
-H "Authorization: Token your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"filters": {
|
||||
"AND": [
|
||||
{"user_id":"alex"},
|
||||
{"run_id": "*"}
|
||||
]
|
||||
}
|
||||
}'
|
||||
|
||||
# Pagination (You can also use the page and page_size parameters)
|
||||
curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
|
||||
-H "Authorization: Token your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"filters": {
|
||||
"AND": [
|
||||
{"user_id":"alex"},
|
||||
{"run_id": "*"}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
```json Output (Default)
|
||||
[
|
||||
{
|
||||
"id": "f38b689d-6b24-45b7-bced-17fbb4d8bac7",
|
||||
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
|
||||
"user_id": "alex",
|
||||
"run_id": "session-1",
|
||||
"metadata": null,
|
||||
"categories": ["food_preferences"],
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-20T01:30:36.275141-07:00",
|
||||
"updated_at": "2024-07-20T01:30:36.275172-07:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```json Output (Paginated)
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": "f38b689d-6b24-45b7-bced-17fbb4d8bac7",
|
||||
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
|
||||
"user_id": "alex",
|
||||
"run_id": "session-1",
|
||||
"metadata": null,
|
||||
"categories": ["food_preferences"],
|
||||
"immutable": false,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-20T01:30:36.275141-07:00",
|
||||
"updated_at": "2024-07-20T01:30:36.275172-07:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### 4.5 Memory History
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "2.1.29",
|
||||
"version": "2.1.30",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -107,6 +107,8 @@
|
||||
"@types/sqlite3": "3.1.11",
|
||||
"groq-sdk": "0.3.0",
|
||||
"@langchain/core": "^0.3.44",
|
||||
"cloudflare": "^4.2.0",
|
||||
"@cloudflare/workers-types": "^4.20250504.0",
|
||||
"neo4j-driver": "^5.28.1",
|
||||
"ollama": "^0.5.14",
|
||||
"pg": "8.11.3",
|
||||
|
||||
Generated
+32
@@ -10,6 +10,9 @@ importers:
|
||||
"@anthropic-ai/sdk":
|
||||
specifier: ^0.40.1
|
||||
version: 0.40.1(encoding@0.1.13)
|
||||
"@cloudflare/workers-types":
|
||||
specifier: ^4.20250504.0
|
||||
version: 4.20250606.0
|
||||
"@google/genai":
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(@modelcontextprotocol/sdk@1.12.1)(encoding@0.1.13)
|
||||
@@ -37,6 +40,9 @@ importers:
|
||||
axios:
|
||||
specifier: 1.7.7
|
||||
version: 1.7.7
|
||||
cloudflare:
|
||||
specifier: ^4.2.0
|
||||
version: 4.3.0(encoding@0.1.13)
|
||||
groq-sdk:
|
||||
specifier: 0.3.0
|
||||
version: 0.3.0(encoding@0.1.13)
|
||||
@@ -385,6 +391,12 @@ packages:
|
||||
integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==,
|
||||
}
|
||||
|
||||
"@cloudflare/workers-types@4.20250606.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-9T/Y/Mxe57UVzqgfjJKheiMplnStj/3CmCHlgoZNLU8JW2waRbXvpY3EEeliiYAJfeHZTjeAaKO2pCabxAoyCw==,
|
||||
}
|
||||
|
||||
"@cspotcode/source-map-support@0.8.1":
|
||||
resolution:
|
||||
{
|
||||
@@ -1811,6 +1823,12 @@ packages:
|
||||
}
|
||||
engines: { node: ">=12" }
|
||||
|
||||
cloudflare@4.3.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-C+4Jhsl/OY4V5sykRB1yJxComDld5BkKW1xd3s0MDJ1yYamT2sFAoC2FEUQg5zipyxMaaGU4N7hZ6il+gfJxZg==,
|
||||
}
|
||||
|
||||
cluster-key-slot@1.1.2:
|
||||
resolution:
|
||||
{
|
||||
@@ -5465,6 +5483,8 @@ snapshots:
|
||||
|
||||
"@cfworker/json-schema@4.1.1": {}
|
||||
|
||||
"@cloudflare/workers-types@4.20250606.0": {}
|
||||
|
||||
"@cspotcode/source-map-support@0.8.1":
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping": 0.3.9
|
||||
@@ -6397,6 +6417,18 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
|
||||
cloudflare@4.3.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
"@types/node": 18.19.76
|
||||
"@types/node-fetch": 2.6.12
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
co@4.6.0: {}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@types/redis": "^4.0.10",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"cloudflare": "^4.2.0",
|
||||
"dotenv": "^16.4.4",
|
||||
"groq-sdk": "^0.3.0",
|
||||
"openai": "^4.28.0",
|
||||
@@ -31,6 +32,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250504.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.5",
|
||||
|
||||
@@ -22,4 +22,5 @@ export * from "./vector_stores/qdrant";
|
||||
export * from "./vector_stores/redis";
|
||||
export * from "./vector_stores/supabase";
|
||||
export * from "./vector_stores/langchain";
|
||||
export * from "./vector_stores/vectorize";
|
||||
export * from "./utils/factory";
|
||||
|
||||
@@ -117,6 +117,7 @@ export const MemoryConfigSchema = z.object({
|
||||
modelProperties: z.record(z.string(), z.any()).optional(),
|
||||
apiKey: z.string().optional(),
|
||||
model: z.union([z.string(), z.any()]).optional(),
|
||||
baseURL: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
vectorStore: z.object({
|
||||
@@ -135,6 +136,7 @@ export const MemoryConfigSchema = z.object({
|
||||
apiKey: z.string().optional(),
|
||||
model: z.union([z.string(), z.any()]).optional(),
|
||||
modelProperties: z.record(z.string(), z.any()).optional(),
|
||||
baseURL: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
historyDbPath: z.string().optional(),
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Embedder } from "../embeddings/base";
|
||||
import { LLM } from "../llms/base";
|
||||
import { VectorStore } from "../vector_stores/base";
|
||||
import { Qdrant } from "../vector_stores/qdrant";
|
||||
import { VectorizeDB } from "../vector_stores/vectorize";
|
||||
import { RedisDB } from "../vector_stores/redis";
|
||||
import { OllamaLLM } from "../llms/ollama";
|
||||
import { SupabaseDB } from "../vector_stores/supabase";
|
||||
@@ -90,6 +91,8 @@ export class VectorStoreFactory {
|
||||
return new SupabaseDB(config as any);
|
||||
case "langchain":
|
||||
return new LangchainVectorStore(config as any);
|
||||
case "vectorize":
|
||||
return new VectorizeDB(config as any);
|
||||
default:
|
||||
throw new Error(`Unsupported vector store provider: ${provider}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
import Cloudflare from "cloudflare";
|
||||
import type { Vectorize, VectorizeVector } from "@cloudflare/workers-types";
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
|
||||
interface VectorizeConfig extends VectorStoreConfig {
|
||||
apiKey?: string;
|
||||
indexName: string;
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
interface CloudflareVector {
|
||||
id: string;
|
||||
values: number[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class VectorizeDB implements VectorStore {
|
||||
private client: Cloudflare | null = null;
|
||||
private dimensions: number;
|
||||
private indexName: string;
|
||||
private accountId: string;
|
||||
|
||||
constructor(config: VectorizeConfig) {
|
||||
this.client = new Cloudflare({ apiToken: config.apiKey });
|
||||
this.dimensions = config.dimension || 1536;
|
||||
this.indexName = config.indexName;
|
||||
this.accountId = config.accountId;
|
||||
this.initialize().catch(console.error);
|
||||
}
|
||||
|
||||
async insert(
|
||||
vectors: number[][],
|
||||
ids: string[],
|
||||
payloads: Record<string, any>[],
|
||||
): Promise<void> {
|
||||
try {
|
||||
const vectorObjects: CloudflareVector[] = vectors.map(
|
||||
(vector, index) => ({
|
||||
id: ids[index],
|
||||
values: vector,
|
||||
metadata: payloads[index] || {},
|
||||
}),
|
||||
);
|
||||
|
||||
const ndjsonPayload = vectorObjects
|
||||
.map((v) => JSON.stringify(v))
|
||||
.join("\n");
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/${this.indexName}/insert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-ndjson",
|
||||
Authorization: `Bearer ${this.client?.apiToken}`,
|
||||
},
|
||||
body: ndjsonPayload,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Failed to insert vectors: ${response.status} ${errorText}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error inserting vectors:", error);
|
||||
throw new Error(
|
||||
`Failed to insert vectors: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async search(
|
||||
query: number[],
|
||||
limit: number = 5,
|
||||
filters?: SearchFilters,
|
||||
): Promise<VectorStoreResult[]> {
|
||||
try {
|
||||
const result = await this.client?.vectorize.indexes.query(
|
||||
this.indexName,
|
||||
{
|
||||
account_id: this.accountId,
|
||||
vector: query,
|
||||
filter: filters,
|
||||
returnMetadata: "all",
|
||||
topK: limit,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
(result?.matches?.map((match) => ({
|
||||
id: match.id,
|
||||
payload: match.metadata,
|
||||
score: match.score,
|
||||
})) as VectorStoreResult[]) || []
|
||||
); // Return empty array if result or matches is null/undefined
|
||||
} catch (error) {
|
||||
console.error("Error searching vectors:", error);
|
||||
throw new Error(
|
||||
`Failed to search vectors: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async get(vectorId: string): Promise<VectorStoreResult | null> {
|
||||
try {
|
||||
const result = (await this.client?.vectorize.indexes.getByIds(
|
||||
this.indexName,
|
||||
{
|
||||
account_id: this.accountId,
|
||||
ids: [vectorId],
|
||||
},
|
||||
)) as any;
|
||||
|
||||
if (!result?.length) return null;
|
||||
|
||||
return {
|
||||
id: vectorId,
|
||||
payload: result[0].metadata,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting vector:", error);
|
||||
throw new Error(
|
||||
`Failed to get vector: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
vectorId: string,
|
||||
vector: number[],
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data: VectorizeVector = {
|
||||
id: vectorId,
|
||||
values: vector,
|
||||
metadata: payload,
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/${this.indexName}/upsert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-ndjson",
|
||||
Authorization: `Bearer ${this.client?.apiToken}`,
|
||||
},
|
||||
body: JSON.stringify(data) + "\n", // ndjson format
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Failed to update vector: ${response.status} ${errorText}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating vector:", error);
|
||||
throw new Error(
|
||||
`Failed to update vector: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(vectorId: string): Promise<void> {
|
||||
try {
|
||||
await this.client?.vectorize.indexes.deleteByIds(this.indexName, {
|
||||
account_id: this.accountId,
|
||||
ids: [vectorId],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting vector:", error);
|
||||
throw new Error(
|
||||
`Failed to delete vector: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCol(): Promise<void> {
|
||||
try {
|
||||
await this.client?.vectorize.indexes.delete(this.indexName, {
|
||||
account_id: this.accountId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting collection:", error);
|
||||
throw new Error(
|
||||
`Failed to delete collection: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async list(
|
||||
filters?: SearchFilters,
|
||||
limit: number = 20,
|
||||
): Promise<[VectorStoreResult[], number]> {
|
||||
try {
|
||||
const result = await this.client?.vectorize.indexes.query(
|
||||
this.indexName,
|
||||
{
|
||||
account_id: this.accountId,
|
||||
vector: Array(this.dimensions).fill(0), // Dummy vector for listing
|
||||
filter: filters,
|
||||
topK: limit,
|
||||
returnMetadata: "all",
|
||||
},
|
||||
);
|
||||
|
||||
const matches =
|
||||
(result?.matches?.map((match) => ({
|
||||
id: match.id,
|
||||
payload: match.metadata,
|
||||
score: match.score,
|
||||
})) as VectorStoreResult[]) || [];
|
||||
|
||||
return [matches, matches.length];
|
||||
} catch (error) {
|
||||
console.error("Error listing vectors:", error);
|
||||
throw new Error(
|
||||
`Failed to list vectors: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private generateUUID(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
||||
/[xy]/g,
|
||||
function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getUserId(): Promise<string> {
|
||||
try {
|
||||
let found = false;
|
||||
for await (const index of this.client!.vectorize.indexes.list({
|
||||
account_id: this.accountId,
|
||||
})) {
|
||||
if (index.name === "memory_migrations") {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
await this.client?.vectorize.indexes.create({
|
||||
account_id: this.accountId,
|
||||
name: "memory_migrations",
|
||||
config: {
|
||||
dimensions: 1,
|
||||
metric: "cosine",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Now try to get the userId
|
||||
const result: any = await this.client?.vectorize.indexes.query(
|
||||
"memory_migrations",
|
||||
{
|
||||
account_id: this.accountId,
|
||||
vector: [0],
|
||||
topK: 1,
|
||||
returnMetadata: "all",
|
||||
},
|
||||
);
|
||||
if (result.matches.length > 0) {
|
||||
return result.matches[0].metadata.userId as string;
|
||||
}
|
||||
|
||||
// Generate a random userId if none exists
|
||||
const randomUserId =
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15);
|
||||
const data: VectorizeVector = {
|
||||
id: this.generateUUID(),
|
||||
values: [0],
|
||||
metadata: { userId: randomUserId },
|
||||
};
|
||||
|
||||
await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/memory_migrations/upsert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-ndjson",
|
||||
Authorization: `Bearer ${this.client?.apiToken}`,
|
||||
},
|
||||
body: JSON.stringify(data) + "\n", // ndjson format
|
||||
},
|
||||
);
|
||||
return randomUserId;
|
||||
} catch (error) {
|
||||
console.error("Error getting user ID:", error);
|
||||
throw new Error(
|
||||
`Failed to get user ID: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async setUserId(userId: string): Promise<void> {
|
||||
try {
|
||||
// Get existing point ID
|
||||
const result: any = await this.client?.vectorize.indexes.query(
|
||||
"memory_migrations",
|
||||
{
|
||||
account_id: this.accountId,
|
||||
vector: [0],
|
||||
topK: 1,
|
||||
returnMetadata: "all",
|
||||
},
|
||||
);
|
||||
const pointId =
|
||||
result.matches.length > 0 ? result.matches[0].id : this.generateUUID();
|
||||
|
||||
const data: VectorizeVector = {
|
||||
id: pointId,
|
||||
values: [0],
|
||||
metadata: { userId },
|
||||
};
|
||||
await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/memory_migrations/upsert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-ndjson",
|
||||
Authorization: `Bearer ${this.client?.apiToken}`,
|
||||
},
|
||||
body: JSON.stringify(data) + "\n", // ndjson format
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error setting user ID:", error);
|
||||
throw new Error(
|
||||
`Failed to set user ID: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
// Check if the index already exists
|
||||
let indexFound = false;
|
||||
for await (const idx of this.client!.vectorize.indexes.list({
|
||||
account_id: this.accountId,
|
||||
})) {
|
||||
if (idx.name === this.indexName) {
|
||||
indexFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If the index doesn't exist, create it
|
||||
if (!indexFound) {
|
||||
try {
|
||||
await this.client?.vectorize.indexes.create({
|
||||
account_id: this.accountId,
|
||||
name: this.indexName,
|
||||
config: {
|
||||
dimensions: this.dimensions,
|
||||
metric: "cosine",
|
||||
},
|
||||
});
|
||||
|
||||
const properties = ["userId", "agentId", "runId"];
|
||||
|
||||
for (const propertyName of properties) {
|
||||
await this.client?.vectorize.indexes.metadataIndex.create(
|
||||
this.indexName,
|
||||
{
|
||||
account_id: this.accountId,
|
||||
indexType: "string",
|
||||
propertyName,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// check for metadata index
|
||||
const metadataIndexes =
|
||||
await this.client?.vectorize.indexes.metadataIndex.list(
|
||||
this.indexName,
|
||||
{
|
||||
account_id: this.accountId,
|
||||
},
|
||||
);
|
||||
const existingMetadataIndexes = new Set<string>();
|
||||
for (const metadataIndex of metadataIndexes?.metadataIndexes || []) {
|
||||
existingMetadataIndexes.add(metadataIndex.propertyName!);
|
||||
}
|
||||
const properties = ["userId", "agentId", "runId"];
|
||||
for (const propertyName of properties) {
|
||||
if (!existingMetadataIndexes.has(propertyName)) {
|
||||
await this.client?.vectorize.indexes.metadataIndex.create(
|
||||
this.indexName,
|
||||
{
|
||||
account_id: this.accountId,
|
||||
indexType: "string",
|
||||
propertyName,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// Create memory_migrations collection if it doesn't exist
|
||||
let found = false;
|
||||
for await (const index of this.client!.vectorize.indexes.list({
|
||||
account_id: this.accountId,
|
||||
})) {
|
||||
if (index.name === "memory_migrations") {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
await this.client?.vectorize.indexes.create({
|
||||
account_id: this.accountId,
|
||||
name: "memory_migrations",
|
||||
config: {
|
||||
dimensions: 1,
|
||||
metric: "cosine",
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-9
@@ -526,7 +526,13 @@ class MemoryClient:
|
||||
if not (self.org_id and self.project_id):
|
||||
raise ValueError("org_id and project_id must be set to update instructions or categories")
|
||||
|
||||
if custom_instructions is None and custom_categories is None and retrieval_criteria is None and enable_graph is None and version is None:
|
||||
if (
|
||||
custom_instructions is None
|
||||
and custom_categories is None
|
||||
and retrieval_criteria is None
|
||||
and enable_graph is None
|
||||
and version is None
|
||||
):
|
||||
raise ValueError(
|
||||
"Currently we only support updating custom_instructions or custom_categories or retrieval_criteria, so you must provide at least one of them"
|
||||
)
|
||||
@@ -675,9 +681,7 @@ class MemoryClient:
|
||||
capture_client_event("client.feedback", self, data, {"sync_type": "sync"})
|
||||
return response.json()
|
||||
|
||||
def _prepare_payload(
|
||||
self, messages: List[Dict[str, str]], kwargs: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
def _prepare_payload(self, messages: List[Dict[str, str]], kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Prepare the payload for API requests.
|
||||
|
||||
Args:
|
||||
@@ -795,7 +799,7 @@ class AsyncMemoryClient:
|
||||
|
||||
return data.get("user_email")
|
||||
|
||||
except requests.HTTPStatusError as e:
|
||||
except requests.exceptions.HTTPError as e:
|
||||
try:
|
||||
error_data = e.response.json()
|
||||
error_message = error_data.get("detail", str(e))
|
||||
@@ -803,9 +807,7 @@ class AsyncMemoryClient:
|
||||
error_message = str(e)
|
||||
raise ValueError(f"Error: {error_message}")
|
||||
|
||||
def _prepare_payload(
|
||||
self, messages: List[Dict[str, str]], kwargs: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
def _prepare_payload(self, messages: List[Dict[str, str]], kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Prepare the payload for API requests.
|
||||
|
||||
Args:
|
||||
@@ -1115,7 +1117,13 @@ class AsyncMemoryClient:
|
||||
if not (self.org_id and self.project_id):
|
||||
raise ValueError("org_id and project_id must be set to update instructions or categories")
|
||||
|
||||
if custom_instructions is None and custom_categories is None and retrieval_criteria is None and enable_graph is None and version is None:
|
||||
if (
|
||||
custom_instructions is None
|
||||
and custom_categories is None
|
||||
and retrieval_criteria is None
|
||||
and enable_graph is None
|
||||
and version is None
|
||||
):
|
||||
raise ValueError(
|
||||
"Currently we only support updating custom_instructions or custom_categories or retrieval_criteria, so you must provide at least one of them"
|
||||
)
|
||||
|
||||
+8
-21
@@ -18,22 +18,15 @@ class SarvamLLM(LLMBase):
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError(
|
||||
"Sarvam API key is required. Set SARVAM_API_KEY environment variable "
|
||||
"or provide api_key in config."
|
||||
"Sarvam API key is required. Set SARVAM_API_KEY environment variable " "or provide api_key in config."
|
||||
)
|
||||
|
||||
# Set base URL - use config value or environment or default
|
||||
self.base_url = (
|
||||
getattr(self.config, 'sarvam_base_url', None) or
|
||||
os.getenv("SARVAM_API_BASE") or
|
||||
"https://api.sarvam.ai/v1"
|
||||
getattr(self.config, "sarvam_base_url", None) or os.getenv("SARVAM_API_BASE") or "https://api.sarvam.ai/v1"
|
||||
)
|
||||
|
||||
def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
response_format=None
|
||||
) -> str:
|
||||
def generate_response(self, messages: List[Dict[str, str]], response_format=None) -> str:
|
||||
"""
|
||||
Generate a response based on the given messages using Sarvam-M.
|
||||
|
||||
@@ -47,10 +40,7 @@ class SarvamLLM(LLMBase):
|
||||
"""
|
||||
url = f"{self.base_url}/chat/completions"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
||||
|
||||
# Prepare the request payload
|
||||
params = {
|
||||
@@ -74,10 +64,7 @@ class SarvamLLM(LLMBase):
|
||||
params["model"] = self.config.model.get("name", "sarvam-m")
|
||||
|
||||
# Add Sarvam-specific parameters
|
||||
sarvam_specific_params = [
|
||||
'reasoning_effort', 'frequency_penalty', 'presence_penalty',
|
||||
'seed', 'stop', 'n'
|
||||
]
|
||||
sarvam_specific_params = ["reasoning_effort", "frequency_penalty", "presence_penalty", "seed", "stop", "n"]
|
||||
|
||||
for param in sarvam_specific_params:
|
||||
if param in self.config.model:
|
||||
@@ -89,12 +76,12 @@ class SarvamLLM(LLMBase):
|
||||
|
||||
result = response.json()
|
||||
|
||||
if 'choices' in result and len(result['choices']) > 0:
|
||||
return result['choices'][0]['message']['content']
|
||||
if "choices" in result and len(result["choices"]) > 0:
|
||||
return result["choices"][0]["message"]["content"]
|
||||
else:
|
||||
raise ValueError("No response choices found in Sarvam API response")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise RuntimeError(f"Sarvam API request failed: {e}")
|
||||
except KeyError as e:
|
||||
raise ValueError(f"Unexpected response format from Sarvam API: {e}")
|
||||
raise ValueError(f"Unexpected response format from Sarvam API: {e}")
|
||||
|
||||
+78
-44
@@ -29,6 +29,7 @@ from mem0.memory.utils import (
|
||||
parse_messages,
|
||||
parse_vision_messages,
|
||||
remove_code_blocks,
|
||||
process_telemetry_filters,
|
||||
)
|
||||
from mem0.utils.factory import EmbedderFactory, LlmFactory, VectorStoreFactory
|
||||
|
||||
@@ -45,13 +46,13 @@ def _build_filters_and_metadata(
|
||||
"""
|
||||
Constructs metadata for storage and filters for querying based on session and actor identifiers.
|
||||
|
||||
This helper ties every memory/query to exactly one session id (`user_id`, `agent_id`, or `run_id`) and optionally narrows queries to a specific `actor_id`. It returns two dicts:
|
||||
|
||||
This helper supports multiple session identifiers (`user_id`, `agent_id`, and/or `run_id`)
|
||||
for flexible session scoping and optionally narrows queries to a specific `actor_id`. It returns two dicts:
|
||||
|
||||
1. `base_metadata_template`: Used as a template for metadata when storing new memories.
|
||||
It includes the primary session identifier(s) and any `input_metadata`.
|
||||
2. `effective_query_filters`: Used for querying existing memories. It includes the
|
||||
primary session identifier(s), any `input_filters`, and a resolved actor
|
||||
It includes all provided session identifier(s) and any `input_metadata`.
|
||||
2. `effective_query_filters`: Used for querying existing memories. It includes all
|
||||
provided session identifier(s), any `input_filters`, and a resolved actor
|
||||
identifier for targeted filtering if specified by any actor-related inputs.
|
||||
|
||||
Actor filtering precedence: explicit `actor_id` arg → `filters["actor_id"]`
|
||||
@@ -59,11 +60,9 @@ def _build_filters_and_metadata(
|
||||
as the actor for storage is typically derived from message content at a later stage.
|
||||
|
||||
Args:
|
||||
user_id (Optional[str]): User identifier, primarily for Classic Mode session scoping.
|
||||
agent_id (Optional[str]): Agent identifier, for Classic Mode session scoping or
|
||||
as auxiliary information in Group Mode.
|
||||
run_id (Optional[str]): Run identifier, for Classic Mode session scoping or
|
||||
as auxiliary information in Group Mode.
|
||||
user_id (Optional[str]): User identifier, for session scoping.
|
||||
agent_id (Optional[str]): Agent identifier, for session scoping.
|
||||
run_id (Optional[str]): Run identifier, for session scoping.
|
||||
actor_id (Optional[str]): Explicit actor identifier, used as a potential source for
|
||||
actor-specific filtering. See actor resolution precedence in the main description.
|
||||
input_metadata (Optional[Dict[str, Any]]): Base dictionary to be augmented with
|
||||
@@ -74,28 +73,34 @@ def _build_filters_and_metadata(
|
||||
Returns:
|
||||
tuple[Dict[str, Any], Dict[str, Any]]: A tuple containing:
|
||||
- base_metadata_template (Dict[str, Any]): Metadata template for storing memories,
|
||||
scoped to the determined session.
|
||||
scoped to the provided session(s).
|
||||
- effective_query_filters (Dict[str, Any]): Filters for querying memories,
|
||||
scoped to the determined session and potentially a resolved actor.
|
||||
scoped to the provided session(s) and potentially a resolved actor.
|
||||
"""
|
||||
|
||||
base_metadata_template = deepcopy(input_metadata) if input_metadata else {}
|
||||
effective_query_filters = deepcopy(input_filters) if input_filters else {}
|
||||
|
||||
# ---------- resolve session id (mandatory) ----------
|
||||
session_key, session_val = None, None
|
||||
# ---------- add all provided session ids ----------
|
||||
session_ids_provided = []
|
||||
|
||||
if user_id:
|
||||
session_key, session_val = "user_id", user_id
|
||||
elif agent_id:
|
||||
session_key, session_val = "agent_id", agent_id
|
||||
elif run_id:
|
||||
session_key, session_val = "run_id", run_id
|
||||
base_metadata_template["user_id"] = user_id
|
||||
effective_query_filters["user_id"] = user_id
|
||||
session_ids_provided.append("user_id")
|
||||
|
||||
if session_key is None:
|
||||
raise ValueError("One of 'user_id', 'agent_id', or 'run_id' must be provided.")
|
||||
if agent_id:
|
||||
base_metadata_template["agent_id"] = agent_id
|
||||
effective_query_filters["agent_id"] = agent_id
|
||||
session_ids_provided.append("agent_id")
|
||||
|
||||
base_metadata_template[session_key] = session_val
|
||||
effective_query_filters[session_key] = session_val
|
||||
if run_id:
|
||||
base_metadata_template["run_id"] = run_id
|
||||
effective_query_filters["run_id"] = run_id
|
||||
session_ids_provided.append("run_id")
|
||||
|
||||
if not session_ids_provided:
|
||||
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be provided.")
|
||||
|
||||
# ---------- optional actor filter ----------
|
||||
resolved_actor_id = actor_id or effective_query_filters.get("actor_id")
|
||||
@@ -433,10 +438,11 @@ class Memory(MemoryBase):
|
||||
except Exception as e:
|
||||
logging.error(f"Error iterating new_memories_with_actions: {e}")
|
||||
|
||||
keys, encoded_ids = process_telemetry_filters(filters)
|
||||
capture_event(
|
||||
"mem0.add",
|
||||
self,
|
||||
{"version": self.api_version, "keys": list(filters.keys()), "sync_type": "sync"},
|
||||
{"version": self.api_version, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"},
|
||||
)
|
||||
return returned_memories
|
||||
|
||||
@@ -529,8 +535,9 @@ class Memory(MemoryBase):
|
||||
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
||||
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be specified.")
|
||||
|
||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||
capture_event(
|
||||
"mem0.get_all", self, {"limit": limit, "keys": list(effective_filters.keys()), "sync_type": "sync"}
|
||||
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"}
|
||||
)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
@@ -564,7 +571,9 @@ class Memory(MemoryBase):
|
||||
def _get_all_from_vector_store(self, filters, limit):
|
||||
memories_result = self.vector_store.list(filters=filters, limit=limit)
|
||||
actual_memories = (
|
||||
memories_result[0] if isinstance(memories_result, (tuple, list)) and len(memories_result) > 0 else memories_result
|
||||
memories_result[0]
|
||||
if isinstance(memories_result, (tuple, list)) and len(memories_result) > 0
|
||||
else memories_result
|
||||
)
|
||||
|
||||
promoted_payload_keys = [
|
||||
@@ -607,6 +616,7 @@ class Memory(MemoryBase):
|
||||
run_id: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
threshold: Optional[float] = None,
|
||||
):
|
||||
"""
|
||||
Searches for memories based on a query
|
||||
@@ -617,6 +627,7 @@ class Memory(MemoryBase):
|
||||
run_id (str, optional): ID of the run to search for. Defaults to None.
|
||||
limit (int, optional): Limit the number of results. Defaults to 100.
|
||||
filters (dict, optional): Filters to apply to the search. Defaults to None..
|
||||
threshold (float, optional): Minimum score for a memory to be included in the results. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the search results, typically under a "results" key,
|
||||
@@ -630,14 +641,22 @@ class Memory(MemoryBase):
|
||||
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
||||
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be specified.")
|
||||
|
||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||
capture_event(
|
||||
"mem0.search",
|
||||
self,
|
||||
{"limit": limit, "version": self.api_version, "keys": list(effective_filters.keys()), "sync_type": "sync"},
|
||||
{
|
||||
"limit": limit,
|
||||
"version": self.api_version,
|
||||
"keys": keys,
|
||||
"encoded_ids": encoded_ids,
|
||||
"sync_type": "sync",
|
||||
"threshold": threshold,
|
||||
},
|
||||
)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future_memories = executor.submit(self._search_vector_store, query, effective_filters, limit)
|
||||
future_memories = executor.submit(self._search_vector_store, query, effective_filters, limit, threshold)
|
||||
future_graph_entities = (
|
||||
executor.submit(self.graph.search, query, effective_filters, limit) if self.enable_graph else None
|
||||
)
|
||||
@@ -664,7 +683,7 @@ class Memory(MemoryBase):
|
||||
else:
|
||||
return {"results": original_memories}
|
||||
|
||||
def _search_vector_store(self, query, filters, limit):
|
||||
def _search_vector_store(self, query, filters, limit, threshold: Optional[float] = None):
|
||||
embeddings = self.embedding_model.embed(query, "search")
|
||||
memories = self.vector_store.search(query=query, vectors=embeddings, limit=limit, filters=filters)
|
||||
|
||||
@@ -697,7 +716,8 @@ class Memory(MemoryBase):
|
||||
if additional_metadata:
|
||||
memory_item_dict["metadata"] = additional_metadata
|
||||
|
||||
original_memories.append(memory_item_dict)
|
||||
if threshold is None or mem.score >= threshold:
|
||||
original_memories.append(memory_item_dict)
|
||||
|
||||
return original_memories
|
||||
|
||||
@@ -752,7 +772,8 @@ class Memory(MemoryBase):
|
||||
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
|
||||
)
|
||||
|
||||
capture_event("mem0.delete_all", self, {"keys": list(filters.keys()), "sync_type": "sync"})
|
||||
keys, encoded_ids = process_telemetry_filters(filters)
|
||||
capture_event("mem0.delete_all", self, {"keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"})
|
||||
memories = self.vector_store.list(filters=filters)[0]
|
||||
for memory in memories:
|
||||
self._delete_memory(memory.id)
|
||||
@@ -1086,7 +1107,7 @@ class AsyncMemory(MemoryBase):
|
||||
self,
|
||||
messages: list,
|
||||
metadata: dict,
|
||||
filters: dict,
|
||||
effective_filters: dict,
|
||||
infer: bool,
|
||||
):
|
||||
if not infer:
|
||||
@@ -1160,7 +1181,7 @@ class AsyncMemory(MemoryBase):
|
||||
query=new_mem_content,
|
||||
vectors=embeddings,
|
||||
limit=5,
|
||||
filters=filters, # 'filters' is query_filters_for_inference
|
||||
filters=effective_filters, # 'filters' is query_filters_for_inference
|
||||
)
|
||||
return [{"id": mem.id, "text": mem.payload["data"]} for mem in existing_mems]
|
||||
|
||||
@@ -1189,7 +1210,6 @@ class AsyncMemory(MemoryBase):
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
response = ""
|
||||
logging.error(f"Error in new memory actions response: {e}")
|
||||
response = ""
|
||||
@@ -1197,7 +1217,6 @@ class AsyncMemory(MemoryBase):
|
||||
response = remove_code_blocks(response)
|
||||
new_memories_with_actions = json.loads(response)
|
||||
except Exception as e:
|
||||
|
||||
new_memories_with_actions = {}
|
||||
|
||||
if not new_memories_with_actions:
|
||||
@@ -1207,7 +1226,6 @@ class AsyncMemory(MemoryBase):
|
||||
logging.error(f"Invalid JSON response: {e}")
|
||||
new_memories_with_actions = {}
|
||||
|
||||
|
||||
returned_memories = []
|
||||
try:
|
||||
memory_tasks = []
|
||||
@@ -1267,8 +1285,11 @@ class AsyncMemory(MemoryBase):
|
||||
except Exception as e:
|
||||
logging.error(f"Error in memory processing loop (async): {e}")
|
||||
|
||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||
capture_event(
|
||||
"mem0.add", self, {"version": self.api_version, "keys": list(filters.keys()), "sync_type": "async"}
|
||||
"mem0.add",
|
||||
self,
|
||||
{"version": self.api_version, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"},
|
||||
)
|
||||
return returned_memories
|
||||
|
||||
@@ -1364,8 +1385,9 @@ class AsyncMemory(MemoryBase):
|
||||
"at least one of 'user_id', 'agent_id', or 'run_id' must be specified for get_all."
|
||||
)
|
||||
|
||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||
capture_event(
|
||||
"mem0.get_all", self, {"limit": limit, "keys": list(effective_filters.keys()), "sync_type": "async"}
|
||||
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"}
|
||||
)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
@@ -1442,6 +1464,7 @@ class AsyncMemory(MemoryBase):
|
||||
run_id: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
threshold: Optional[float] = None,
|
||||
):
|
||||
"""
|
||||
Searches for memories based on a query
|
||||
@@ -1451,7 +1474,8 @@ class AsyncMemory(MemoryBase):
|
||||
agent_id (str, optional): ID of the agent to search for. Defaults to None.
|
||||
run_id (str, optional): ID of the run to search for. Defaults to None.
|
||||
limit (int, optional): Limit the number of results. Defaults to 100.
|
||||
filters (dict, optional): Filters to apply to the search. Defaults to None..
|
||||
filters (dict, optional): Filters to apply to the search. Defaults to None.
|
||||
threshold (float, optional): Minimum score for a memory to be included in the results. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the search results, typically under a "results" key,
|
||||
@@ -1466,13 +1490,21 @@ class AsyncMemory(MemoryBase):
|
||||
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
|
||||
raise ValueError("at least one of 'user_id', 'agent_id', or 'run_id' must be specified ")
|
||||
|
||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||
capture_event(
|
||||
"mem0.search",
|
||||
self,
|
||||
{"limit": limit, "version": self.api_version, "keys": list(effective_filters.keys()), "sync_type": "async"},
|
||||
{
|
||||
"limit": limit,
|
||||
"version": self.api_version,
|
||||
"keys": keys,
|
||||
"encoded_ids": encoded_ids,
|
||||
"sync_type": "async",
|
||||
"threshold": threshold,
|
||||
},
|
||||
)
|
||||
|
||||
vector_store_task = asyncio.create_task(self._search_vector_store(query, effective_filters, limit))
|
||||
vector_store_task = asyncio.create_task(self._search_vector_store(query, effective_filters, limit, threshold))
|
||||
|
||||
graph_task = None
|
||||
if self.enable_graph:
|
||||
@@ -1502,7 +1534,7 @@ class AsyncMemory(MemoryBase):
|
||||
else:
|
||||
return {"results": original_memories}
|
||||
|
||||
async def _search_vector_store(self, query, filters, limit):
|
||||
async def _search_vector_store(self, query, filters, limit, threshold: Optional[float] = None):
|
||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, query, "search")
|
||||
memories = await asyncio.to_thread(
|
||||
self.vector_store.search, query=query, vectors=embeddings, limit=limit, filters=filters
|
||||
@@ -1537,7 +1569,8 @@ class AsyncMemory(MemoryBase):
|
||||
if additional_metadata:
|
||||
memory_item_dict["metadata"] = additional_metadata
|
||||
|
||||
original_memories.append(memory_item_dict)
|
||||
if threshold is None or mem.score >= threshold:
|
||||
original_memories.append(memory_item_dict)
|
||||
|
||||
return original_memories
|
||||
|
||||
@@ -1593,7 +1626,8 @@ class AsyncMemory(MemoryBase):
|
||||
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
|
||||
)
|
||||
|
||||
capture_event("mem0.delete_all", self, {"keys": list(filters.keys()), "sync_type": "async"})
|
||||
keys, encoded_ids = process_telemetry_filters(filters)
|
||||
capture_event("mem0.delete_all", self, {"keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"})
|
||||
memories = await asyncio.to_thread(self.vector_store.list, filters=filters)
|
||||
|
||||
delete_tasks = []
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
from mem0.configs.prompts import FACT_RETRIEVAL_PROMPT
|
||||
|
||||
@@ -98,3 +99,21 @@ def parse_vision_messages(messages, llm=None, vision_details="auto"):
|
||||
returned_messages.append(msg)
|
||||
|
||||
return returned_messages
|
||||
|
||||
|
||||
def process_telemetry_filters(filters):
|
||||
"""
|
||||
Process the telemetry filters
|
||||
"""
|
||||
if filters is None:
|
||||
return {}
|
||||
|
||||
encoded_ids = {}
|
||||
if "user_id" in filters:
|
||||
encoded_ids["user_id"] = hashlib.md5(filters["user_id"].encode()).hexdigest()
|
||||
if "agent_id" in filters:
|
||||
encoded_ids["agent_id"] = hashlib.md5(filters["agent_id"].encode()).hexdigest()
|
||||
if "run_id" in filters:
|
||||
encoded_ids["run_id"] = hashlib.md5(filters["run_id"].encode()).hexdigest()
|
||||
|
||||
return list(filters.keys()), encoded_ids
|
||||
|
||||
@@ -37,6 +37,45 @@ curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh |
|
||||
|
||||
## Quickstart
|
||||
|
||||
### 1. Set Up Environment Variables
|
||||
|
||||
Before running the project, you need to configure environment variables for both the API and the UI.
|
||||
|
||||
You can do this in one of the following ways:
|
||||
|
||||
- **Manually**:
|
||||
Create a `.env` file in each of the following directories:
|
||||
- `/api/.env`
|
||||
- `/ui/.env`
|
||||
|
||||
- **Using `.env.example` files**:
|
||||
Copy and rename the example files:
|
||||
|
||||
```bash
|
||||
cp api/.env.example api/.env
|
||||
cp ui/.env.example ui/.env
|
||||
```
|
||||
|
||||
- **Using Makefile** (if supported):
|
||||
Run:
|
||||
|
||||
```bash
|
||||
make env
|
||||
```
|
||||
- #### Example `/api/.env`
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
USER=<user-id> # The User Id you want to associate the memories with
|
||||
```
|
||||
- #### Example `/ui/.env`
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8765
|
||||
NEXT_PUBLIC_USER_ID=<user-id> # Same as the user id for environment variable in api
|
||||
```
|
||||
|
||||
### 2. Build and Run the Project
|
||||
You can run the project using the following two commands:
|
||||
```bash
|
||||
make build # builds the mcp server and ui
|
||||
@@ -47,6 +86,17 @@ After running these commands, you will have:
|
||||
- OpenMemory MCP server running at: http://localhost:8765 (API documentation available at http://localhost:8765/docs)
|
||||
- OpenMemory UI running at: http://localhost:3000
|
||||
|
||||
#### UI not working on `localhost:3000`?
|
||||
|
||||
If the UI does not start properly on [http://localhost:3000](http://localhost:3000), try running it manually:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `api/` - Backend APIs + MCP server
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
USER=user
|
||||
@@ -0,0 +1,36 @@
|
||||
"""remove_global_unique_constraint_on_app_name_add_composite_unique
|
||||
|
||||
Revision ID: afd00efbd06b
|
||||
Revises: add_config_table
|
||||
Create Date: 2025-06-04 01:59:41.637440
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'afd00efbd06b'
|
||||
down_revision: Union[str, None] = 'add_config_table'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('ix_apps_name', table_name='apps')
|
||||
op.create_index(op.f('ix_apps_name'), 'apps', ['name'], unique=False)
|
||||
op.create_index('idx_app_owner_name', 'apps', ['owner_id', 'name'], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('idx_app_owner_name', table_name='apps')
|
||||
op.drop_index(op.f('ix_apps_name'), table_name='apps')
|
||||
op.create_index('ix_apps_name', 'apps', ['name'], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,6 +1,7 @@
|
||||
import enum
|
||||
import uuid
|
||||
import datetime
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import (
|
||||
Column, String, Boolean, ForeignKey, Enum, Table,
|
||||
DateTime, JSON, Integer, UUID, Index, event
|
||||
@@ -43,7 +44,7 @@ class App(Base):
|
||||
__tablename__ = "apps"
|
||||
id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
|
||||
owner_id = Column(UUID, ForeignKey("users.id"), nullable=False, index=True)
|
||||
name = Column(String, unique=True, nullable=False, index=True)
|
||||
name = Column(String, nullable=False, index=True)
|
||||
description = Column(String)
|
||||
metadata_ = Column('metadata', JSON, default=dict)
|
||||
is_active = Column(Boolean, default=True, index=True)
|
||||
@@ -55,6 +56,10 @@ class App(Base):
|
||||
owner = relationship("User", back_populates="apps")
|
||||
memories = relationship("Memory", back_populates="app")
|
||||
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('owner_id', 'name', name='idx_app_owner_name'),
|
||||
)
|
||||
|
||||
|
||||
class Config(Base):
|
||||
__tablename__ = "configs"
|
||||
|
||||
@@ -212,7 +212,8 @@ async def create_memory(
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
# Get or create app
|
||||
app_obj = db.query(App).filter(App.name == request.app).first()
|
||||
app_obj = db.query(App).filter(App.name == request.app,
|
||||
App.owner_id == user.id).first()
|
||||
if not app_obj:
|
||||
app_obj = App(name=request.app, owner_id=user.id)
|
||||
db.add(app_obj)
|
||||
|
||||
@@ -10,7 +10,7 @@ interface ProfileState {
|
||||
}
|
||||
|
||||
const initialState: ProfileState = {
|
||||
userId: process.env.NEXT_PUBLIC_USER_ID || 'deshraj',
|
||||
userId: process.env.NEXT_PUBLIC_USER_ID || 'user',
|
||||
totalMemories: 0,
|
||||
totalApps: 0,
|
||||
status: 'idle',
|
||||
@@ -36,7 +36,7 @@ const profileSlice = createSlice({
|
||||
resetProfileState: (state) => {
|
||||
state.status = 'idle';
|
||||
state.error = null;
|
||||
state.userId = process.env.NEXT_PUBLIC_USER_ID || 'deshraj';
|
||||
state.userId = process.env.NEXT_PUBLIC_USER_ID || 'user';
|
||||
},
|
||||
setTotalMemories: (state, action: PayloadAction<number>) => {
|
||||
state.totalMemories = action.payload;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0ai"
|
||||
version = "0.1.104"
|
||||
version = "0.1.106"
|
||||
description = "Long-term memory for AI Agents"
|
||||
authors = [
|
||||
{ name = "Mem0", email = "founders@mem0.ai" }
|
||||
|
||||
Reference in New Issue
Block a user