Compare commits

..
8 Commits
25 changed files with 1050 additions and 94 deletions
@@ -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>
+8
View File
@@ -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>
+1 -1
View File
@@ -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}>
+63 -10
View File
@@ -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
+190 -2
View File
@@ -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
+3 -1
View File
@@ -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",
+32
View File
@@ -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: {}
+2
View File
@@ -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",
+1
View File
@@ -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";
+2
View File
@@ -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(),
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 = []
+19
View File
@@ -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
+50
View File
@@ -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
+2 -1
View File
@@ -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 ###
+6 -1
View File
@@ -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"
+2 -1
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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" }