Compare commits

..
40 Commits
Author SHA1 Message Date
Dev KhantandGitHub 6e4fb22a7c version bump -> 0.1.67 (#2357) 2025-03-10 23:51:30 +05:30
Dev KhantandGitHub 9c0954133f Improve multimodal functionality (#2297) 2025-03-10 23:33:18 +05:30
Dev KhantandGitHub e9a0be66d8 Doc: Fix examples (#2355) 2025-03-10 20:08:58 +05:30
Dev KhantandGitHub 192c33f190 Doc: update examples page (#2348) 2025-03-10 12:04:44 +05:30
Dev KhantandGitHub 75ca528666 Doc: Fix examples page (#2346) 2025-03-10 11:36:02 +05:30
Saket AryanandGitHub e30e4967ae Added Cloudflare Worker Compatible Configs (#2343) 2025-03-09 13:11:47 -07:00
Prateek ChhikaraandGitHub d3911b92cf Docs Update (#2337) 2025-03-08 10:30:42 -08:00
Dev KhantandGitHub 92cfc1c8ef Doc: update examples name (#2342) 2025-03-08 23:56:00 +05:30
Dev KhantandGitHub 33fcc53e4b Doc: Update name of deepresearch example (#2338) 2025-03-08 12:25:06 +05:30
Dev KhantandGitHub e761a1e865 Doc: Deepresearch example (#2336) 2025-03-08 01:02:51 +05:30
Dev KhantandGitHub f2ce92ebcc Update multimodal example (#2335) 2025-03-08 00:06:45 +05:30
Dev KhantandGitHub bbb812e0a0 version bump -> 0.1.66 (#2334) 2025-03-07 23:56:40 +05:30
Parshva DaftariandGitHub 9a302cef30 [ Fix ] for the vertex_ai_vector_search documentation (#2323) 2025-03-07 23:54:07 +05:30
Dev KhantandGitHub ae729da4d1 Doc: Multimodality usecase (#2333) 2025-03-07 23:52:27 +05:30
Dev KhantandGitHub 655ae794b6 Examples: Add multimodal app (#2328) 2025-03-07 23:36:32 +05:30
Dev KhantandGitHub 1aef468ebe Handle empty field in new_memories_with_actions (#2330) 2025-03-07 16:51:10 +05:30
Dev KhantandGitHub 78baf7495d Doc: Document editing with Mem0 (#2325) 2025-03-07 16:40:41 +05:30
Dev KhantandGitHub 6cf7ac3e30 Fixes for new_memories_with_actions (#2326) 2025-03-07 13:13:00 +05:30
Dev KhantandGitHub 07d2f11081 Catch json error for new_memories_with_action (#2324) 2025-03-07 13:12:00 +05:30
Prateek ChhikaraandGitHub c6fbba6a4d Changed multimodal prompt to extract better text from images (#2322) 2025-03-06 11:57:20 -08:00
Dev KhantandGitHub b701a50b51 Doc: Update chrome extension placement (#2321) 2025-03-06 23:58:23 +05:30
Dev KhantandGitHub 5865d79de7 Doc: Chrome extension (#2319) 2025-03-06 21:41:34 +05:30
Dev KhantandGitHub 41a42da774 Doc: Mem0 mcp with cursor (#2318) 2025-03-06 07:46:14 -08:00
Saket AryanandGitHub 6d7ef3ae45 Multimodal Support NodeSDK (#2320) 2025-03-06 17:50:41 +05:30
yanzzandGitHub 2c31a930a3 Update README.md (#2187) 2025-03-05 12:41:43 -08:00
Dev KhantandGitHub c7e2a71cd5 Update cd.yml 2025-03-06 00:19:12 +05:30
Dev KhantandGitHub 4237b9220b CD changes (#2316) 2025-03-06 00:10:57 +05:30
Dev-Khant cabe29c7c7 version bump -> 0.1.65 2025-03-06 00:02:18 +05:30
Mini256andGitHub 80b7202db6 fix: fix sample code on README.md (#2312) 2025-03-06 00:00:13 +05:30
Rafael Nico T. ManiquizandGitHub 8c6d16a6f0 Fix Embedding Dimension Parameter Not Being Passed (#2304) 2025-03-05 20:23:36 +05:30
Dev KhantandGitHub dd1f2989bc revert cd changes (#2315) 2025-03-05 17:33:17 +05:30
Dev KhantandGitHub 540ec1b816 fix cd (#2314) 2025-03-05 17:14:37 +05:30
Dev KhantandGitHub 728ef98d6e Doc: Update doc for both user and agent (#2313) 2025-03-05 17:05:28 +05:30
Dev KhantandGitHub 329d0cc945 version bump -> 0.1.64 (#2310) 2025-03-05 16:17:19 +05:30
Dev KhantandGitHub 0234c85be5 Fix CD (#2309) 2025-03-05 16:10:59 +05:30
Dev KhantandGitHub eca1e06711 Doc: Update add memories (#2306) 2025-03-05 01:57:44 -08:00
Saket AryanandGitHub 2611343cbe Updated Docs to add Mem0 Demo Link/ Updated Mem0 Demo (#2305) 2025-03-05 01:11:33 -08:00
Dev KhantandGitHub 8bde881e2c Add AWS lambda issue to FAQ (#2303) 2025-03-04 23:43:28 -08:00
Saket AryanandGitHub 6fdc63504a Graph Support for NodeSDK (#2298) 2025-03-04 23:22:50 -08:00
anchit-nishantandGitHub 23dbce4f59 Added support for google vector search - (matching engine) (#2177) 2025-03-05 11:45:47 +05:30
122 changed files with 6791 additions and 727 deletions
+9 -8
View File
@@ -25,22 +25,23 @@ jobs:
- name: Install dependencies
run: |
cd embedchain
cd mem0
poetry install
- name: Build a binary wheel and a source tarball
run: |
cd embedchain
cd mem0
poetry build
- name: Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository_url: https://test.pypi.org/legacy/
packages_dir: embedchain/dist/
# TODO: Needs to setup mem0 repo on Test PyPI
# - name: Publish distribution 📦 to Test PyPI
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# repository_url: https://test.pypi.org/legacy/
# packages_dir: dist/
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages_dir: embedchain/dist/
packages_dir: dist/
+10 -6
View File
@@ -16,6 +16,8 @@
<a href="https://mem0.ai">Learn more</a>
·
<a href="https://mem0.dev/DiG">Join Discord</a>
·
<a href="https://mem0.dev/demo">Demo</a>
</p>
</p>
@@ -79,7 +81,7 @@ npm install mem0ai
### Basic Usage
Mem0 requires an LLM to function, with `gpt-4o` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/llms).
Mem0 requires an LLM to function, with `gpt-4o-mini` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/llms).
First step is to instantiate the memory:
@@ -93,7 +95,7 @@ memory = Memory()
def chat_with_memories(message: str, user_id: str = "default_user") -> str:
# Retrieve relevant memories
relevant_memories = memory.search(query=message, user_id=user_id, limit=3)
memories_str = "\n".join(f"- {entry['memory']}" for entry in relevant_memories)
memories_str = "\n".join(f"- {entry['memory']}" for entry in relevant_memories["results"])
# Generate Assistant response
system_prompt = f"You are a helpful AI. Answer the question based on query and memories.\nUser Memories:\n{memories_str}"
@@ -129,15 +131,17 @@ For more advanced usage and API documentation, visit our [documentation](https:/
## Demos
- AI Companion: Experience personalized conversations with an AI that remembers your preferences and past interactions
- Mem0 - ChatGPT with Memory: A personalized AI chat app powered by Mem0 that remembers your preferences, facts, and memories.
[AI Companion Demo](https://github.com/user-attachments/assets/3fc72023-a72c-4593-8be0-3cee3ba744da)
[Mem0 - ChatGPT with Memory](https://github.com/user-attachments/assets/cebc4f8e-bdb9-4837-868d-13c5ab7bb433)
Try live [demo](https://mem0.dev/demo/)
<br/><br/>
- Mem0 Demo: A personalized AI chat app powered by Mem0 that remembers your preferences, facts, and memories.
- AI Companion: Experience personalized conversations with an AI that remembers your preferences and past interactions
[Mem0 Demo](https://github.com/user-attachments/assets/cebc4f8e-bdb9-4837-868d-13c5ab7bb433)
[AI Companion Demo](https://github.com/user-attachments/assets/3fc72023-a72c-4593-8be0-3cee3ba744da)
<br/><br/>
+9 -2
View File
@@ -8,8 +8,9 @@ iconType: "solid"
The `config` is defined as an object with two main keys:
- `vector_store`: Specifies the vector database provider and its configuration
- `provider`: The name of the vector database (e.g., "chroma", "pgvector", "qdrant", "milvus", "azure_ai_search")
- `config`: A nested object containing provider-specific settings
- `provider`: The name of the vector database (e.g., "chroma", "pgvector", "qdrant", "milvus","azure_ai_search", "vertex_ai_vector_search")
- `config`: A nested dictionary containing provider-specific settings
## How to Use Config
@@ -86,6 +87,12 @@ Here's a comprehensive list of all parameters that can be used across different
| `url` | Full URL for the server |
| `api_key` | API key for the server |
| `on_disk` | Enable persistent storage |
| `endpoint_id` | Endpoint ID (vertex_ai_vector_search) |
| `index_id` | Index ID (vertex_ai_vector_search) |
| `deployment_index_id` | Deployment index ID (vertex_ai_vector_search) |
| `project_id` | Project ID (vertex_ai_vector_search) |
| `project_number` | Project number (vertex_ai_vector_search) |
| `vector_search_api_endpoint` | Vector search API endpoint (vertex_ai_vector_search) |
| `connection_string` | PostgreSQL connection string (for Supabase/PGVector) |
| `index_method` | Vector index method (for Supabase) |
| `index_measure` | Distance measure for similarity search (for Supabase) |
@@ -0,0 +1,46 @@
## Google Cloud Vertex AI Vector Search
### Usage
To use Google Cloud Vertex AI Vector Search with `mem0`, you need to configure the `vector_store` in your `mem0` config:
```python
import os
from mem0 import Memory
os.environ["GEMINI_API_KEY"] = = "sk-xx"
config = {
"vector_store": {
"provider": "vertex_ai_vector_search",
"config": {
"endpoint_id": "YOUR_ENDPOINT_ID", # Required: Vector Search endpoint ID
"index_id": "YOUR_INDEX_ID", # Required: Vector Search index ID
"deployment_index_id": "YOUR_DEPLOYMENT_INDEX_ID", # Required: Deployment-specific ID
"project_id": "YOUR_PROJECT_ID", # Required: Google Cloud project ID
"project_number": "YOUR_PROJECT_NUMBER", # Required: Google Cloud project number
"region": "YOUR_REGION", # Optional: Defaults to GOOGLE_CLOUD_REGION
"credentials_path": "path/to/credentials.json", # Optional: Defaults to GOOGLE_APPLICATION_CREDENTIALS
"vector_search_api_endpoint": "YOUR_API_ENDPOINT" # Required for get operations
}
}
}
m = Memory.from_config(config)
m.add("Your text here", user_id="user", metadata={"category": "example"})
```
### Required Parameters
| Parameter | Description | Required |
|-----------|-------------|----------|
| `endpoint_id` | Vector Search endpoint ID | Yes |
| `index_id` | Vector Search index ID | Yes |
| `deployment_index_id` | Deployment-specific index ID | Yes |
| `project_id` | Google Cloud project ID | Yes |
| `project_number` | Google Cloud project number | Yes |
| `vector_search_api_endpoint` | Vector search API endpoint | Yes (for get operations) |
| `region` | Google Cloud region | No (defaults to GOOGLE_CLOUD_REGION) |
| `credentials_path` | Path to service account credentials | No (defaults to GOOGLE_APPLICATION_CREDENTIALS) |
+1
View File
@@ -24,6 +24,7 @@ See the list of supported vector databases below.
<Card title="Elasticsearch" href="/components/vectordbs/dbs/elasticsearch"></Card>
<Card title="OpenSearch" href="/components/vectordbs/dbs/opensearch"></Card>
<Card title="Supabase" href="/components/vectordbs/dbs/supabase"></Card>
<Card title="Vertex AI Vector Search" href="/components/vectordbs/dbs/vertex_ai_vector_search"></Card>
</CardGroup>
## Usage
+10 -4
View File
@@ -129,7 +129,8 @@
"components/vectordbs/dbs/redis",
"components/vectordbs/dbs/elasticsearch",
"components/vectordbs/dbs/opensearch",
"components/vectordbs/dbs/supabase"
"components/vectordbs/dbs/supabase",
"components/vectordbs/dbs/vertex_ai_vector_search"
]
}
]
@@ -180,7 +181,11 @@
"examples/personal-ai-tutor",
"examples/customer-support-agent",
"examples/personal-travel-assistant",
"examples/llama-index-mem0"
"examples/llama-index-mem0",
"examples/chrome-extension",
"examples/document-writing",
"examples/multimodal-demo",
"examples/personalized-deep-research"
]
}
]
@@ -200,7 +205,8 @@
"integrations/langgraph",
"integrations/llama-index",
"integrations/langchain-tools",
"integrations/dify"
"integrations/dify",
"integrations/mcp-server"
]
}
]
@@ -277,7 +283,7 @@
},
{
"anchor": "Demo",
"href": "https://demo.mem0.ai",
"href": "https://mem0.dev/demo",
"icon": "play"
},
{
+55
View File
@@ -0,0 +1,55 @@
# Mem0 Chrome Extension
Enhance your AI interactions with **Mem0**, a Chrome extension that introduces a universal memory layer across platforms like `ChatGPT`, `Claude`, and `Perplexity`. Mem0 ensures seamless context sharing, making your AI experiences more personalized and efficient.
<Note>
🎉 We now support Grok! The Mem0 Chrome Extension has been updated to work with Grok, bringing the same powerful memory capabilities to your Grok conversations.
</Note>
## Features
- **Universal Memory Layer**: Share context seamlessly across ChatGPT, Claude, Perplexity, and Grok.
- **Smart Context Detection**: Automatically captures relevant information from your conversations.
- **Intelligent Memory Retrieval**: Surfaces pertinent memories at the right time.
- **One-Click Sync**: Easily synchronize with existing ChatGPT memories.
- **Memory Dashboard**: Manage all your memories in one centralized location.
## Installation
You can install the Mem0 Chrome Extension using one of the following methods:
### Method 1: Chrome Web Store Installation
1. **Download the Extension**: Open Google Chrome and navigate to the [Mem0 Chrome Extension page](https://chromewebstore.google.com/detail/mem0/onihkkbipkfeijkadecaafbgagkhglop?hl=en).
2. **Add to Chrome**: Click on the "Add to Chrome" button.
3. **Confirm Installation**: In the pop-up dialog, click "Add extension" to confirm. The Mem0 icon should now appear in your Chrome toolbar.
### Method 2: Manual Installation
1. **Download the Extension**: Clone or download the extension files from the [Mem0 Chrome Extension GitHub repository](https://github.com/mem0ai/mem0-chrome-extension).
2. **Access Chrome Extensions**: Open Google Chrome and navigate to `chrome://extensions`.
3. **Enable Developer Mode**: Toggle the "Developer mode" switch in the top right corner.
4. **Load Unpacked Extension**: Click "Load unpacked" and select the directory containing the extension files.
5. **Confirm Installation**: The Mem0 Chrome Extension should now appear in your Chrome toolbar.
## Usage
1. **Locate the Mem0 Icon**: After installation, find the Mem0 icon in your Chrome toolbar.
2. **Sign In**: Click the icon and sign in with your Google account.
3. **Interact with AI Assistants**:
- **ChatGPT and Perplexity**: Continue your conversations as usual; Mem0 operates seamlessly in the background.
- **Claude**: Click the Mem0 button or use the shortcut `Ctrl + M` to activate memory functions.
## Configuration
- **API Key**: Obtain your API key from the Mem0 Dashboard to connect the extension to the Mem0 API.
- **User ID**: This is your unique identifier in the Mem0 system. If not provided, it defaults to 'chrome-extension-user'.
## Demo Video
<iframe width="700" height="400" src="https://www.youtube.com/embed/dqenCMMlfwQ?si=zhGVrkq6IS_0Jwyj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## Privacy and Data Security
Your messages are sent to the Mem0 API for extracting and retrieving memories. Mem0 is committed to ensuring your data's privacy and security.
+2 -2
View File
@@ -94,8 +94,8 @@ You can fetch all the memories at any point in time using the following code:
```python
memories = support_agent.get_memories(user_id=customer_id)
for m in memories:
print(m['text'])
for m in memories['results']:
print(m['memory'])
```
### Key Points
+184
View File
@@ -0,0 +1,184 @@
---
title: Document Editing with Mem0
---
This guide demonstrates how to leverage **Mem0** to edit documents efficiently, ensuring they align with your unique writing style and preferences.
## **Why Use Mem0?**
By integrating Mem0 into your workflow, you can streamline your document editing process with:
1. **Persistent Writing Preferences**: Mem0 stores and recalls your style preferences, ensuring consistency across all documents.
2. **Automated Enhancements**: Your stored preferences guide document refinements, making edits seamless and efficient.
3. **Scalability & Reusability**: Your writing style can be applied to multiple documents, saving time and effort.
---
## **Setup**
```python
import os
from mem0 import MemoryClient
# Set up Mem0 client
os.environ["MEM0_API_KEY"] = "your-mem0-api-key"
client = MemoryClient()
# Define constants
USER_ID = "content_writer"
RUN_ID = "smart_editing_session"
```
---
## **Storing Your Writing Preferences in Mem0**
```python
def store_writing_preferences():
"""Store your writing preferences in Mem0."""
# Define writing preferences
preferences = """My writing preferences:
1. Use headings and sub-headings for structure.
2. Keep paragraphs concise (8-10 sentences max).
3. Incorporate specific numbers and statistics.
4. Provide concrete examples.
5. Use bullet points for clarity.
6. Avoid jargon and buzzwords."""
# Store preferences in Mem0
preference_message = [
{"role": "user", "content": "Here are my writing style preferences"},
{"role": "assistant", "content": preferences}
]
response = client.add(preference_message, user_id=USER_ID, run_id=RUN_ID, metadata={"type": "preferences", "category": "writing_style"})
print("Writing preferences stored successfully.")
return response
```
---
## **Editing Documents with Mem0**
```python
def edit_document_based_on_preferences(original_content):
"""Edit a document using Mem0-based stored preferences."""
# Retrieve stored preferences
query = "What are my writing style preferences?"
preferences_results = client.search(query, user_id=USER_ID, run_id=RUN_ID)
if not preferences_results:
print("No writing preferences found.")
return None
# Extract preferences
preferences = ' '.join(memory["memory"] for memory in preferences_results)
# Apply stored preferences to refine the document
edited_content = f"Applying stored preferences:\n{preferences}\n\nEdited Document:\n{original_content}"
return edited_content
```
---
## **Complete Workflow: Document Editing**
```python
def document_editing_workflow(content):
"""Automated workflow for editing a document based on writing preferences."""
# Step 1: Store writing preferences (if not already stored)
store_writing_preferences()
# Step 2: Edit the document with Mem0 preferences
edited_content = edit_document_based_on_preferences(content)
if not edited_content:
return "Failed to edit document."
# Step 3: Display results
print("\n=== ORIGINAL DOCUMENT ===\n")
print(content)
print("\n=== EDITED DOCUMENT ===\n")
print(edited_content)
return edited_content
```
---
## **Example Usage**
```python
# Define your document
original_content = """Project Proposal
The following proposal outlines our strategy for the Q3 marketing campaign.
We believe this approach will significantly increase our market share.
Increase brand awareness
Boost sales by 15%
Expand our social media following
We plan to launch the campaign in July and continue through September.
"""
# Run the workflow
result = document_editing_workflow(original_content)
```
---
## **Expected Output**
Your document will be transformed into a structured, well-formatted version based on your preferences.
### **Original Document**
```
Project Proposal
The following proposal outlines our strategy for the Q3 marketing campaign.
We believe this approach will significantly increase our market share.
Increase brand awareness
Boost sales by 15%
Expand our social media following
We plan to launch the campaign in July and continue through September.
```
### **Edited Document**
```
# **Project Proposal**
## **Q3 Marketing Campaign Strategy**
This proposal outlines our strategy for the Q3 marketing campaign. We aim to significantly increase our market share with this approach.
### **Objectives**
- **Increase Brand Awareness**: Implement targeted advertising and community engagement to enhance visibility.
- **Boost Sales by 15%**: Increase sales by 15% compared to Q2 figures.
- **Expand Social Media Following**: Grow our social media audience by 20%.
### **Timeline**
- **Launch Date**: July
- **Duration**: July – September
### **Key Actions**
- **Targeted Advertising**: Utilize platforms like Google Ads and Facebook to reach specific demographics.
- **Community Engagement**: Host webinars and live Q&A sessions.
- **Content Creation**: Produce engaging videos and infographics.
### **Supporting Data**
- **Previous Campaign Success**: Our Q2 campaign increased sales by 12%. We will refine similar strategies for Q3.
- **Social Media Growth**: Last year, our Instagram followers grew by 25% during a similar campaign.
### **Conclusion**
We believe this strategy will effectively increase our market share. To achieve these goals, we need your support and collaboration. Let’s work together to make this campaign a success. Please review the proposal and provide your feedback by the end of the week.
```
Mem0 creates a seamless, intelligent document editing experience—perfect for content creators, technical writers, and businesses alike!
+2
View File
@@ -13,6 +13,8 @@ You can create a personalized AI Companion using Mem0. This guide will walk you
src="https://github.com/user-attachments/assets/cebc4f8e-bdb9-4837-868d-13c5ab7bb433"
></video>
You can try the [Mem0 Demo](https://mem0.dev/demo) live here.
## Overview
The Personalized AI Companion leverages Mem0 to retain information across interactions, enabling a tailored learning experience. It creates memories for each user interaction and integrates with OpenAI's GPT models to provide detailed and context-aware responses to user queries.
+31
View File
@@ -0,0 +1,31 @@
---
title: Multimodal Demo with Mem0
---
Enhance your AI interactions with **Mem0**'s multimodal capabilities. Mem0 now supports image understanding, allowing for richer context and more natural interactions across supported AI platforms.
> 🎉 Experience the power of multimodal AI! Test out Mem0's image understanding capabilities at [multimodal-demo.mem0.ai](https://multimodal-demo.mem0.ai)
## 🚀 Features
- **🖼️ Image Understanding**: Share and discuss images with AI assistants while maintaining context.
- **🔍 Smart Visual Context**: Automatically capture and reference visual elements in conversations.
- **🔗 Cross-Modal Memory**: Link visual and textual information seamlessly in your memory layer.
- **📌 Cross-Session Recall**: Reference previously discussed visual content across different conversations.
- **⚡ Seamless Integration**: Works naturally with existing chat interfaces for a smooth experience.
## 📖 How It Works
1. **📂 Upload Visual Content**: Simply drag and drop or paste images into your conversations.
2. **💬 Natural Interaction**: Discuss the visual content naturally with AI assistants.
3. **📚 Memory Integration**: Visual context is automatically stored and linked with your conversation history.
4. **🔄 Persistent Recall**: Retrieve and reference past visual content effortlessly.
## Demo Video
<iframe width="700" height="400" src="https://www.youtube.com/embed/2Md5AEFVpmg?si=rXXupn6CiDUPJsi3" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## 🔥 Try It Out
Visit [multimodal-demo.mem0.ai](https://multimodal-demo.mem0.ai) to experience Mem0's multimodal capabilities firsthand. Upload images and see how Mem0 understands and remembers visual context across your conversations.
+42 -19
View File
@@ -16,23 +16,46 @@ Here are some examples of how Mem0 can be integrated into various applications:
## Examples
<CardGroup cols={2}>
<Card title="AI Companion in Node.js" icon="square-6" href="/examples/ai_companion_js">
Create a Personalized AI Companion using Mem0 in Node.js.
</Card>
<Card title="Mem0 with Ollama" icon="square-1" href="/examples/mem0-with-ollama">
Run Mem0 locally with Ollama.
</Card>
<Card title="Personal AI Tutor" icon="square-2" href="/examples/personal-ai-tutor">
Create a Personalized AI Tutor that adapts to student progress and learning preferences.
</Card>
<Card title="Personal Travel Assistant" icon="square-3" href="/examples/personal-travel-assistant">
Build a Personalized AI Travel Assistant that understands your travel preferences and past itineraries.
</Card>
<Card title="Customer Support Agent" icon="square-4" href="/examples/customer-support-agent">
Develop a Personal AI Assistant that remembers user preferences, past interactions, and context to provide personalized and efficient assistance.
</Card>
<Card title="LlamaIndex Mem0" icon="square-5" href="/examples/llama-index-mem0">
Create a ReAct Agent with LlamaIndex which uses Mem0 as the memory store.
</Card>
Explore how **Mem0** can power real-world applications and bring personalized, intelligent experiences to life:
<CardGroup cols={2}>
<Card title="AI Companion in Node.js" icon="node" href="/examples/ai_companion_js">
Build a personalized AI Companion in **Node.js** that remembers conversations and adapts over time using Mem0.
</Card>
<Card title="Mem0 with Ollama" icon="server" href="/examples/mem0-with-ollama">
Run **Mem0 locally** with **Ollama** to create private, stateful AI experiences without relying on cloud APIs.
</Card>
<Card title="Personal AI Tutor" icon="graduation-cap" href="/examples/personal-ai-tutor">
Create an **AI Tutor** that adapts to student progress, learning style, and history — for a truly customized learning experience.
</Card>
<Card title="Personal Travel Assistant" icon="plane" href="/examples/personal-travel-assistant">
Develop a **Personal Travel Assistant** that remembers your preferences, past trips, and helps plan future adventures.
</Card>
<Card title="Customer Support Agent" icon="headset" href="/examples/customer-support-agent">
Build a **Customer Support AI** that recalls user preferences, past chats, and provides context-aware, efficient help.
</Card>
<Card title="LlamaIndex + Mem0" icon="book-open" href="/examples/llama-index-mem0">
Combine **LlamaIndex** and Mem0 to create a powerful **ReAct Agent** with persistent memory for smarter interactions.
</Card>
<Card title="Chrome Extension" icon="puzzle-piece" href="/examples/chrome-extension">
Add **long-term memory** to ChatGPT, Claude, or Perplexity via the **Mem0 Chrome Extension** — personalize your AI chats anywhere.
</Card>
<Card title="Document Writing Assistant" icon="pen" href="/examples/document-writing">
Create a **Writing Assistant** that understands and adapts to your unique style, improving consistency and productivity.
</Card>
<Card title="Multimodal AI Demo" icon="image" href="/examples/multimodal-demo">
Supercharge AI with **Mem0's multimodal memory** — blend text, images, and more for richer, context-aware interactions.
</Card>
<Card title="Personalized Research Agent" icon="robot" href="/examples/personalized-deep-research">
Build a **Deep Research AI** that remembers your research goals and compiles insights from vast information sources.
</Card>
</CardGroup>
+2 -2
View File
@@ -96,8 +96,8 @@ You can fetch all the memories at any point in time using the following code:
```python
memories = ai_tutor.get_memories(user_id=user_id)
for m in memories:
print(m['text'])
for m in memories['results']:
print(m['memory'])
```
### Key Points
+2 -2
View File
@@ -82,11 +82,11 @@ class PersonalTravelAssistant:
def get_memories(self, user_id):
memories = self.memory.get_all(user_id=user_id)
return [m['memory'] for m in memories['memories']]
return [m['memory'] for m in memories['results']]
def search_memories(self, query, user_id):
memories = self.memory.search(query, user_id=user_id)
return [m['memory'] for m in memories['memories']]
return [m['memory'] for m in memories['results']]
# Usage example
user_id = "traveler_123"
@@ -0,0 +1,70 @@
---
title: Personalized Deep Research
---
Deep Research is an intelligent agent that synthesizes large amounts of online data and completes complex research tasks, customized to your unique preferences and insights. Built on Mem0's technology, it enhances AI-driven online exploration with personalized memories.
## Overview
Deep Research leverages Mem0's memory capabilities to:
- Synthesize large amounts of online data
- Complete complex research tasks
- Customize results to your preferences
- Store and utilize personal insights
- Maintain context across research sessions
## Demo
Watch Deep Research in action:
<iframe
width="700"
height="400"
src="https://www.youtube.com/embed/8vQlCtXzF60?si=b8iTOgummAVzR7ia"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
## Getting Started
1. Visit [deep-research.mem0.ai](https://deep-research.mem0.ai/)
2. Upload your resume (PDF or text) or manually enter information about yourself
3. Enter your research topic
4. Click "Start Research" to begin
## Features
### 1. Personalized Research
- Analyzes your background and expertise
- Tailors research depth and complexity to your level
- Incorporates your previous research context
### 2. Comprehensive Data Synthesis
- Processes multiple online sources
- Extracts relevant information
- Provides coherent summaries
### 3. Memory Integration
- Stores research findings for future reference
- Maintains context across sessions
- Links related research topics
### 4. Interactive Exploration
- Allows real-time query refinement
- Supports follow-up questions
- Enables deep-diving into specific areas
## Use Cases
- **Academic Research**: Literature reviews, thesis research, paper writing
- **Market Research**: Industry analysis, competitor research, trend identification
- **Technical Research**: Technology evaluation, solution comparison
- **Business Research**: Strategic planning, opportunity analysis
## Try It Out
Experience AI-powered research personalization at [deep-research.mem0.ai](https://deep-research.mem0.ai/)
+41 -2
View File
@@ -84,9 +84,48 @@ iconType: "solid"
- Include specific examples or cases rather than general definitions
</Accordion>
<Accordion title="How do I configure Mem0 for AWS Lambda?">
When deploying Mem0 on AWS Lambda, you'll need to modify the storage directory configuration due to Lambda's file system restrictions. By default, Lambda only allows writing to the `/tmp` directory.
To configure Mem0 for AWS Lambda, set the `MEM0_DIR` environment variable to point to a writable directory in `/tmp`:
```bash
MEM0_DIR=/tmp/.mem0
```
If you're not using environment variables, you'll need to modify the storage path in your code:
```python
# Change from
home_dir = os.path.expanduser("~")
mem0_dir = os.environ.get("MEM0_DIR") or os.path.join(home_dir, ".mem0")
# To
mem0_dir = os.environ.get("MEM0_DIR", "/tmp/.mem0")
```
Note that the `/tmp` directory in Lambda has a size limit of 512MB and its contents are not persistent between function invocations.
</Accordion>
<Accordion title="How can I use metadata with Mem0?">
Metadata is the recommended approach for incorporating additional information with Mem0. You can store any type of structured data as metadata during the `add` method, such as location, timestamp, weather conditions, user state, or application context. This enriches your memories with valuable contextual information that can be used for more precise retrieval and filtering.
During retrieval, you have two main approaches for using metadata:
1. **Pre-filtering**: Include metadata parameters in your initial search query to narrow down the memory pool
2. **Post-processing**: Retrieve a broader set of memories based on query, then apply metadata filters to refine the results
Examples of useful metadata you might store:
- **Contextual information**: Location, time, device type, application state
- **User attributes**: Preferences, skill levels, demographic information
- **Interaction details**: Conversation topics, sentiment, urgency levels
- **Custom tags**: Any domain-specific categorization relevant to your application
This flexibility allows you to create highly contextually aware AI applications that can adapt to specific user needs and situations. Metadata provides an additional dimension for memory retrieval, enabling more precise and relevant responses.
</Accordion>
</AccordionGroup>
+57 -3
View File
@@ -14,23 +14,77 @@ Mem0's **Advanced Retrieval** feature delivers superior search results by levera
client.search(query, keyword_search=True, user_id='alex')
```
**Example:**
```python
# Search for memories about food preferences with keyword search enabled
query = "What are my food preferences?"
results = client.search(query, keyword_search=True, user_id='alex')
# Output might include:
# - "Vegetarian. Allergic to nuts." (highly relevant)
# - "Prefers spicy food and enjoys Thai cuisine" (relevant)
# - "Mentioned disliking sea food during restaurant discussion" (keyword match)
# Without keyword_search=True, only the most relevant memories would be returned:
# - "Vegetarian. Allergic to nuts." (highly relevant)
# - "Prefers spicy food and enjoys Thai cuisine" (relevant)
# The keyword-based match about "sea food" would be excluded
```
2. **Reranking**
Reranking allows you to reorder the memories returned by the default search based on relevance. This parameter is set to `false` by default. When enabled, it reorders the memories based on the relevance score.
Normal retrieval gives you memories sorted in order of their relevancy, but the order may not be perfect. Reranking uses a deep neural network to correct this order, ensuring the most relevant memories appear first. If you are concerned about the order of memories, or want that the best results always comes at top then use reranking. This parameter is set to `false` by default. When enabled, it reorders the memories based on a more accurate relevance score.
```python
client.search(query, rerank=True, user_id='alex')
```
**Example:**
```python
# Search for travel plans with reranking enabled
query = "What are my travel plans?"
results = client.search(query, rerank=True, user_id='alex')
# Without reranking, results might be ordered like:
# 1. "Traveled to France last year" (less relevant to current plans)
# 2. "Planning a trip to Japan next month" (more relevant to current plans)
# 3. "Interested in visiting Tokyo restaurants" (relevant to current plans)
# With reranking enabled, results would be reordered:
# 1. "Planning a trip to Japan next month" (most relevant to current plans)
# 2. "Interested in visiting Tokyo restaurants" (highly relevant to current plans)
# 3. "Traveled to France last year" (less relevant to current plans)
```
3. **Filtering**
Filtering enables you to narrow down the search results by applying specific criteria. This parameter is set to `false` by default. Activating it enhances search precision, potentially reducing recall by a small margin.
Filtering allows you to narrow down search results by applying specific criterias. This parameter is set to `false` by default. When activated, it significantly enhances search precision by removing irrelevant memories, though it may slightly reduce recall. Filtering is particularly useful when you need highly specific information.
```python
client.search(query, filter_memories=True, user_id='alex')
```
**Note:** You can enable or disable these search modes by passing the respective parameters to the `search` method. There is no required sequence for these modes, and any combination can be used based on your needs.
**Example:**
```python
# Search for dietary restrictions with filtering enabled
query = "What are my dietary restrictions?"
results = client.search(query, filter_memories=True, user_id='alex')
# Without filtering, results might include:
# - "Vegetarian. Allergic to nuts." (directly relevant)
# - "I enjoy cooking Italian food on weekends" (somewhat related to food)
# - "Mentioned disliking seafood during restaurant discussion" (food-related)
# - "Prefers to eat dinner at 7pm" (tangentially food-related)
# With filtering enabled, results would be focused:
# - "Vegetarian. Allergic to nuts." (directly relevant)
# - "Mentioned disliking seafood during restaurant discussion" (relevant restriction)
#
# The filtering process removes memories that are about food preferences
# but not specifically about dietary restrictions
```
<Note> You can enable or disable these search modes by passing the respective parameters to the `search` method. There is no required sequence for these modes, and any combination can be used based on your needs. </Note>
### Latency Numbers
+53 -2
View File
@@ -12,7 +12,7 @@ Mem0 extends its capabilities beyond text by supporting multimodal data, includi
When a user submits an image, Mem0 processes it to extract textual information and other pertinent details. These details are then added to the user's memory, enhancing the system's ability to understand and recall visual inputs.
<CodeGroup>
```python Code
```python Python
import os
from mem0 import MemoryClient
@@ -44,6 +44,34 @@ messages = [
client.add(messages, user_id="alice")
```
```typescript TypeScript
import MemoryClient from "mem0ai";
const client = new MemoryClient();
const messages = [
{
role: "user",
content: "Hi, my name is Alice."
},
{
role: "assistant",
content: "Nice to meet you, Alice! What do you like to eat?"
},
{
role: "user",
content: {
type: "image_url",
image_url: {
url: "https://www.superhealthykids.com/wp-content/uploads/2021/10/best-veggie-pizza-featured-image-square-2.jpg"
}
}
},
]
await client.add(messages, { user_id: "alice" })
```
```json Output
{
"results": [
@@ -90,7 +118,9 @@ client.add([image_message], user_id="alice")
## 2. Using Base64 Image Encoding for Local Files
For local images—or when embedding the image directly is preferable—you can use a Base64-encoded string.
```python
<CodeGroup>
```python Python
import base64
# Path to the image file
@@ -113,6 +143,27 @@ image_message = {
client.add([image_message], user_id="alice")
```
```typescript TypeScript
import MemoryClient from "mem0ai";
import fs from 'fs';
const imagePath = 'path/to/your/image.jpg';
const base64Image = fs.readFileSync(imagePath, { encoding: 'base64' });
const imageMessage = {
role: "user",
content: {
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image}`
}
}
};
await client.add([imageMessage], { user_id: "alice" })
```
</CodeGroup>
Using these methods, you can seamlessly incorporate images into your interactions, further enhancing Mem0's multimodal capabilities.
If you have any questions, please feel free to reach out to us using one of the following methods:
+60
View File
@@ -0,0 +1,60 @@
---
title: MCP Server
---
## Integrating mem0 as an MCP Server in Cursor
[mem0](https://github.com/mem0ai/mem0-mcp) is a powerful tool designed to enhance AI-driven workflows, particularly in code generation and contextual memory. In this guide, we'll walk through integrating mem0 as an **MCP (Model Context Protocol) server** within [Cursor](https://cursor.sh/), an AI-powered coding editor.
## Prerequisites
Before proceeding, ensure you have the following installed:
- Cursor IDE
- Python (>=3.8)
- Git
- [mem0-mcp](https://github.com/mem0ai/mem0-mcp) (Clone the repository and set up as per the instructions in the README)
## Configuring Cursor to use mem0 as an MCP Server
1. **Open Cursor.**
2. **Navigate to `Settings` > `Cursor Settings` > `Features` > `MCP Servers`.**
3. **Add a new provider using the MCP server:**
- Click on **`Add new MCP server`**
- Provide a name for the server, e.g. `mem0` and select type as `sse`
- Enter the **SSE Endpoint**: `http://0.0.0.0:8080/sse`
4. **Save and Restart Cursor** to apply changes.
## Demo
<iframe width="560" height="315" src="https://www.youtube.com/embed/fWa6KX7cpG8?si=cmJDz2sQevGnItSI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## Using mem0 in Cursor
Once integrated, mem0 can assist with contextual memory and AI-driven coding enhancements. Some key functionalities include:
### 1. Storing Coding Preferences
Mem0 can store and manage coding preferences, including:
- Complete code snippets with dependencies
- Language/framework versions
- Documentation and comments
- Best practices and example usage
### 2. Retrieving Stored Preferences
Access all stored coding references to:
- Review implementations
- Maintain consistency in coding practices
### 3. Semantic Search for Preferences
Use natural language queries to find:
- Code snippets
- Technical documentation
- Best practices
- Setup guides
## Benefits of Using mem0 in Cursor
- **Persistent Context Storage**: Retain and reuse coding insights across sessions.
- **Seamless Integration**: Works directly within Cursor as an MCP server.
- **Efficient Search**: Retrieve relevant coding insights using semantic search.
## Conclusion
By integrating mem0 as an MCP server within Cursor, you enhance your development workflow with AI-powered memory and context-aware assistance. Follow the steps above to set up and start leveraging mem0 in your coding environment.
For more details on MCP integration, refer to Cursor's [Model Context Protocol documentation](https://docs.cursor.com/context/model-context-protocol).
+22
View File
@@ -198,4 +198,26 @@ Here are the available integrations for Mem0:
>
Build AI applications with persistent memory using Dify and Mem0.
</Card>
<Card
title="MCP Server"
icon={
<svg
viewBox="0 0 180 180"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
>
<path
d="M45 45 L135 45 M45 90 L135 90 M45 135 L135 135"
stroke="currentColor"
strokeWidth="12"
strokeLinecap="round"
fill="none"
/>
</svg>
}
href="/integrations/mcp-server"
>
Integrate Mem0 as an MCP Server in Cursor.
</Card>
</CardGroup>
+34 -14
View File
@@ -16,23 +16,43 @@ Users can add a customized prompt that will be used to extract specific entities
This allows for more targeted and relevant information extraction based on the user's needs.
Here's an example of how to add a customized prompt:
```python
from mem0 import Memory
<CodeGroup>
```python Python
from mem0 import Memory
config = {
"graph_store": {
"provider": "neo4j",
"config": {
"url": "neo4j+s://xxx",
"username": "neo4j",
"password": "xxx"
},
"custom_prompt": "Please only extract entities containing sports related relationships and nothing else.",
config = {
"graph_store": {
"provider": "neo4j",
"config": {
"url": "neo4j+s://xxx",
"username": "neo4j",
"password": "xxx"
},
"custom_prompt": "Please only extract entities containing sports related relationships and nothing else.",
}
}
}
m = Memory.from_config(config_dict=config)
```
m = Memory.from_config(config_dict=config)
```
```typescript TypeScript
import { Memory } from "mem0ai/oss";
const config = {
graphStore: {
provider: "neo4j",
config: {
url: "neo4j+s://xxx",
username: "neo4j",
password: "xxx",
},
customPrompt: "Please only extract entities containing sports related relationships and nothing else.",
}
}
const memory = new Memory(config);
```
</CodeGroup>
If you want to use a managed version of Mem0, please check out [Mem0](https://mem0.dev/pd). If you have any questions, please feel free to reach out to us using one of the following methods:
+152 -20
View File
@@ -9,14 +9,24 @@ Mem0 now supports **Graph Memory**.
With Graph Memory, users can now create and utilize complex relationships between pieces of information, allowing for more nuanced and context-aware responses.
This integration enables users to leverage the strengths of both vector-based and graph-based approaches, resulting in more accurate and comprehensive information retrieval and generation.
<Note>
NodeSDK now supports Graph Memory. 🎉
</Note>
## Installation
To use Mem0 with Graph Memory support, install it using pip:
```bash
<CodeGroup>
```bash Python
pip install "mem0ai[graph]"
```
```bash TypeScript
npm install mem0ai
```
</CodeGroup>
This command installs Mem0 along with the necessary dependencies for graph functionality.
Try Graph Memory on Google Colab.
@@ -42,7 +52,6 @@ Currently, we support Neo4j as a graph store provider. You can setup [Neo4j](htt
<Note>If you are using Neo4j locally, then you need to install [APOC plugins](https://neo4j.com/labs/apoc/4.1/installation/).</Note>
User can also customize the LLM for Graph Memory from the [Supported LLM list](https://docs.mem0.ai/components/llms/overview) with three levels of configuration:
1. **Main Configuration**: If `llm` is set in the main config, it will be used for all graph operations.
@@ -53,7 +62,7 @@ Here's how you can do it:
<CodeGroup>
```python Basic
```python Python
from mem0 import Memory
config = {
@@ -70,9 +79,25 @@ config = {
m = Memory.from_config(config_dict=config)
```
```python Advanced (Custom LLM)
from mem0 import Memory
```typescript TypeScript
import { Memory } from "mem0ai/oss";
const config = {
enableGraph: true,
graphStore: {
provider: "neo4j",
config: {
url: "neo4j+s://xxx",
username: "neo4j",
password: "xxx",
}
}
}
const memory = new Memory(config);
```
```python Python (Advanced)
config = {
"llm": {
"provider": "openai",
@@ -101,22 +126,61 @@ config = {
m = Memory.from_config(config_dict=config)
```
```typescript TypeScript (Advanced)
const config = {
llm: {
provider: "openai",
config: {
model: "gpt-4o",
temperature: 0.2,
max_tokens: 2000,
}
},
enableGraph: true,
graphStore: {
provider: "neo4j",
config: {
url: "neo4j+s://xxx",
username: "neo4j",
password: "xxx",
},
llm: {
provider: "openai",
config: {
model: "gpt-4o-mini",
temperature: 0.0,
}
}
}
}
const memory = new Memory(config);
```
</CodeGroup>
<Note>
If you are using NodeSDK, you need to pass `enableGraph` as `true` in the `config` object.
</Note>
## Graph Operations
The Mem0's graph supports the following operations:
### Add Memories
<Note>
If you are using Mem0 with Graph Memory, it is recommended to pass `user_id`. The default value of `user_id` (in case of graph memory) is `user`.
If you are using Mem0 with Graph Memory, it is recommended to pass `user_id`. Use `userId` in NodeSDK.
</Note>
<CodeGroup>
```python Code
```python Python
m.add("I like pizza", user_id="alice")
```
```typescript TypeScript
memory.add("I like pizza", { userId: "alice" });
```
```json Output
{'message': 'ok'}
```
@@ -126,10 +190,14 @@ m.add("I like pizza", user_id="alice")
### Get all memories
<CodeGroup>
```python Code
```python Python
m.get_all(user_id="alice")
```
```typescript TypeScript
memory.getAll({ userId: "alice" });
```
```json Output
{
'memories': [
@@ -157,10 +225,14 @@ m.get_all(user_id="alice")
### Search Memories
<CodeGroup>
```python Code
```python Python
m.search("tell me my name.", user_id="alice")
```
```typescript TypeScript
memory.search("tell me my name.", { userId: "alice" });
```
```json Output
{
'memories': [
@@ -187,10 +259,16 @@ m.search("tell me my name.", user_id="alice")
### Delete all Memories
```python
<CodeGroup>
```python Python
m.delete_all(user_id="alice")
```
```typescript TypeScript
memory.deleteAll({ userId: "alice" });
```
</CodeGroup>
# Example Usage
Here's an example of how to use Mem0's graph operations:
@@ -206,64 +284,110 @@ Below are the steps to add memories and visualize the graph:
<Steps>
<Step title="Add memory 'I like going to hikes'">
```python
<CodeGroup>
```python Python
m.add("I like going to hikes", user_id="alice123")
```
```typescript TypeScript
memory.add("I like going to hikes", { userId: "alice123" });
```
</CodeGroup>
![Graph Memory Visualization](/images/graph_memory/graph_example1.png)
</Step>
<Step title="Add memory 'I love to play badminton'">
```python
<CodeGroup>
```python Python
m.add("I love to play badminton", user_id="alice123")
```
```typescript TypeScript
memory.add("I love to play badminton", { userId: "alice123" });
```
</CodeGroup>
![Graph Memory Visualization](/images/graph_memory/graph_example2.png)
</Step>
<Step title="Add memory 'I hate playing badminton'">
```python
<CodeGroup>
```python Python
m.add("I hate playing badminton", user_id="alice123")
```
```typescript TypeScript
memory.add("I hate playing badminton", { userId: "alice123" });
```
</CodeGroup>
![Graph Memory Visualization](/images/graph_memory/graph_example3.png)
</Step>
<Step title="Add memory 'My friend name is john and john has a dog named tommy'">
```python
<CodeGroup>
```python Python
m.add("My friend name is john and john has a dog named tommy", user_id="alice123")
```
```typescript TypeScript
memory.add("My friend name is john and john has a dog named tommy", { userId: "alice123" });
```
</CodeGroup>
![Graph Memory Visualization](/images/graph_memory/graph_example4.png)
</Step>
<Step title="Add memory 'My name is Alice'">
```python
<CodeGroup>
```python Python
m.add("My name is Alice", user_id="alice123")
```
```typescript TypeScript
memory.add("My name is Alice", { userId: "alice123" });
```
</CodeGroup>
![Graph Memory Visualization](/images/graph_memory/graph_example5.png)
</Step>
<Step title="Add memory 'John loves to hike and Harry loves to hike as well'">
```python
<CodeGroup>
```python Python
m.add("John loves to hike and Harry loves to hike as well", user_id="alice123")
```
```typescript TypeScript
memory.add("John loves to hike and Harry loves to hike as well", { userId: "alice123" });
```
</CodeGroup>
![Graph Memory Visualization](/images/graph_memory/graph_example6.png)
</Step>
<Step title="Add memory 'My friend peter is the spiderman'">
```python
<CodeGroup>
```python Python
m.add("My friend peter is the spiderman", user_id="alice123")
```
```typescript TypeScript
memory.add("My friend peter is the spiderman", { userId: "alice123" });
```
</CodeGroup>
![Graph Memory Visualization](/images/graph_memory/graph_example7.png)
</Step>
@@ -274,10 +398,14 @@ m.add("My friend peter is the spiderman", user_id="alice123")
### Search Memories
<CodeGroup>
```python Code
```python Python
m.search("What is my name?", user_id="alice123")
```
```typescript TypeScript
memory.search("What is my name?", { userId: "alice123" });
```
```json Output
{
'memories': [...],
@@ -297,10 +425,14 @@ Below graph visualization shows what nodes and relationships are fetched from th
![Graph Memory Visualization](/images/graph_memory/graph_example8.png)
<CodeGroup>
```python Code
```python Python
m.search("Who is spiderman?", user_id="alice123")
```
```typescript TypeScript
memory.search("Who is spiderman?", { userId: "alice123" });
```
```json Output
{
'memories': [...],
+155 -6
View File
@@ -10,11 +10,25 @@ Mem0 extends its capabilities beyond text by supporting multimodal data, includi
When a user provides an image, Mem0 processes the image to extract textual information and relevant details, which are then added to the user's memory. This feature enhances the system's ability to understand and remember details based on visual inputs.
<Note>
To enable multimodal support, you must set `enable_vision = True` in your configuration. The `vision_details` parameter can be set to "auto" (default), "low", or "high" to control the level of detail in image processing.
</Note>
<CodeGroup>
```python Code
from mem0 import Memory
client = Memory()
config = {
"llm": {
"provider": "openai",
"config": {
"enable_vision": True,
"vision_details": "high"
}
}
}
client = Memory.from_config(config=config)
messages = [
{
@@ -40,6 +54,34 @@ messages = [
client.add(messages, user_id="alice")
```
```typescript TypeScript
import { Memory, Message } from "mem0ai/oss";
const client = new Memory();
const messages: Message[] = [
{
role: "user",
content: "Hi, my name is Alice."
},
{
role: "assistant",
content: "Nice to meet you, Alice! What do you like to eat?"
},
{
role: "user",
content: {
type: "image_url",
image_url: {
url: "https://www.superhealthykids.com/wp-content/uploads/2021/10/best-veggie-pizza-featured-image-square-2.jpg"
}
}
},
]
await client.add(messages, { userId: "alice" })
```
```json Output
{
"results": [
@@ -66,6 +108,7 @@ Mem0 allows you to add images to user interactions through two primary methods:
You can include an image by passing its direct URL. This method is simple and efficient for online images.
<CodeGroup>
```python
# Define the image URL
image_url = "https://www.superhealthykids.com/wp-content/uploads/2021/10/best-veggie-pizza-featured-image-square-2.jpg"
@@ -82,11 +125,33 @@ image_message = {
}
```
```typescript TypeScript
import { Memory, Message } from "mem0ai/oss";
const client = new Memory();
const imageUrl = "https://www.superhealthykids.com/wp-content/uploads/2021/10/best-veggie-pizza-featured-image-square-2.jpg";
const imageMessage: Message = {
role: "user",
content: {
type: "image_url",
image_url: {
url: imageUrl
}
}
}
await client.add([imageMessage], { userId: "alice" })
```
</CodeGroup>
## 2. Using Base64 Image Encoding for Local Files
For local images or scenarios where embedding the image directly is preferable, you can use a Base64-encoded string.
```python
<CodeGroup>
```python Python
import base64
# Path to the image file
@@ -108,11 +173,95 @@ image_message = {
}
```
By utilizing these methods, you can effectively incorporate images into user interactions, enhancing the multimodal capabilities of your Mem0 instance.
```typescript TypeScript
import { Memory, Message } from "mem0ai/oss";
<Note>
Currently, we support only OpenAI models for image description.
</Note>
const client = new Memory();
const imagePath = "path/to/your/image.jpg";
const base64Image = fs.readFileSync(imagePath, { encoding: 'base64' });
const imageMessage: Message = {
role: "user",
content: {
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image}`
}
}
}
await client.add([imageMessage], { userId: "alice" })
```
</CodeGroup>
## 3. OpenAI-Compatible Message Format
You can also use the OpenAI-compatible format to combine text and images in a single message:
<CodeGroup>
```python Python
import base64
# Path to the image file
image_path = "path/to/your/image.jpg"
# Encode the image in Base64
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
# Create the message using OpenAI-compatible format
message = {
"role": "user",
"content": [
{
"type": "text",
"text": "What is in this image?",
},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
},
],
}
# Add the message to memory
client.add([message], user_id="alice")
```
```typescript TypeScript
import { Memory, Message } from "mem0ai/oss";
const client = new Memory();
const imagePath = "path/to/your/image.jpg";
const base64Image = fs.readFileSync(imagePath, { encoding: 'base64' });
const message: Message = {
role: "user",
content: [
{
type: "text",
text: "What is in this image?",
},
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image}`
}
},
],
}
await client.add([message], { userId: "alice" })
```
</CodeGroup>
This format allows you to combine text and images in a single message, making it easier to provide context along with visual content.
By utilizing these methods, you can effectively incorporate images into user interactions, enhancing the multimodal capabilities of your Mem0 instance.
If you have any questions, please feel free to reach out to us using one of the following methods:
+33 -24
View File
@@ -74,14 +74,14 @@ const messages = [
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
]
await memory.add(messages, { userId: "user123", metadata: { category: "movie_recommendations" } });
await memory.add(messages, { userId: "alice", metadata: { category: "movie_recommendations" } });
```
```json Output
{
"results": [
{
"id": "c03c9045-df76-4949-bbc5-d5dc1932aa5c",
"id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"memory": "User is planning to watch a movie tonight.",
"metadata": {
"category": "movie_recommendations"
@@ -111,7 +111,7 @@ await memory.add(messages, { userId: "user123", metadata: { category: "movie_rec
<CodeGroup>
```typescript Code
// Get all memories
const allMemories = await memory.getAll({ userId: "user123" });
const allMemories = await memory.getAll({ userId: "alice" });
console.log(allMemories)
```
@@ -127,7 +127,7 @@ console.log(allMemories)
"metadata": {
"category": "movie_recommendations"
},
"userId": "user123"
"userId": "alice"
},
{
"id": "475bde34-21e6-42ab-8bef-0ab84474f156",
@@ -138,7 +138,7 @@ console.log(allMemories)
"metadata": {
"category": "movie_recommendations"
},
"userId": "user123"
"userId": "alice"
},
{
"id": "cbb1fe73-0bf1-4067-8c1f-63aa53e7b1a4",
@@ -149,7 +149,7 @@ console.log(allMemories)
"metadata": {
"category": "movie_recommendations"
},
"userId": "user123"
"userId": "alice"
}
]
}
@@ -176,7 +176,7 @@ console.log(singleMemory);
"metadata": {
"category": "movie_recommendations"
},
"userId": "user123"
"userId": "alice"
}
```
</CodeGroup>
@@ -185,7 +185,7 @@ console.log(singleMemory);
<CodeGroup>
```typescript Code
const result = await memory.search('What do you know about me?', { userId: "user123" });
const result = await memory.search('What do you know about me?', { userId: "alice" });
console.log(result);
```
@@ -202,7 +202,7 @@ console.log(result);
"metadata": {
"category": "movie_recommendations"
},
"userId": "user123"
"userId": "alice"
},
{
"id": "475bde34-21e6-42ab-8bef-0ab84474f156",
@@ -214,7 +214,7 @@ console.log(result);
"metadata": {
"category": "movie_recommendations"
},
"userId": "user123"
"userId": "alice"
},
{
"id": "cbb1fe73-0bf1-4067-8c1f-63aa53e7b1a4",
@@ -226,7 +226,7 @@ console.log(result);
"metadata": {
"category": "movie_recommendations"
},
"userId": "user123"
"userId": "alice"
}
]
}
@@ -263,23 +263,23 @@ console.log(history);
[
{
"id": 39,
"memory_id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"previous_value": "User is planning to watch a movie tonight.",
"new_value": "I love India, it is my favorite country.",
"memoryId": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"previousValue": "User is planning to watch a movie tonight.",
"newValue": "I love India, it is my favorite country.",
"action": "UPDATE",
"created_at": "2025-02-27T16:33:20.557Z",
"updated_at": "2025-02-27T16:33:27.051Z",
"is_deleted": 0
"createdAt": "2025-02-27T16:33:20.557Z",
"updatedAt": "2025-02-27T16:33:27.051Z",
"isDeleted": 0
},
{
"id": 37,
"memory_id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"previous_value": null,
"new_value": "User is planning to watch a movie tonight.",
"memoryId": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"previousValue": null,
"newValue": "User is planning to watch a movie tonight.",
"action": "ADD",
"created_at": "2025-02-27T16:33:20.557Z",
"updated_at": null,
"is_deleted": 0
"createdAt": "2025-02-27T16:33:20.557Z",
"updatedAt": null,
"isDeleted": 0
}
]
```
@@ -292,7 +292,7 @@ console.log(history);
await memory.delete('892db2ae-06d9-49e5-8b3e-585ef9b85b8e');
// Delete all memories for a user
await memory.deleteAll({ userId: "user123" });
await memory.deleteAll({ userId: "alice" });
```
### Reset Memory
@@ -327,6 +327,15 @@ Mem0 offers extensive configuration options to customize its behavior according
| `openaiBaseUrl` | Base URL for OpenAI API | OpenAI |
</Accordion>
<Accordion title="Graph Store Configuration">
| Parameter | Description | Default |
|-------------|---------------------------------|-------------|
| `provider` | Graph store provider (e.g., "neo4j") | "neo4j" |
| `url` | Connection URL | env.NEO4J_URL |
| `username` | Authentication username | env.NEO4J_USERNAME |
| `password` | Authentication password | env.NEO4J_PASSWORD |
</Accordion>
<Accordion title="Embedder Configuration">
| Parameter | Description | Default |
|-------------|---------------------------------|------------------------------|
+134 -57
View File
@@ -85,20 +85,43 @@ m = Memory.from_config(config_dict=config)
<CodeGroup>
```python Code
# For a user
result = m.add("Likes to play cricket on weekends", user_id="alice", metadata={"category": "hobbies"})
const messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about a thriller movies? They can be quite engaging."},
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
]
# messages = [
# {"role": "user", "content": "Hi, I'm Alex. I like to play cricket on weekends."},
# {"role": "assistant", "content": "Hello Alex! It's great to know that you enjoy playing cricket on weekends. I'll remember that for future reference."}
# ]
# client.add(messages, user_id="alice")
result = m.add(messages, user_id="alice", metadata={"category": "movie_recommendations"})
```
```json Output
{
"results": [
{"id": "bf4d4092-cf91-4181-bfeb-b6fa2ed3061b", "memory": "Likes to play cricket on weekends", "event": "ADD"}
{
"id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"memory": "User is planning to watch a movie tonight.",
"metadata": {
"category": "movie_recommendations"
},
"event": "ADD"
},
{
"id": "cbb1fe73-0bf1-4067-8c1f-63aa53e7b1a4",
"memory": "User is not a big fan of thriller movies.",
"metadata": {
"category": "movie_recommendations"
},
"event": "ADD"
},
{
"id": "475bde34-21e6-42ab-8bef-0ab84474f156",
"memory": "User loves sci-fi movies.",
"metadata": {
"category": "movie_recommendations"
},
"event": "ADD"
}
]
}
```
@@ -115,14 +138,38 @@ all_memories = m.get_all(user_id="alice")
```json Output
{
"results": [
{
"id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"memory": "User is planning to watch a movie tonight.",
"hash": "1a271c007316c94377175ee80e746a19",
"created_at": "2025-02-27T16:33:20.557Z",
"updated_at": "2025-02-27T16:33:27.051Z",
"metadata": {
"category": "movie_recommendations"
},
"user_id": "alice"
},
{
"id": "bf4d4092-cf91-4181-bfeb-b6fa2ed3061b",
"memory": "Likes to play cricket on weekends",
"hash": "285d07801ae42054732314853e9eadd7",
"metadata": {"category": "hobbies"},
"created_at": "2024-10-28T12:32:07.744891-07:00",
"updated_at": None,
"user_id": "alice"
"id": "475bde34-21e6-42ab-8bef-0ab84474f156",
"memory": "User loves sci-fi movies.",
"hash": "285d07801ae42054732314853e9eadd7",
"created_at": "2025-02-27T16:33:20.560Z",
"updated_at": None,
"metadata": {
"category": "movie_recommendations"
},
"user_id": "alice"
},
{
"id": "cbb1fe73-0bf1-4067-8c1f-63aa53e7b1a4",
"memory": "User is not a big fan of thriller movies.",
"hash": "285d07801ae42054732314853e9eadd7",
"created_at": "2025-02-27T16:33:20.560Z",
"updated_at": None,
"metadata": {
"category": "movie_recommendations"
},
"user_id": "alice"
}
]
}
@@ -135,18 +182,20 @@ all_memories = m.get_all(user_id="alice")
<CodeGroup>
```python Code
# Get a single memory by ID
specific_memory = m.get("bf4d4092-cf91-4181-bfeb-b6fa2ed3061b")
specific_memory = m.get("892db2ae-06d9-49e5-8b3e-585ef9b85b8e")
```
```json Output
{
"id": "bf4d4092-cf91-4181-bfeb-b6fa2ed3061b",
"memory": "Likes to play cricket on weekends",
"hash": "285d07801ae42054732314853e9eadd7",
"metadata": {"category": "hobbies"},
"created_at": "2024-10-28T12:32:07.744891-07:00",
"updated_at": None,
"user_id": "alice"
"id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"memory": "User is planning to watch a movie tonight.",
"hash": "1a271c007316c94377175ee80e746a19",
"created_at": "2025-02-27T16:33:20.557Z",
"updated_at": None,
"metadata": {
"category": "movie_recommendations"
},
"user_id": "alice"
}
```
</CodeGroup>
@@ -155,23 +204,49 @@ specific_memory = m.get("bf4d4092-cf91-4181-bfeb-b6fa2ed3061b")
<CodeGroup>
```python Code
related_memories = m.search(query="What are Alice's hobbies?", user_id="alice")
related_memories = m.search(query="What do you know about me?", user_id="alice")
```
```json Output
{
"results": [
{
"id": "bf4d4092-cf91-4181-bfeb-b6fa2ed3061b",
"memory": "Likes to play cricket on weekends",
"hash": "285d07801ae42054732314853e9eadd7",
"metadata": {"category": "hobbies"},
"score": 0.30808347,
"created_at": "2024-10-28T12:32:07.744891-07:00",
"updated_at": None,
"user_id": "alice"
}
]
"results": [
{
"id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"memory": "User is planning to watch a movie tonight.",
"hash": "1a271c007316c94377175ee80e746a19",
"created_at": "2025-02-27T16:33:20.557Z",
"updated_at": None,
"score": 0.38920719231944799,
"metadata": {
"category": "movie_recommendations"
},
"user_id": "alice"
},
{
"id": "475bde34-21e6-42ab-8bef-0ab84474f156",
"memory": "User loves sci-fi movies.",
"hash": "285d07801ae42054732314853e9eadd7",
"created_at": "2025-02-27T16:33:20.560Z",
"updated_at": None,
"score": 0.36869761478135689,
"metadata": {
"category": "movie_recommendations"
},
"user_id": "alice"
},
{
"id": "cbb1fe73-0bf1-4067-8c1f-63aa53e7b1a4",
"memory": "User is not a big fan of thriller movies.",
"hash": "285d07801ae42054732314853e9eadd7",
"created_at": "2025-02-27T16:33:20.560Z",
"updated_at": None,
"score": 0.33855272141248272,
"metadata": {
"category": "movie_recommendations"
},
"user_id": "alice"
}
]
}
```
</CodeGroup>
@@ -180,7 +255,7 @@ related_memories = m.search(query="What are Alice's hobbies?", user_id="alice")
<CodeGroup>
```python Code
result = m.update(memory_id="bf4d4092-cf91-4181-bfeb-b6fa2ed3061b", data="Likes to play tennis on weekends")
result = m.update(memory_id="892db2ae-06d9-49e5-8b3e-585ef9b85b8e", data="I love India, it is my favorite country.")
```
```json Output
@@ -192,29 +267,31 @@ result = m.update(memory_id="bf4d4092-cf91-4181-bfeb-b6fa2ed3061b", data="Likes
<CodeGroup>
```python Code
history = m.history(memory_id="bf4d4092-cf91-4181-bfeb-b6fa2ed3061b")
history = m.history(memory_id="892db2ae-06d9-49e5-8b3e-585ef9b85b8e")
```
```json Output
[
{
"id": "96d2821d-e551-4089-aa57-9398c421d450",
"memory_id": "bf4d4092-cf91-4181-bfeb-b6fa2ed3061b",
"old_memory": None,
"new_memory": "Likes to play cricket on weekends",
"event": "ADD",
"created_at": "2024-10-28T12:32:07.744891-07:00",
"updated_at": None
},
{
"id": "3db4cb58-c0f1-4dd0-b62a-8123068ebfe7",
"memory_id": "bf4d4092-cf91-4181-bfeb-b6fa2ed3061b",
"old_memory": "Likes to play cricket on weekends",
"new_memory": "Likes to play tennis on weekends",
"event": "UPDATE",
"created_at": "2024-10-28T12:32:07.744891-07:00",
"updated_at": "2024-10-28T13:05:46.987978-07:00"
}
{
"id": 39,
"memory_id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"previous_value": "User is planning to watch a movie tonight.",
"new_value": "I love India, it is my favorite country.",
"action": "UPDATE",
"created_at": "2025-02-27T16:33:20.557Z",
"updated_at": "2025-02-27T16:33:27.051Z",
"is_deleted": 0
},
{
"id": 37,
"memory_id": "892db2ae-06d9-49e5-8b3e-585ef9b85b8e",
"previous_value": null,
"new_value": "User is planning to watch a movie tonight.",
"action": "ADD",
"created_at": "2025-02-27T16:33:20.557Z",
"updated_at": null,
"is_deleted": 0
}
]
```
</CodeGroup>
@@ -223,7 +300,7 @@ history = m.history(memory_id="bf4d4092-cf91-4181-bfeb-b6fa2ed3061b")
```python
# Delete a memory by id
m.delete(memory_id="bf4d4092-cf91-4181-bfeb-b6fa2ed3061b")
m.delete(memory_id="892db2ae-06d9-49e5-8b3e-585ef9b85b8e")
# Delete all memories for a user
m.delete_all(user_id="alice")
```
+1 -1
View File
@@ -4743,7 +4743,7 @@
"nullable": true
},
"metadata": {
"description": "Additional metadata associated with the memory, which can be used to store any additional information or context about the memory.",
"description": "Additional metadata associated with the memory, which can be used to store any additional information or context about the memory. Best practice for incorporating additional information is through metadata (e.g. location, time, ids, etc.). During retrieval, you can either use these metadata alongside the query to fetch relevant memories or retrieve memories based on the query first and then refine the results using metadata during post-processing.",
"title": "Metadata",
"type": "object",
"properties": {
+83 -37
View File
@@ -173,6 +173,8 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \
Messages passed along with `user_id`, `run_id`, or `app_id` are stored as user memories, while messages from the assistant are excluded from memory. To store messages for the assistant, use `agent_id` exclusively and avoid including other IDs, such as user_id, alongside it. This ensures the memory is properly attributed to the assistant.
</Note>
<Note>Metadata allows you to store structured information (location, timestamp, user state) with memories. Add it during creation to enable precise filtering and retrieval during searches.</Note>
#### Short-term memory for a user session
@@ -352,6 +354,64 @@ curl -X POST "https://api.mem0.ai/v1/memories/" \
The `agent_id` retains memories exclusively based on messages generated by the assistant or those explicitly provided as input to the assistant. Messages outside these criteria are not stored as memory.
</Note>
#### Long-term memory for both users and agents
When you provide both `user_id` and `agent_id`, Mem0 will store memories with both identifiers attached:
- Each memory will be tagged with both the specified `user_id` and `agent_id`
- During retrieval, you'll need to provide both IDs to access the memories
- This enables tracking the full context of conversations between specific users and agents
<CodeGroup>
```python Python
messages = [
{"role": "user", "content": "I'm travelling to San Francisco"},
{"role": "assistant", "content": "That's great! I'm going to Dubai next month."},
]
client.add(messages=messages, user_id="user1", agent_id="agent1")
```
```javascript JavaScript
const messages = [
{"role": "user", "content": "I'm travelling to San Francisco"},
{"role": "assistant", "content": "That's great! I'm going to Dubai next month."},
]
client.add(messages, { user_id: "user1", agent_id: "agent1" })
.then(response => console.log(response))
.catch(error => console.error(error));
```
```bash cURL
curl -X POST "https://api.mem0.ai/v1/memories/" \
-H "Authorization: Token your-api-key" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "I'm travelling to San Francisco"},
{"role": "assistant", "content": "That's great! I'm going to Dubai next month."},
],
"user_id": "user1",
"agent_id": "agent1"
}'
```
```json Output
[
{
'id': 'c57abfa2-f0ac-48af-896a-21728dbcecee0',
'data': {'memory': 'Travelling to San Francisco'},
'event': 'ADD'
},
{ 'id': '0e8c003f-7db7-426a-9fdc-a46f9331a0c2',
'data': {'memory': 'Going to Dubai next month'},
'event': 'ADD'
}
]
```
</CodeGroup>
#### Monitor Memories
@@ -431,6 +491,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/" \
</CodeGroup>
Use category and metadata filters:
<CodeGroup>
```python Python
@@ -1230,11 +1291,6 @@ const filters = {
"categories":{
"contains": "food_preferences"
}
},
{
"keywords":{
"contains": "to play"
}
}
]
};
@@ -1258,20 +1314,14 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2" \
-d '{
"filters": {
"AND": [
{
"user_id": "alex"
},
{
"created_at": {
"gte": "2024-07-01",
"lte": "2024-07-31"
}
},
{
"categories":{
"contains": "food_preferences"
}
}
{"user_id":"alex"},
{"created_at":{
"gte":"2024-07-01",
"lte":"2024-07-31"
}},
{"categories":{
"contains": "food_preferences"
}}
]
}
}'
@@ -1283,15 +1333,14 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
-d '{
"filters": {
"AND": [
{
"user_id": "alex"
},
{
"created_at": {
"gte": "2024-07-01",
"lte": "2024-07-31"
}
}
{"user_id":"alex"},
{"created_at":{
"gte":"2024-07-01",
"lte":"2024-07-31"
}},
{"categories":{
"contains": "food_preferences"
}}
]
}
}'
@@ -1392,8 +1441,8 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2" \
"categories": {
"contains": "food_preferences"
}
}
]
}}
]
}
}'
@@ -1409,7 +1458,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
"categories": {
"contains": "food_preferences"
}
}
}}
]
}
}'
@@ -1767,12 +1816,10 @@ print(response)
```
```javascript JavaScript
const updateMemories = [
{
memoryId: "285ed74b-6e05-4043-b16b-3abd5b533496",
{"memory_id": "285ed74b-6e05-4043-b16b-3abd5b533496",
text: "Watches football"
},
{
memoryId: "2c9bd859-d1b7-4d33-a6b8-94e0147c4f07",
{"memory_id": "2c9bd859-d1b7-4d33-a6b8-94e0147c4f07",
text: "Loves to travel"
}
];
@@ -1817,8 +1864,7 @@ response = client.batch_delete(delete_memories)
print(response)
```
```javascript JavaScript
const deleteMemories = [
{"memory_id": "285ed74b-6e05-4043-b16b-3abd5b533496"},
const deleteMemories = [{"memory_id": "285ed74b-6e05-4043-b16b-3abd5b533496"},
{"memory_id": "2c9bd859-d1b7-4d33-a6b8-94e0147c4f07"}
];
+3 -3
View File
@@ -45,7 +45,7 @@ const useUserId = () => {
export const Assistant = () => {
const { userId, resetUserId } = useUserId();
const runtime = useChatRuntime({
api: "/api/chat",
api: "https://demo.mem0.ai/api/chat",
body: { userId },
});
@@ -92,8 +92,8 @@ export const Assistant = () => {
</header>
<div className="grid grid-cols-1 md:grid-cols-[260px_1fr] gap-x-0 h-[calc(100vh-8rem)] md:h-[calc(100vh-4rem)]">
<ThreadList onResetUserId={resetUserId} />
<Thread sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} onResetUserId={resetUserId} />
<ThreadList onResetUserId={resetUserId} isDarkMode={isDarkMode} />
<Thread sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} onResetUserId={resetUserId} isDarkMode={isDarkMode} />
</div>
</div>
</AssistantRuntimeProvider>
+2 -2
View File
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Mem0-Demo",
description: "Mem0-Demo: By Mem0",
title: "Mem0 - ChatGPT with Memory",
description: "Mem0 - ChatGPT with Memory is a personalized AI chat app powered by Mem0 that remembers your preferences, facts, and memories.",
};
export default function RootLayout({
@@ -1,5 +1,6 @@
"use client";
import darkAssistantUi from "@/images/assistant-ui-dark.svg";
import assistantUi from "@/images/assistant-ui.svg";
import React from "react";
import Image from "next/image";
@@ -27,7 +28,7 @@ export default function ThemeAwareLogo({
}
// For default variant, use the full logo image
const logoSrc = isDarkMode ? "/images/assistant-ui-dark.svg" : "/images/assistant-ui.svg";
const logoSrc = isDarkMode ? darkAssistantUi : assistantUi;
return (
<Image
@@ -19,55 +19,73 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import ThemeAwareLogo from "@/components/assistant-ui/theme-aware-logo";
import Link from "next/link";
interface ThreadListProps {
onResetUserId?: () => void;
isDarkMode: boolean;
}
export const ThreadList: FC<ThreadListProps> = ({ onResetUserId }) => {
export const ThreadList: FC<ThreadListProps> = ({ onResetUserId, isDarkMode }) => {
const [open, setOpen] = useState(false);
return (
<div className="flex-col h-full border-r border-[#e2e8f0] bg-white dark:bg-zinc-900 dark:border-zinc-800 p-3 overflow-y-auto hidden md:flex">
<ThreadListPrimitive.Root className="flex flex-col items-stretch gap-1.5">
<ThreadListNew />
<div className="mt-4 mb-2 flex justify-between items-center px-2.5">
<h2 className="text-sm font-medium text-[#475569] dark:text-zinc-300">Recent Chats</h2>
{onResetUserId && (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<TooltipIconButton
tooltip="Reset Memory"
className="hover:text-[#4f46e5] text-[#475569] dark:text-zinc-300 dark:hover:text-[#6366f1] size-4 p-0"
variant="ghost"
>
<RefreshCwIcon className="w-4 h-4" />
</TooltipIconButton>
</AlertDialogTrigger>
<AlertDialogContent className="bg-white dark:bg-zinc-900 border-[#e2e8f0] dark:border-zinc-800">
<AlertDialogHeader>
<AlertDialogTitle className="text-[#1e293b] dark:text-white">Reset Memory</AlertDialogTitle>
<AlertDialogDescription className="text-[#475569] dark:text-zinc-300">
This will permanently delete all your chat history and memories. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff] dark:hover:bg-zinc-800">Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
onResetUserId();
setOpen(false);
}}
className="bg-[#4f46e5] hover:bg-[#4338ca] dark:bg-[#6366f1] dark:hover:bg-[#4f46e5] text-white"
<ThreadListPrimitive.Root className="flex flex-col justify-between h-full items-stretch gap-1.5">
<div className="flex flex-col h-full items-stretch gap-1.5">
<ThreadListNew />
<div className="mt-4 mb-2 flex justify-between items-center px-2.5">
<h2 className="text-sm font-medium text-[#475569] dark:text-zinc-300">
Recent Chats
</h2>
{onResetUserId && (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<TooltipIconButton
tooltip="Reset Memory"
className="hover:text-[#4f46e5] text-[#475569] dark:text-zinc-300 dark:hover:text-[#6366f1] size-4 p-0"
variant="ghost"
>
Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<RefreshCwIcon className="w-4 h-4" />
</TooltipIconButton>
</AlertDialogTrigger>
<AlertDialogContent className="bg-white dark:bg-zinc-900 border-[#e2e8f0] dark:border-zinc-800">
<AlertDialogHeader>
<AlertDialogTitle className="text-[#1e293b] dark:text-white">
Reset Memory
</AlertDialogTitle>
<AlertDialogDescription className="text-[#475569] dark:text-zinc-300">
This will permanently delete all your chat history and
memories. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff] dark:hover:bg-zinc-800">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
onResetUserId();
setOpen(false);
}}
className="bg-[#4f46e5] hover:bg-[#4338ca] dark:bg-[#6366f1] dark:hover:bg-[#4f46e5] text-white"
>
Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
<ThreadListItems />
</div>
<div>
<Link href="https://www.assistant-ui.com/" target="_blank" className="flex justify-center items-center gap-2">
<h1 className="text-sm text-[#475569] dark:text-zinc-300 text-center">built using</h1>
<ThemeAwareLogo width={24} height={24} isDarkMode={isDarkMode} />
<p className="text-md font-bold dark:text-zinc-300">assistant-ui</p>
</Link>
</div>
<ThreadListItems />
</ThreadListPrimitive.Root>
</div>
);
@@ -1,4 +1,4 @@
"use client"
"use client";
import {
ActionBarPrimitive,
@@ -42,14 +42,22 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import Link from "next/link";
import ThemeAwareLogo from "./theme-aware-logo";
interface ThreadProps {
sidebarOpen: boolean;
setSidebarOpen: Dispatch<SetStateAction<boolean>>;
onResetUserId?: () => void;
isDarkMode: boolean;
}
export const Thread: FC<ThreadProps> = ({ sidebarOpen, setSidebarOpen, onResetUserId }) => {
export const Thread: FC<ThreadProps> = ({
sidebarOpen,
setSidebarOpen,
onResetUserId,
isDarkMode,
}) => {
const [resetDialogOpen, setResetDialogOpen] = useState(false);
return (
@@ -61,23 +69,28 @@ export const Thread: FC<ThreadProps> = ({ sidebarOpen, setSidebarOpen, onResetUs
>
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
<div
className="fixed inset-0 bg-black/40 z-30 md:hidden"
onClick={() => setSidebarOpen(false)}
></div>
)}
{/* Mobile sidebar drawer */}
<div className={cn(
"fixed inset-y-0 left-0 z-40 w-[85%] bg-white dark:bg-zinc-900 transform transition-transform duration-300 ease-in-out md:hidden",
sidebarOpen ? "translate-x-0" : "-translate-x-full"
)}>
<div
className={cn(
"fixed inset-y-0 left-0 z-40 w-[85%] bg-white dark:bg-zinc-900 transform transition-transform duration-300 ease-in-out md:hidden",
sidebarOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<div className="h-full flex flex-col">
<div className="flex items-center justify-between border-b dark:text-white border-[#e2e8f0] dark:border-zinc-800 p-4">
<h2 className="font-medium">Recent Chats</h2>
<div className="flex items-center gap-2">
{onResetUserId && (
<AlertDialog open={resetDialogOpen} onOpenChange={setResetDialogOpen}>
<AlertDialog
open={resetDialogOpen}
onOpenChange={setResetDialogOpen}
>
<AlertDialogTrigger asChild>
<TooltipIconButton
tooltip="Reset Memory"
@@ -89,13 +102,18 @@ export const Thread: FC<ThreadProps> = ({ sidebarOpen, setSidebarOpen, onResetUs
</AlertDialogTrigger>
<AlertDialogContent className="bg-white dark:bg-zinc-900 border-[#e2e8f0] dark:border-zinc-800">
<AlertDialogHeader>
<AlertDialogTitle className="text-[#1e293b] dark:text-white">Reset Memory</AlertDialogTitle>
<AlertDialogTitle className="text-[#1e293b] dark:text-white">
Reset Memory
</AlertDialogTitle>
<AlertDialogDescription className="text-[#475569] dark:text-zinc-300">
This will permanently delete all your chat history and memories. This action cannot be undone.
This will permanently delete all your chat history and
memories. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff] dark:hover:bg-zinc-800">Cancel</AlertDialogCancel>
<AlertDialogCancel className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff] dark:hover:bg-zinc-800">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
onResetUserId();
@@ -109,9 +127,9 @@ export const Thread: FC<ThreadProps> = ({ sidebarOpen, setSidebarOpen, onResetUs
</AlertDialogContent>
</AlertDialog>
)}
<Button
variant="ghost"
size="sm"
<Button
variant="ghost"
size="sm"
onClick={() => setSidebarOpen(false)}
className="text-[#475569] dark:text-zinc-300 hover:bg-[#eef2ff] dark:hover:bg-zinc-800 h-8 w-8 p-0"
>
@@ -120,21 +138,40 @@ export const Thread: FC<ThreadProps> = ({ sidebarOpen, setSidebarOpen, onResetUs
</div>
</div>
<div className="flex-1 overflow-y-auto p-3">
<ThreadListPrimitive.Root className="flex flex-col items-stretch gap-1.5 h-full dark:text-white">
<ThreadListPrimitive.New asChild>
<Button
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 dark:data-[active]:bg-zinc-800 flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start bg-[#4f46e5] text-white dark:bg-[#6366f1]"
variant="default"
<div className="flex flex-col justify-between items-stretch gap-1.5 h-full dark:text-white">
<ThreadListPrimitive.Root className="flex flex-col items-stretch gap-1.5 h-full dark:text-white">
<ThreadListPrimitive.New asChild>
<Button
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 dark:data-[active]:bg-zinc-800 flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start bg-[#4f46e5] text-white dark:bg-[#6366f1]"
variant="default"
>
<PlusIcon className="w-4 h-4" />
New Thread
</Button>
</ThreadListPrimitive.New>
<div className="mt-4 mb-2">
<h2 className="text-sm font-medium text-[#475569] dark:text-zinc-300 px-2.5">
Recent Chats
</h2>
</div>
<ThreadListPrimitive.Items components={{ ThreadListItem }} />
</ThreadListPrimitive.Root>
<div>
<Link
href="https://www.assistant-ui.com/"
target="_blank"
className="flex justify-center items-center gap-2"
>
<PlusIcon className="w-4 h-4" />
New Thread
</Button>
</ThreadListPrimitive.New>
<div className="mt-4 mb-2">
<h2 className="text-sm font-medium text-[#475569] dark:text-zinc-300 px-2.5">Recent Chats</h2>
<h1 className="text-sm text-[#475569] dark:text-zinc-300 text-center">
built using
</h1>
<ThemeAwareLogo width={24} height={24} isDarkMode={isDarkMode} />
<p className="text-md font-bold dark:text-zinc-300">
assistant-ui
</p>
</Link>
</div>
<ThreadListPrimitive.Items components={{ ThreadListItem }} />
</ThreadListPrimitive.Root>
</div>
</div>
</div>
</div>
@@ -185,19 +222,20 @@ const ThreadWelcome: FC = () => {
<div className="flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col">
<div className="flex w-full flex-grow flex-col items-center justify-start h-[calc(100vh-23rem)] md:h-[calc(100vh-18rem)]">
<div className="flex flex-col items-center justify-center h-full">
<div className="text-5xl font-bold text-[#1e293b] dark:text-white mb-2">
Mem0 Demo
<div className="text-2xl md:text-4xl font-bold text-[#1e293b] dark:text-white mb-2">
Mem0 - ChatGPT with memory
</div>
<p className="text-center text-sm text-[#1e293b] dark:text-white mb-2 w-3/4">
A personalized AI chat app powered by Mem0 that remembers your preferences, facts, and memories.
A personalized AI chat app powered by Mem0 that remembers your
preferences, facts, and memories.
</p>
</div>
</div>
<div className="flex flex-col items-center justify-center">
<p className="mt-4 font-medium text-[#1e293b] dark:text-white">
<p className="mt-4 font-medium text-[#1e293b] dark:text-white">
How can I help you today?
</p>
<ThreadWelcomeSuggestions />
<ThreadWelcomeSuggestions />
</div>
</div>
</ThreadPrimitive.Empty>
@@ -212,21 +250,27 @@ const ThreadWelcomeSuggestions: FC = () => {
prompt="I like to travel to "
method="replace"
>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">Travel</span>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">
Travel
</span>
</ThreadPrimitive.Suggestion>
<ThreadPrimitive.Suggestion
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
prompt="I like to eat "
method="replace"
>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">Food</span>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">
Food
</span>
</ThreadPrimitive.Suggestion>
<ThreadPrimitive.Suggestion
className="hover:bg-[#eef2ff] dark:hover:bg-zinc-800 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-[2rem] border border-[#e2e8f0] dark:border-zinc-700 p-3 transition-colors ease-in"
prompt="I am working on "
method="replace"
>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">Project details</span>
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">
Project details
</span>
</ThreadPrimitive.Suggestion>
</div>
);
@@ -335,22 +379,22 @@ const EditComposer: FC = () => {
const AssistantMessage: FC = () => {
const content = useMessage((m) => m.content);
const markdownText = React.useMemo(() => {
if (!content) return '';
if (typeof content === 'string') return content;
if (Array.isArray(content) && content.length > 0 && 'text' in content[0]) {
return content[0].text || '';
if (!content) return "";
if (typeof content === "string") return content;
if (Array.isArray(content) && content.length > 0 && "text" in content[0]) {
return content[0].text || "";
}
return '';
return "";
}, [content]);
return (
<MessagePrimitive.Root className="grid grid-cols-[auto_auto_1fr] grid-rows-[auto_1fr] relative w-full max-w-[var(--thread-max-width)] py-4">
<div className="text-[#1e293b] dark:text-zinc-200 max-w-[calc(var(--thread-max-width)*0.8)] break-words leading-7 col-span-2 col-start-2 row-start-1 my-1.5 bg-white dark:bg-zinc-800 rounded-3xl px-5 py-2.5 border border-[#e2e8f0] dark:border-zinc-700 shadow-sm">
<MemoryUI />
<MarkdownRenderer
<MarkdownRenderer
markdownText={markdownText}
showCopyButton={true}
isDarkMode={document.documentElement.classList.contains('dark')}
isDarkMode={document.documentElement.classList.contains("dark")}
/>
</div>
@@ -400,7 +444,10 @@ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
return (
<BranchPickerPrimitive.Root
hideWhenSingleBranch
className={cn("text-[#475569] dark:text-zinc-300 inline-flex items-center text-xs", className)}
className={cn(
"text-[#475569] dark:text-zinc-300 inline-flex items-center text-xs",
className
)}
{...rest}
>
<BranchPickerPrimitive.Previous asChild>
@@ -428,7 +475,13 @@ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
const CircleStopIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width="16"
height="16"
>
<rect width="10" height="10" x="3" y="3" rx="2" />
</svg>
);
@@ -1,5 +1,7 @@
"use client";
import darkLogo from "@/images/dark.svg";
import lightLogo from "@/images/light.svg";
import React from "react";
import Image from "next/image";
@@ -27,7 +29,7 @@ export default function ThemeAwareLogo({
}
// For default variant, use the full logo image
const logoSrc = isDarkMode ? "/images/dark.svg" : "/images/light.svg";
const logoSrc = isDarkMode ? darkLogo : lightLogo;
return (
<Image
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-messages-square">
<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2z">
</path>
<path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1">
</path>
</svg>

After

Width:  |  Height:  |  Size: 371 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-messages-square">
<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2z">
</path>
<path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1">
</path>
</svg>

After

Width:  |  Height:  |  Size: 378 B

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+18
View File
@@ -2,6 +2,24 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
assetPrefix: "https://demo.mem0.ai",
images: {
path: "https://demo.mem0.ai",
},
async headers() {
return [
{
// matching all API routes
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" },
{ key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT" },
{ key: "Access-Control-Allow-Headers", value: "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" },
]
}
]
}
};
export default nextConfig;
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+29
View File
@@ -0,0 +1,29 @@
**/.env
**/node_modules
**/dist
**/.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/libs/utils",
"ui": "@/components/ui",
"lib": "@/libs",
"hooks": "@/hooks"
}
}
+28
View File
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/mem0_logo.jpeg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JustChat | Chat with AI</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -24,9 +24,11 @@
"clsx": "^2.1.1",
"framer-motion": "^11.11.11",
"lucide-react": "^0.454.0",
"openai": "^4.86.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"mem0ai": "2.1.2",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
@@ -46,6 +48,7 @@
"tailwindcss": "^3.4.14",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10"
}
}
"vite": "^6.2.1"
},
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b"
}
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

+13
View File
@@ -0,0 +1,13 @@
import Home from "./page"
function App() {
return (
<>
<Home />
</>
)
}
export default App
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,91 @@
import { Dispatch, SetStateAction, useContext, useEffect, useState } from 'react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import GlobalContext from '@/contexts/GlobalContext'
import { Provider } from '@/constants/messages'
export default function ApiSettingsPopup(props: { isOpen: boolean, setIsOpen: Dispatch<SetStateAction<boolean>> }) {
const {isOpen, setIsOpen} = props
const [mem0ApiKey, setMem0ApiKey] = useState('')
const [providerApiKey, setProviderApiKey] = useState('')
const [provider, setProvider] = useState('OpenAI')
const { selectorHandler, selectedOpenAIKey, selectedMem0Key, selectedProvider } = useContext(GlobalContext);
const handleSave = () => {
// Here you would typically save the settings to your backend or local storage
selectorHandler(mem0ApiKey, providerApiKey, provider as Provider);
setIsOpen(false)
}
useEffect(() => {
if (selectedOpenAIKey) {
setProviderApiKey(selectedOpenAIKey);
}
if (selectedMem0Key) {
setMem0ApiKey(selectedMem0Key);
}
if (selectedProvider) {
setProvider(selectedProvider);
}
}, [selectedOpenAIKey, selectedMem0Key, selectedProvider]);
return (
<>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>API Configuration Settings</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="mem0-api-key" className="text-right">
Mem0 API Key
</Label>
<Input
id="mem0-api-key"
value={mem0ApiKey}
onChange={(e) => setMem0ApiKey(e.target.value)}
className="col-span-3 rounded-3xl"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="provider-api-key" className="text-right">
Provider API Key
</Label>
<Input
id="provider-api-key"
value={providerApiKey}
onChange={(e) => setProviderApiKey(e.target.value)}
className="col-span-3 rounded-3xl"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="provider" className="text-right">
Provider
</Label>
<Select value={provider} onValueChange={setProvider}>
<SelectTrigger className="col-span-3 rounded-3xl">
<SelectValue placeholder="Select provider" />
</SelectTrigger>
<SelectContent className='rounded-3xl'>
<SelectItem value="openai" className='rounded-3xl'>OpenAI</SelectItem>
<SelectItem value="anthropic" className='rounded-3xl'>Anthropic</SelectItem>
<SelectItem value="cohere" className='rounded-3xl'>Cohere</SelectItem>
<SelectItem value="groq" className='rounded-3xl'>Groq</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button className='rounded-3xl' variant="outline" onClick={() => setIsOpen(false)}>Cancel</Button>
<Button className='rounded-3xl' onClick={handleSave}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
@@ -0,0 +1,35 @@
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import React from "react";
const ChevronToggle = (props: {
isMemoriesExpanded: boolean;
setIsMemoriesExpanded: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const { isMemoriesExpanded, setIsMemoriesExpanded } = props;
return (
<>
<div className="relaive">
<div className="flex items-center absolute top-1/2 z-10">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 border-y border rounded-lg relative right-10"
onClick={() => setIsMemoriesExpanded(!isMemoriesExpanded)}
aria-label={
isMemoriesExpanded ? "Collapse memories" : "Expand memories"
}
>
{isMemoriesExpanded ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</div>
</div>
</>
);
};
export default ChevronToggle;
@@ -0,0 +1,81 @@
import { Button } from "@/components/ui/button";
import { ChevronRight, X, RefreshCcw, Settings } from "lucide-react";
import { Dispatch, SetStateAction, useContext, useEffect, useState } from "react";
import GlobalContext from "../contexts/GlobalContext";
import { Input } from "./ui/input";
const Header = (props: {
setIsSettingsOpen: Dispatch<SetStateAction<boolean>>;
}) => {
const { setIsSettingsOpen } = props;
const { selectUserHandler, clearUserHandler, selectedUser, clearConfiguration } = useContext(GlobalContext);
const [userId, setUserId] = useState<string>("");
const handleSelectUser = (e: React.ChangeEvent<HTMLInputElement>) => {
setUserId(e.target.value);
};
const handleClearUser = () => {
clearUserHandler();
setUserId("");
};
const handleSubmit = () => {
selectUserHandler(userId);
};
// New function to handle key down events
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault(); // Prevent form submission if it's in a form
handleSubmit();
}
};
useEffect(() => {
if (selectedUser) {
setUserId(selectedUser);
}
}, [selectedUser]);
return (
<>
<header className="border-b p-4 flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-xl font-semibold">Mem0 Assistant</span>
</div>
<div className="flex items-center space-x-2 text-sm">
<div className="flex">
<Input
placeholder="UserId"
className="w-full rounded-3xl pr-6 pl-4"
value={userId}
onChange={handleSelectUser}
onKeyDown={handleKeyDown} // Attach the key down handler here
/>
<Button variant="ghost" size="icon" onClick={handleClearUser} className="relative hover:bg-transparent hover:text-neutral-400 right-8">
<X className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={handleSubmit} className="relative right-6">
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center space-x-2">
<Button variant="ghost" size="icon" onClick={clearConfiguration}>
<RefreshCcw className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setIsSettingsOpen(true)}
>
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
</header>
</>
);
};
export default Header;
@@ -0,0 +1,107 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import GlobalContext from "@/contexts/GlobalContext";
import { FileInfo } from "@/types";
import { Images, Send, X } from "lucide-react";
import { useContext, useRef, useState } from "react";
const InputArea = () => {
const [inputValue, setInputValue] = useState("");
const { handleSend, selectedFile, setSelectedFile, setFile } = useContext(GlobalContext);
const [loading, setLoading] = useState(false);
const ref = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
setSelectedFile({
name: file.name,
type: file.type,
size: file.size
})
setFile(file)
}
}
const handleSendController = async () => {
setLoading(true);
setInputValue("");
await handleSend(inputValue);
setLoading(false);
// focus on input
setTimeout(() => {
ref.current?.focus();
}, 0);
};
const handleClosePopup = () => {
setSelectedFile(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
return (
<>
<div className="border-t p-4">
<div className="flex items-center space-x-2">
<div className="relative bottom-3 left-5">
<div className="absolute">
<Input
type="file"
accept="image/*"
onChange={handleFileChange}
ref={fileInputRef}
className="sr-only"
id="file-upload"
/>
<label
htmlFor="file-upload"
className="flex items-center justify-center w-6 h-6 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 cursor-pointer"
>
<Images className="h-4 w-4" />
</label>
{selectedFile && <FileInfoPopup file={selectedFile} onClose={handleClosePopup} />}
</div>
</div>
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSendController()}
placeholder="Type a message..."
className="flex-1 pl-10 rounded-3xl"
disabled={loading}
ref={ref}
/>
<div className="relative right-14 bottom-5 flex">
<Button className="absolute rounded-full w-10 h-10 bg-transparent hover:bg-transparent cursor-pointer z-20 text-primary" onClick={handleSendController} disabled={!inputValue.trim() || loading}>
<Send className="h-8 w-8" size={50} />
</Button>
</div>
</div>
</div>
</>
);
};
const FileInfoPopup = ({ file, onClose }: { file: FileInfo, onClose: () => void }) => {
return (
<div className="relative bottom-36">
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 p-2 rounded-md shadow-md border border-gray-200 dark:border-gray-700 z-10 w-48">
<div className="flex justify-between items-center">
<h3 className="font-semibold text-sm truncate">{file.name}</h3>
<Button variant="ghost" size="sm" onClick={onClose} className="h-5 w-5 p-0">
<X className="h-3 w-3" />
</Button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">Type: {file.type}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">Size: {(file.size / 1024).toFixed(2)} KB</p>
</div>
</div>
)
}
export default InputArea;
@@ -0,0 +1,84 @@
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { ScrollArea } from "@radix-ui/react-scroll-area";
import { Memory } from "../types";
import GlobalContext from "@/contexts/GlobalContext";
import { useContext } from "react";
import { motion } from "framer-motion";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const MemoryItem = ({ memory }: { memory: Memory; index: number }) => {
return (
<motion.div
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
key={memory.id}
className="space-y-2"
>
<div className="flex items-start justify-between">
<p className="text-sm font-medium">{memory.content}</p>
</div>
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
<span>{new Date(memory.timestamp).toLocaleString()}</span>
</div>
<div className="flex flex-wrap gap-1">
{memory.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
</motion.div>
);
};
const Memories = (props: { isMemoriesExpanded: boolean }) => {
const { isMemoriesExpanded } = props;
const { memories } = useContext(GlobalContext);
return (
<Card
className={`border-l rounded-none flex flex-col transition-all duration-300 ${
isMemoriesExpanded ? "w-80" : "w-0 overflow-hidden"
}`}
>
<div className="px-4 py-[22px] border-b">
<span className="font-semibold">
Relevant Memories ({memories.length})
</span>
</div>
{memories.length === 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="p-4 text-center"
>
<span className="font-semibold">No relevant memories found.</span>
<br />
Only the relevant memories will be displayed here.
</motion.div>
)}
<ScrollArea className="flex-1 p-4">
<motion.div
className="space-y-4"
>
{/* <AnimatePresence mode="popLayout"> */}
{memories.map((memory: Memory, index: number) => (
<MemoryItem
key={memory.id}
memory={memory}
index={index}
/>
))}
{/* </AnimatePresence> */}
</motion.div>
</ScrollArea>
</Card>
);
};
export default Memories;
@@ -0,0 +1,102 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Message } from "../types";
import { useContext, useEffect, useRef } from "react";
import GlobalContext from "@/contexts/GlobalContext";
import Markdown from "react-markdown";
import Mem00Logo from "../assets/mem0_logo.jpeg";
import UserLogo from "../assets/user.jpg";
const Messages = () => {
const { messages, thinking } = useContext(GlobalContext);
const scrollAreaRef = useRef<HTMLDivElement>(null);
// scroll to bottom
useEffect(() => {
if (scrollAreaRef.current) {
scrollAreaRef.current.scrollTop += 40; // Scroll down by 40 pixels
}
}, [messages, thinking]);
return (
<>
<ScrollArea ref={scrollAreaRef} className="flex-1 p-4 pr-10">
<div className="space-y-4">
{messages.map((message: Message) => (
<div
key={message.id}
className={`flex ${
message.sender === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`flex items-start space-x-2 max-w-[80%] ${
message.sender === "user"
? "flex-row-reverse space-x-reverse"
: "flex-row"
}`}
>
<div className="h-full flex flex-col items-center justify-end">
<Avatar className="h-8 w-8">
<AvatarImage
src={
message.sender === "assistant" ? Mem00Logo : UserLogo
}
/>
<AvatarFallback>
{message.sender === "assistant" ? "AI" : "U"}
</AvatarFallback>
</Avatar>
</div>
<div
className={`rounded-xl px-3 py-2 ${
message.sender === "user"
? "bg-blue-500 text-white rounded-br-none"
: "bg-muted text-muted-foreground rounded-bl-none"
}`}
>
{message.image && (
<div className="w-44 flex items-center justify-center overflow-hidden rounded-lg">
<img
src={message.image}
alt="Message attachment"
className="my-2 rounded-lg max-w-full h-auto w-44 mx-auto"
/>
</div>
)}
<Markdown>{message.content}</Markdown>
<span className="text-xs opacity-50 mt-1 block text-end relative bottom-1 -mb-2">
{message.timestamp}
</span>
</div>
</div>
</div>
))}
{thinking && (
<div className={`flex justify-start`}>
<div
className={`flex items-start space-x-2 max-w-[80%] flex-row`}
>
<Avatar className="h-8 w-8">
<AvatarImage src={Mem00Logo} />
<AvatarFallback>{"AI"}</AvatarFallback>
</Avatar>
<div
className={`rounded-lg p-3 bg-muted text-muted-foreground`}
>
<div className="loader">
<div className="ball"></div>
<div className="ball"></div>
<div className="ball"></div>
</div>
</div>
</div>
</div>
)}
</div>
</ScrollArea>
</>
);
};
export default Messages;
@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/libs/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/libs/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/libs/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/libs/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/libs/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/libs/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
@@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/libs/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
@@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/libs/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
@@ -0,0 +1,164 @@
"use client"
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "@/libs/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
@@ -0,0 +1,31 @@
import { Message } from "@/types";
export const WELCOME_MESSAGE: Message = {
id: "1",
content: "👋 Hi there! I'm your personal assistant. How can I help you today? 😊",
sender: "assistant",
timestamp: new Date().toLocaleTimeString(),
};
export const INVALID_CONFIG_MESSAGE: Message = {
id: "2",
content: "Invalid configuration. Please check your API keys, and add a user and try again.",
sender: "assistant",
timestamp: new Date().toLocaleTimeString(),
};
export const ERROR_MESSAGE: Message = {
id: "3",
content: "Something went wrong. Please try again.",
sender: "assistant",
timestamp: new Date().toLocaleTimeString(),
};
export const AI_MODELS = {
openai: "gpt-4o",
anthropic: "claude-3-haiku-20240307",
cohere: "command-r-plus",
groq: "gemma2-9b-it",
} as const;
export type Provider = keyof typeof AI_MODELS;
@@ -0,0 +1,110 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createContext } from 'react';
import { Message, Memory, FileInfo } from '@/types';
import { useAuth } from '@/hooks/useAuth';
import { useChat } from '@/hooks/useChat';
import { useFileHandler } from '@/hooks/useFileHandler';
import { Provider } from '@/constants/messages';
interface GlobalContextType {
selectedUser: string;
selectUserHandler: (user: string) => void;
clearUserHandler: () => void;
messages: Message[];
memories: Memory[];
handleSend: (content: string) => Promise<void>;
thinking: boolean;
selectedMem0Key: string;
selectedOpenAIKey: string;
selectedProvider: Provider;
selectorHandler: (mem0: string, openai: string, provider: Provider) => void;
clearConfiguration: () => void;
selectedFile: FileInfo | null;
setSelectedFile: (file: FileInfo | null) => void;
file: File | null;
setFile: (file: File | null) => void;
}
const GlobalContext = createContext<GlobalContextType>({} as GlobalContextType);
const GlobalState = (props: { children: React.ReactNode }) => {
const {
mem0ApiKey: selectedMem0Key,
openaiApiKey: selectedOpenAIKey,
provider: selectedProvider,
user: selectedUser,
setAuth: selectorHandler,
setUser: selectUserHandler,
clearAuth: clearConfiguration,
clearUser: clearUserHandler,
} = useAuth();
const {
selectedFile,
file,
fileData,
setSelectedFile,
handleFile,
clearFile,
} = useFileHandler();
const {
messages,
memories,
thinking,
sendMessage,
} = useChat({
user: selectedUser,
mem0ApiKey: selectedMem0Key,
openaiApiKey: selectedOpenAIKey,
provider: selectedProvider,
});
const handleSend = async (content: string) => {
if (file) {
await sendMessage(content, {
type: file.type,
data: fileData!,
});
clearFile();
} else {
await sendMessage(content);
}
};
const setFile = async (newFile: File | null) => {
if (newFile) {
await handleFile(newFile);
} else {
clearFile();
}
};
return (
<GlobalContext.Provider
value={{
selectedUser,
selectUserHandler,
clearUserHandler,
messages,
memories,
handleSend,
thinking,
selectedMem0Key,
selectedOpenAIKey,
selectedProvider,
selectorHandler,
clearConfiguration,
selectedFile,
setSelectedFile,
file,
setFile,
}}
>
{props.children}
</GlobalContext.Provider>
);
};
export default GlobalContext;
export { GlobalState };
@@ -0,0 +1,73 @@
import { useState, useEffect } from 'react';
import { Provider } from '@/constants/messages';
interface UseAuthReturn {
mem0ApiKey: string;
openaiApiKey: string;
provider: Provider;
user: string;
setAuth: (mem0: string, openai: string, provider: Provider) => void;
setUser: (user: string) => void;
clearAuth: () => void;
clearUser: () => void;
}
export const useAuth = (): UseAuthReturn => {
const [mem0ApiKey, setMem0ApiKey] = useState<string>('');
const [openaiApiKey, setOpenaiApiKey] = useState<string>('');
const [provider, setProvider] = useState<Provider>('openai');
const [user, setUser] = useState<string>('');
useEffect(() => {
const mem0 = localStorage.getItem('mem0ApiKey');
const openai = localStorage.getItem('openaiApiKey');
const savedProvider = localStorage.getItem('provider') as Provider;
const savedUser = localStorage.getItem('user');
if (mem0 && openai && savedProvider) {
setAuth(mem0, openai, savedProvider);
}
if (savedUser) {
setUser(savedUser);
}
}, []);
const setAuth = (mem0: string, openai: string, provider: Provider) => {
setMem0ApiKey(mem0);
setOpenaiApiKey(openai);
setProvider(provider);
localStorage.setItem('mem0ApiKey', mem0);
localStorage.setItem('openaiApiKey', openai);
localStorage.setItem('provider', provider);
};
const clearAuth = () => {
localStorage.removeItem('mem0ApiKey');
localStorage.removeItem('openaiApiKey');
localStorage.removeItem('provider');
setMem0ApiKey('');
setOpenaiApiKey('');
setProvider('openai');
};
const updateUser = (user: string) => {
setUser(user);
localStorage.setItem('user', user);
};
const clearUser = () => {
localStorage.removeItem('user');
setUser('');
};
return {
mem0ApiKey,
openaiApiKey,
provider,
user,
setAuth,
setUser: updateUser,
clearAuth,
clearUser,
};
};
@@ -0,0 +1,223 @@
import { useState } from 'react';
import { MemoryClient, Memory as Mem0Memory } from 'mem0ai';
import { OpenAI } from 'openai';
import { Message, Memory } from '@/types';
import { WELCOME_MESSAGE, INVALID_CONFIG_MESSAGE, ERROR_MESSAGE, Provider } from '@/constants/messages';
interface UseChatProps {
user: string;
mem0ApiKey: string;
openaiApiKey: string;
provider: Provider;
}
interface UseChatReturn {
messages: Message[];
memories: Memory[];
thinking: boolean;
sendMessage: (content: string, fileData?: { type: string; data: string | Buffer }) => Promise<void>;
}
type MessageContent = string | {
type: 'image_url';
image_url: {
url: string;
};
};
interface PromptMessage {
role: string;
content: MessageContent;
}
export const useChat = ({ user, mem0ApiKey, openaiApiKey }: UseChatProps): UseChatReturn => {
const [messages, setMessages] = useState<Message[]>([WELCOME_MESSAGE]);
const [memories, setMemories] = useState<Memory[]>();
const [thinking, setThinking] = useState(false);
const openai = new OpenAI({ apiKey: openaiApiKey, dangerouslyAllowBrowser: true});
const updateMemories = async (messages: PromptMessage[]) => {
const memoryClient = new MemoryClient({ apiKey: mem0ApiKey || '' });
try {
await memoryClient.add(messages, {
user_id: user,
});
const response = await memoryClient.getAll({
user_id: user,
});
const newMemories = response.map((memory: Mem0Memory) => ({
id: memory.id || '',
content: memory.memory || '',
timestamp: String(memory.updated_at) || '',
tags: memory.categories || [],
}));
setMemories(newMemories);
} catch (error) {
console.error('Error in updateMemories:', error);
}
};
const formatMessagesForPrompt = (messages: Message[]): PromptMessage[] => {
return messages.map((message) => {
if (message.image) {
return {
role: message.sender,
content: {
type: 'image_url',
image_url: {
url: message.image
}
},
};
}
return {
role: message.sender,
content: message.content,
};
});
};
const sendMessage = async (content: string, fileData?: { type: string; data: string | Buffer }) => {
if (!content.trim() && !fileData) return;
const memoryClient = new MemoryClient({ apiKey: mem0ApiKey || '' });
if (!user) {
const newMessage: Message = {
id: Date.now().toString(),
content,
sender: 'user',
timestamp: new Date().toLocaleTimeString(),
};
setMessages((prev) => [...prev, newMessage, INVALID_CONFIG_MESSAGE]);
return;
}
const userMessage: Message = {
id: Date.now().toString(),
content,
sender: 'user',
timestamp: new Date().toLocaleTimeString(),
...(fileData?.type.startsWith('image/') && { image: fileData.data.toString() }),
};
setMessages((prev) => [...prev, userMessage]);
setThinking(true);
// Get all messages for memory update
const allMessagesForMemory = formatMessagesForPrompt([...messages, userMessage]);
await updateMemories(allMessagesForMemory);
try {
// Get only the last assistant message (if exists) and the current user message
const lastAssistantMessage = messages.filter(msg => msg.sender === 'assistant').slice(-1)[0];
let messagesForLLM = lastAssistantMessage
? [
formatMessagesForPrompt([lastAssistantMessage])[0],
formatMessagesForPrompt([userMessage])[0]
]
: [formatMessagesForPrompt([userMessage])[0]];
// Check if any message has image content
const hasImage = messagesForLLM.some(msg => {
if (typeof msg.content === 'object' && msg.content !== null) {
const content = msg.content as MessageContent;
return typeof content === 'object' && content !== null && 'type' in content && content.type === 'image_url';
}
return false;
});
// For image messages, only use the text content
if (hasImage) {
messagesForLLM = [
...messagesForLLM,
{
role: 'user',
content: userMessage.content
}
];
}
// Fetch relevant memories if there's an image
let relevantMemories = '';
try {
const searchResponse = await memoryClient.getAll({
user_id: user
});
relevantMemories = searchResponse
.map((memory: Mem0Memory) => `Previous context: ${memory.memory}`)
.join('\n');
} catch (error) {
console.error('Error fetching memories:', error);
}
// Add a system message with memories context if there are memories and image
if (relevantMemories.length > 0 && hasImage) {
messagesForLLM = [
{
role: 'system',
content: `Here are some relevant details about the user:\n${relevantMemories}\n\nPlease use this context when responding to the user's message.`
},
...messagesForLLM
];
}
const generateRandomId = () => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
messages: messagesForLLM.map(msg => ({
role: msg.role === 'user' ? 'user' : 'assistant',
content: typeof msg.content === 'object' && msg.content !== null ? [msg.content] : msg.content,
name: generateRandomId(),
})),
stream: true,
});
const assistantMessageId = Date.now() + 1;
const assistantMessage: Message = {
id: assistantMessageId.toString(),
content: '',
sender: 'assistant',
timestamp: new Date().toLocaleTimeString(),
};
setMessages((prev) => [...prev, assistantMessage]);
for await (const chunk of completion) {
const textPart = chunk.choices[0]?.delta?.content || '';
assistantMessage.content += textPart;
setThinking(false);
setMessages((prev) =>
prev.map((msg) =>
msg.id === assistantMessageId.toString()
? { ...msg, content: assistantMessage.content }
: msg
)
);
}
} catch (error) {
console.error('Error in sendMessage:', error);
setMessages((prev) => [...prev, ERROR_MESSAGE]);
} finally {
setThinking(false);
}
};
return {
messages,
memories: memories || [],
thinking,
sendMessage,
};
};
@@ -0,0 +1,45 @@
import { useState } from 'react';
import { FileInfo } from '@/types';
import { convertToBase64, getFileBuffer } from '@/utils/fileUtils';
interface UseFileHandlerReturn {
selectedFile: FileInfo | null;
file: File | null;
fileData: string | Buffer | null;
setSelectedFile: (file: FileInfo | null) => void;
handleFile: (file: File) => Promise<void>;
clearFile: () => void;
}
export const useFileHandler = (): UseFileHandlerReturn => {
const [selectedFile, setSelectedFile] = useState<FileInfo | null>(null);
const [file, setFile] = useState<File | null>(null);
const [fileData, setFileData] = useState<string | Buffer | null>(null);
const handleFile = async (file: File) => {
setFile(file);
if (file.type.startsWith('image/')) {
const base64Data = await convertToBase64(file);
setFileData(base64Data);
} else if (file.type.startsWith('audio/')) {
const bufferData = await getFileBuffer(file);
setFileData(bufferData);
}
};
const clearFile = () => {
setSelectedFile(null);
setFile(null);
setFileData(null);
};
return {
selectedFile,
file,
fileData,
setSelectedFile,
handleFile,
clearFile,
};
};
+97
View File
@@ -0,0 +1,97 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
.loader {
display: flex;
align-items: flex-end;
gap: 5px;
}
.ball {
width: 6px;
height: 6px;
background-color: #4e4e4e;
border-radius: 50%;
animation: bounce 0.6s infinite alternate;
}
.ball:nth-child(2) {
animation-delay: 0.2s;
}
.ball:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
from {
transform: translateY(0);
}
to {
transform: translateY(-4px);
}
}
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+14
View File
@@ -0,0 +1,14 @@
"use client";
import { GlobalState } from "./contexts/GlobalContext";
import Component from "./pages/home";
export default function Home() {
return (
<div>
<GlobalState>
<Component />
</GlobalState>
</div>
);
}
@@ -0,0 +1,41 @@
import { useState } from "react";
import ApiSettingsPopup from "../components/api-settings-popup";
import Memories from "../components/memories";
import Header from "../components/header";
import Messages from "../components/messages";
import InputArea from "../components/input-area";
import ChevronToggle from "../components/chevron-toggle";
export default function Home() {
const [isMemoriesExpanded, setIsMemoriesExpanded] = useState(true);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
return (
<>
<ApiSettingsPopup isOpen={isSettingsOpen} setIsOpen={setIsSettingsOpen} />
<div className="flex h-screen bg-background">
{/* Main Chat Area */}
<div className="flex-1 flex flex-col">
{/* Header */}
<Header setIsSettingsOpen={setIsSettingsOpen} />
{/* Messages */}
<Messages />
{/* Input Area */}
<InputArea />
</div>
{/* Chevron Toggle */}
<ChevronToggle
isMemoriesExpanded={isMemoriesExpanded}
setIsMemoriesExpanded={setIsMemoriesExpanded}
/>
{/* Memories Sidebar */}
<Memories isMemoriesExpanded={isMemoriesExpanded} />
</div>
</>
);
}
+22
View File
@@ -0,0 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface Memory {
id: string;
content: string;
timestamp: string;
tags: string[];
}
export interface Message {
id: string;
content: string;
sender: "user" | "assistant";
timestamp: string;
image?: string;
audio?: any;
}
export interface FileInfo {
name: string;
type: string;
size: number;
}
@@ -0,0 +1,16 @@
import { Buffer } from 'buffer';
export const convertToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
});
};
export const getFileBuffer = async (file: File): Promise<Buffer> => {
const response = await fetch(URL.createObjectURL(file));
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
};
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,62 @@
// tailwind.config.js
/* eslint-env node */
/** @type {import('tailwindcss').Config} */
import tailwindcssAnimate from 'tailwindcss-animate';
export default {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
theme: {
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))',
},
},
},
},
plugins: [tailwindcssAnimate],
};
@@ -0,0 +1,32 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
+13
View File
@@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+223
View File
@@ -0,0 +1,223 @@
import { useState } from 'react';
import { MemoryClient, Memory as Mem0Memory } from 'mem0ai';
import { OpenAI } from 'openai';
import { Message, Memory } from '@/types';
import { WELCOME_MESSAGE, INVALID_CONFIG_MESSAGE, ERROR_MESSAGE, Provider } from '@/constants/messages';
interface UseChatProps {
user: string;
mem0ApiKey: string;
openaiApiKey: string;
provider: Provider;
}
interface UseChatReturn {
messages: Message[];
memories: Memory[];
thinking: boolean;
sendMessage: (content: string, fileData?: { type: string; data: string | Buffer }) => Promise<void>;
}
type MessageContent = string | {
type: 'image_url';
image_url: {
url: string;
};
};
interface PromptMessage {
role: string;
content: MessageContent;
}
export const useChat = ({ user, mem0ApiKey, openaiApiKey }: UseChatProps): UseChatReturn => {
const [messages, setMessages] = useState<Message[]>([WELCOME_MESSAGE]);
const [memories, setMemories] = useState<Memory[]>();
const [thinking, setThinking] = useState(false);
const openai = new OpenAI({ apiKey: openaiApiKey, dangerouslyAllowBrowser: true});
const updateMemories = async (messages: PromptMessage[]) => {
const memoryClient = new MemoryClient({ apiKey: mem0ApiKey || '' });
try {
await memoryClient.add(messages, {
user_id: user,
});
const response = await memoryClient.getAll({
user_id: user,
});
const newMemories = response.map((memory: Mem0Memory) => ({
id: memory.id || '',
content: memory.memory || '',
timestamp: String(memory.updated_at) || '',
tags: memory.categories || [],
}));
setMemories(newMemories);
} catch (error) {
console.error('Error in updateMemories:', error);
}
};
const formatMessagesForPrompt = (messages: Message[]): PromptMessage[] => {
return messages.map((message) => {
if (message.image) {
return {
role: message.sender,
content: {
type: 'image_url',
image_url: {
url: message.image
}
},
};
}
return {
role: message.sender,
content: message.content,
};
});
};
const sendMessage = async (content: string, fileData?: { type: string; data: string | Buffer }) => {
if (!content.trim() && !fileData) return;
const memoryClient = new MemoryClient({ apiKey: mem0ApiKey || '' });
if (!user) {
const newMessage: Message = {
id: Date.now().toString(),
content,
sender: 'user',
timestamp: new Date().toLocaleTimeString(),
};
setMessages((prev) => [...prev, newMessage, INVALID_CONFIG_MESSAGE]);
return;
}
const userMessage: Message = {
id: Date.now().toString(),
content,
sender: 'user',
timestamp: new Date().toLocaleTimeString(),
...(fileData?.type.startsWith('image/') && { image: fileData.data.toString() }),
};
setMessages((prev) => [...prev, userMessage]);
setThinking(true);
// Get all messages for memory update
const allMessagesForMemory = formatMessagesForPrompt([...messages, userMessage]);
await updateMemories(allMessagesForMemory);
try {
// Get only the last assistant message (if exists) and the current user message
const lastAssistantMessage = messages.filter(msg => msg.sender === 'assistant').slice(-1)[0];
let messagesForLLM = lastAssistantMessage
? [
formatMessagesForPrompt([lastAssistantMessage])[0],
formatMessagesForPrompt([userMessage])[0]
]
: [formatMessagesForPrompt([userMessage])[0]];
// Check if any message has image content
const hasImage = messagesForLLM.some(msg => {
if (typeof msg.content === 'object' && msg.content !== null) {
const content = msg.content as MessageContent;
return typeof content === 'object' && content !== null && 'type' in content && content.type === 'image_url';
}
return false;
});
// For image messages, only use the text content
if (hasImage) {
messagesForLLM = [
...messagesForLLM,
{
role: 'user',
content: userMessage.content
}
];
}
// Fetch relevant memories if there's an image
let relevantMemories = '';
try {
const searchResponse = await memoryClient.getAll({
user_id: user
});
relevantMemories = searchResponse
.map((memory: Mem0Memory) => `Previous context: ${memory.memory}`)
.join('\n');
} catch (error) {
console.error('Error fetching memories:', error);
}
// Add a system message with memories context if there are memories and image
if (relevantMemories.length > 0 && hasImage) {
messagesForLLM = [
{
role: 'system',
content: `Here are some relevant details about the user:\n${relevantMemories}\n\nPlease use this context when responding to the user's message.`
},
...messagesForLLM
];
}
const generateRandomId = () => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
messages: messagesForLLM.map(msg => ({
role: msg.role === 'user' ? 'user' : 'assistant',
content: typeof msg.content === 'object' && msg.content !== null ? [msg.content] : msg.content,
name: generateRandomId(),
})),
stream: true,
});
const assistantMessageId = Date.now() + 1;
const assistantMessage: Message = {
id: assistantMessageId.toString(),
content: '',
sender: 'assistant',
timestamp: new Date().toLocaleTimeString(),
};
setMessages((prev) => [...prev, assistantMessage]);
for await (const chunk of completion) {
const textPart = chunk.choices[0]?.delta?.content || '';
assistantMessage.content += textPart;
setThinking(false);
setMessages((prev) =>
prev.map((msg) =>
msg.id === assistantMessageId.toString()
? { ...msg, content: assistantMessage.content }
: msg
)
);
}
} catch (error) {
console.error('Error in sendMessage:', error);
setMessages((prev) => [...prev, ERROR_MESSAGE]);
} finally {
setThinking(false);
}
};
return {
messages,
memories: memories || [],
thinking,
sendMessage,
};
};
+13
View File
@@ -0,0 +1,13 @@
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
buffer: 'buffer'
},
},
})
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.0.1",
"version": "2.1.1",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -82,17 +82,18 @@
},
"dependencies": {
"axios": "1.7.7",
"neo4j-driver": "^5.28.1",
"openai": "4.28.0",
"uuid": "9.0.1",
"zod": "3.22.4"
},
"peerDependencies": {
"@anthropic-ai/sdk": "0.18.0",
"groq-sdk": "0.3.0",
"@qdrant/js-client-rest": "1.13.0",
"@types/jest": "29.5.14",
"@types/pg": "8.11.0",
"@types/sqlite3": "3.1.11",
"groq-sdk": "0.3.0",
"pg": "8.11.3",
"redis": "4.7.0",
"sqlite3": "5.1.7"
@@ -113,5 +114,6 @@
},
"publishConfig": {
"access": "public"
}
},
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b"
}
+3 -2
View File
@@ -11,6 +11,7 @@ import {
SearchOptions,
Webhook,
WebhookPayload,
Message,
} from "./mem0.types";
import { captureClientEvent, generateHash } from "./telemetry";
@@ -168,7 +169,7 @@ export default class MemoryClient {
}
_preparePayload(
messages: string | Array<{ role: string; content: string }>,
messages: string | Array<Message>,
options: MemoryOptions,
): object {
const payload: any = {};
@@ -187,7 +188,7 @@ export default class MemoryClient {
}
async add(
messages: string | Array<{ role: string; content: string }>,
messages: string | Array<Message>,
options: MemoryOptions = {},
): Promise<Array<Memory>> {
this._validateOrgProject();
+8 -1
View File
@@ -28,9 +28,16 @@ export enum API_VERSION {
V2 = "v2",
}
export interface MultiModalMessages {
type: "image_url";
image_url: {
url: string;
};
}
export interface Messages {
role: string;
content: string;
content: string | MultiModalMessages;
}
export interface Message extends Messages {}
+108 -8
View File
@@ -29,7 +29,9 @@ async function runTests(memory: Memory) {
console.log("\nAdding a single memory...");
const result1 = await memory.add(
"Hi, my name is John and I am a software engineer.",
"user123",
{
userId: "john",
},
);
console.log("Added memory:", result1);
@@ -40,7 +42,9 @@ async function runTests(memory: Memory) {
{ role: "user", content: "What is your favorite city?" },
{ role: "assistant", content: "I love Paris, it is my favorite city." },
],
"user123",
{
userId: "john",
},
);
console.log("Added messages:", result2);
@@ -53,7 +57,9 @@ async function runTests(memory: Memory) {
content: "I love New York, it is my favorite city.",
},
],
"user123",
{
userId: "john",
},
);
console.log("Updated messages:", result3);
@@ -75,15 +81,16 @@ async function runTests(memory: Memory) {
// Get all memories
console.log("\nGetting all memories...");
const allMemories = await memory.getAll("user123");
const allMemories = await memory.getAll({
userId: "john",
});
console.log("All memories:", allMemories);
// Search for memories
console.log("\nSearching memories...");
const searchResult = await memory.search(
"What do you know about Paris?",
"user123",
);
const searchResult = await memory.search("What do you know about Paris?", {
userId: "john",
});
console.log("Search results:", searchResult);
// Get memory history
@@ -255,10 +262,103 @@ async function demoRedis() {
await runTests(memory);
}
async function demoGraphMemory() {
console.log("\n=== Testing Graph Memory Store ===\n");
const memory = new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY || "",
model: "text-embedding-3-small",
},
},
vectorStore: {
provider: "memory",
config: {
collectionName: "memories",
dimension: 1536,
},
},
llm: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY || "",
model: "gpt-4-turbo-preview",
},
},
graphStore: {
provider: "neo4j",
config: {
url: process.env.NEO4J_URL || "neo4j://localhost:7687",
username: process.env.NEO4J_USERNAME || "neo4j",
password: process.env.NEO4J_PASSWORD || "password",
},
llm: {
provider: "openai",
config: {
model: "gpt-4-turbo-preview",
},
},
},
historyDbPath: "memory.db",
});
try {
// Reset all memories
await memory.reset();
// Add memories with relationships
const result = await memory.add(
[
{
role: "user",
content: "Alice is Bob's sister and works as a doctor.",
},
{
role: "assistant",
content:
"I understand that Alice and Bob are siblings and Alice is a medical professional.",
},
{ role: "user", content: "Bob is married to Carol who is a teacher." },
],
{
userId: "john",
},
);
console.log("Added memories with relationships:", result);
// Search for connected information
const searchResult = await memory.search(
"Tell me about Bob's family connections",
{
userId: "john",
},
);
console.log("Search results with graph relationships:", searchResult);
} catch (error) {
console.error("Error in graph memory demo:", error);
}
}
async function main() {
// Test in-memory store
await demoMemoryStore();
// Test graph memory if Neo4j environment variables are set
if (
process.env.NEO4J_URL &&
process.env.NEO4J_USERNAME &&
process.env.NEO4J_PASSWORD
) {
await demoGraphMemory();
} else {
console.log(
"\nSkipping Graph Memory test - Neo4j environment variables not set",
);
}
// Test PGVector store if environment variables are set
if (process.env.PGVECTOR_DB) {
await demoPGVector();
+15
View File
@@ -23,5 +23,20 @@ export const DEFAULT_MEMORY_CONFIG: MemoryConfig = {
model: "gpt-4-turbo-preview",
},
},
enableGraph: false,
graphStore: {
provider: "neo4j",
config: {
url: process.env.NEO4J_URL || "neo4j://localhost:7687",
username: process.env.NEO4J_USERNAME || "neo4j",
password: process.env.NEO4J_PASSWORD || "password",
},
llm: {
provider: "openai",
config: {
model: "gpt-4-turbo-preview",
},
},
},
historyDbPath: "memory.db",
};
+5 -1
View File
@@ -47,7 +47,11 @@ export class ConfigManager {
historyDbPath:
userConfig.historyDbPath || DEFAULT_MEMORY_CONFIG.historyDbPath,
customPrompt: userConfig.customPrompt,
graphStore: userConfig.graphStore,
graphStore: {
...DEFAULT_MEMORY_CONFIG.graphStore,
...userConfig.graphStore,
},
enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph,
};
// Validate the merged config
+30
View File
@@ -0,0 +1,30 @@
import { LLMConfig } from "../types";
export interface Neo4jConfig {
url: string | null;
username: string | null;
password: string | null;
}
export interface GraphStoreConfig {
provider: string;
config: Neo4jConfig;
llm?: LLMConfig;
customPrompt?: string;
}
export function validateNeo4jConfig(config: Neo4jConfig): void {
const { url, username, password } = config;
if (!url || !username || !password) {
throw new Error("Please provide 'url', 'username' and 'password'.");
}
}
export function validateGraphStoreConfig(config: GraphStoreConfig): void {
const { provider } = config;
if (provider === "neo4j") {
validateNeo4jConfig(config.config);
} else {
throw new Error(`Unsupported graph store provider: ${provider}`);
}
}
+213
View File
@@ -0,0 +1,213 @@
export interface GraphToolParameters {
source: string;
destination: string;
relationship: string;
source_type?: string;
destination_type?: string;
}
export interface GraphEntitiesParameters {
entities: Array<{
entity: string;
entity_type: string;
}>;
}
export interface GraphRelationsParameters {
entities: Array<{
source: string;
relationship: string;
destination: string;
}>;
}
export const UPDATE_MEMORY_TOOL_GRAPH = {
type: "function",
function: {
name: "update_graph_memory",
description:
"Update the relationship key of an existing graph memory based on new information.",
parameters: {
type: "object",
properties: {
source: {
type: "string",
description:
"The identifier of the source node in the relationship to be updated.",
},
destination: {
type: "string",
description:
"The identifier of the destination node in the relationship to be updated.",
},
relationship: {
type: "string",
description:
"The new or updated relationship between the source and destination nodes.",
},
},
required: ["source", "destination", "relationship"],
additionalProperties: false,
},
},
};
export const ADD_MEMORY_TOOL_GRAPH = {
type: "function",
function: {
name: "add_graph_memory",
description: "Add a new graph memory to the knowledge graph.",
parameters: {
type: "object",
properties: {
source: {
type: "string",
description:
"The identifier of the source node in the new relationship.",
},
destination: {
type: "string",
description:
"The identifier of the destination node in the new relationship.",
},
relationship: {
type: "string",
description:
"The type of relationship between the source and destination nodes.",
},
source_type: {
type: "string",
description: "The type or category of the source node.",
},
destination_type: {
type: "string",
description: "The type or category of the destination node.",
},
},
required: [
"source",
"destination",
"relationship",
"source_type",
"destination_type",
],
additionalProperties: false,
},
},
};
export const NOOP_TOOL = {
type: "function",
function: {
name: "noop",
description: "No operation should be performed to the graph entities.",
parameters: {
type: "object",
properties: {},
required: [],
additionalProperties: false,
},
},
};
export const RELATIONS_TOOL = {
type: "function",
function: {
name: "establish_relationships",
description:
"Establish relationships among the entities based on the provided text.",
parameters: {
type: "object",
properties: {
entities: {
type: "array",
items: {
type: "object",
properties: {
source: {
type: "string",
description: "The source entity of the relationship.",
},
relationship: {
type: "string",
description:
"The relationship between the source and destination entities.",
},
destination: {
type: "string",
description: "The destination entity of the relationship.",
},
},
required: ["source", "relationship", "destination"],
additionalProperties: false,
},
},
},
required: ["entities"],
additionalProperties: false,
},
},
};
export const EXTRACT_ENTITIES_TOOL = {
type: "function",
function: {
name: "extract_entities",
description: "Extract entities and their types from the text.",
parameters: {
type: "object",
properties: {
entities: {
type: "array",
items: {
type: "object",
properties: {
entity: {
type: "string",
description: "The name or identifier of the entity.",
},
entity_type: {
type: "string",
description: "The type or category of the entity.",
},
},
required: ["entity", "entity_type"],
additionalProperties: false,
},
description: "An array of entities with their types.",
},
},
required: ["entities"],
additionalProperties: false,
},
},
};
export const DELETE_MEMORY_TOOL_GRAPH = {
type: "function",
function: {
name: "delete_graph_memory",
description: "Delete the relationship between two nodes.",
parameters: {
type: "object",
properties: {
source: {
type: "string",
description: "The identifier of the source node in the relationship.",
},
relationship: {
type: "string",
description:
"The existing relationship between the source and destination nodes that needs to be deleted.",
},
destination: {
type: "string",
description:
"The identifier of the destination node in the relationship.",
},
},
required: ["source", "relationship", "destination"],
additionalProperties: false,
},
},
};
+114
View File
@@ -0,0 +1,114 @@
export const UPDATE_GRAPH_PROMPT = `
You are an AI expert specializing in graph memory management and optimization. Your task is to analyze existing graph memories alongside new information, and update the relationships in the memory list to ensure the most accurate, current, and coherent representation of knowledge.
Input:
1. Existing Graph Memories: A list of current graph memories, each containing source, target, and relationship information.
2. New Graph Memory: Fresh information to be integrated into the existing graph structure.
Guidelines:
1. Identification: Use the source and target as primary identifiers when matching existing memories with new information.
2. Conflict Resolution:
- If new information contradicts an existing memory:
a) For matching source and target but differing content, update the relationship of the existing memory.
b) If the new memory provides more recent or accurate information, update the existing memory accordingly.
3. Comprehensive Review: Thoroughly examine each existing graph memory against the new information, updating relationships as necessary. Multiple updates may be required.
4. Consistency: Maintain a uniform and clear style across all memories. Each entry should be concise yet comprehensive.
5. Semantic Coherence: Ensure that updates maintain or improve the overall semantic structure of the graph.
6. Temporal Awareness: If timestamps are available, consider the recency of information when making updates.
7. Relationship Refinement: Look for opportunities to refine relationship descriptions for greater precision or clarity.
8. Redundancy Elimination: Identify and merge any redundant or highly similar relationships that may result from the update.
Memory Format:
source -- RELATIONSHIP -- destination
Task Details:
======= Existing Graph Memories:=======
{existing_memories}
======= New Graph Memory:=======
{new_memories}
Output:
Provide a list of update instructions, each specifying the source, target, and the new relationship to be set. Only include memories that require updates.
`;
export const EXTRACT_RELATIONS_PROMPT = `
You are an advanced algorithm designed to extract structured information from text to construct knowledge graphs. Your goal is to capture comprehensive and accurate information. Follow these key principles:
1. Extract only explicitly stated information from the text.
2. Establish relationships among the entities provided.
3. Use "USER_ID" as the source entity for any self-references (e.g., "I," "me," "my," etc.) in user messages.
CUSTOM_PROMPT
Relationships:
- Use consistent, general, and timeless relationship types.
- Example: Prefer "professor" over "became_professor."
- Relationships should only be established among the entities explicitly mentioned in the user message.
Entity Consistency:
- Ensure that relationships are coherent and logically align with the context of the message.
- Maintain consistent naming for entities across the extracted data.
Strive to construct a coherent and easily understandable knowledge graph by eshtablishing all the relationships among the entities and adherence to the user's context.
Adhere strictly to these guidelines to ensure high-quality knowledge graph extraction.
`;
export const DELETE_RELATIONS_SYSTEM_PROMPT = `
You are a graph memory manager specializing in identifying, managing, and optimizing relationships within graph-based memories. Your primary task is to analyze a list of existing relationships and determine which ones should be deleted based on the new information provided.
Input:
1. Existing Graph Memories: A list of current graph memories, each containing source, relationship, and destination information.
2. New Text: The new information to be integrated into the existing graph structure.
3. Use "USER_ID" as node for any self-references (e.g., "I," "me," "my," etc.) in user messages.
Guidelines:
1. Identification: Use the new information to evaluate existing relationships in the memory graph.
2. Deletion Criteria: Delete a relationship only if it meets at least one of these conditions:
- Outdated or Inaccurate: The new information is more recent or accurate.
- Contradictory: The new information conflicts with or negates the existing information.
3. DO NOT DELETE if their is a possibility of same type of relationship but different destination nodes.
4. Comprehensive Analysis:
- Thoroughly examine each existing relationship against the new information and delete as necessary.
- Multiple deletions may be required based on the new information.
5. Semantic Integrity:
- Ensure that deletions maintain or improve the overall semantic structure of the graph.
- Avoid deleting relationships that are NOT contradictory/outdated to the new information.
6. Temporal Awareness: Prioritize recency when timestamps are available.
7. Necessity Principle: Only DELETE relationships that must be deleted and are contradictory/outdated to the new information to maintain an accurate and coherent memory graph.
Note: DO NOT DELETE if their is a possibility of same type of relationship but different destination nodes.
For example:
Existing Memory: alice -- loves_to_eat -- pizza
New Information: Alice also loves to eat burger.
Do not delete in the above example because there is a possibility that Alice loves to eat both pizza and burger.
Memory Format:
source -- relationship -- destination
Provide a list of deletion instructions, each specifying the relationship to be deleted.
`;
export function getDeleteMessages(
existingMemoriesString: string,
data: string,
userId: string,
): [string, string] {
return [
DELETE_RELATIONS_SYSTEM_PROMPT.replace("USER_ID", userId),
`Here are the existing memories: ${existingMemoriesString} \n\n New Information: ${data}`,
];
}
export function formatEntities(
entities: Array<{
source: string;
relationship: string;
destination: string;
}>,
): string {
return entities
.map((e) => `${e.source} -- ${e.relationship} -- ${e.destination}`)
.join("\n");
}
+8 -2
View File
@@ -27,9 +27,15 @@ export class AnthropicLLM implements LLM {
model: this.model,
messages: otherMessages.map((msg) => ({
role: msg.role as "user" | "assistant",
content: msg.content,
content:
typeof msg.content === "string"
? msg.content
: msg.content.image_url.url,
})),
system: systemMessage?.content,
system:
typeof systemMessage?.content === "string"
? systemMessage.content
: undefined,
max_tokens: 4096,
});
+8 -3
View File
@@ -3,12 +3,17 @@ import { Message } from "../types";
export interface LLMResponse {
content: string;
role: string;
toolCalls?: Array<{
name: string;
arguments: string;
}>;
}
export interface LLM {
generateResponse(
messages: Message[],
responseFormat?: { type: string },
): Promise<string>;
messages: Array<{ role: string; content: string }>,
response_format: { type: string },
tools?: any[],
): Promise<any>;
generateChat(messages: Message[]): Promise<LLMResponse>;
}
+8 -2
View File
@@ -23,7 +23,10 @@ export class GroqLLM implements LLM {
model: this.model,
messages: messages.map((msg) => ({
role: msg.role as "system" | "user" | "assistant",
content: msg.content,
content:
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content),
})),
response_format: responseFormat as { type: "text" | "json_object" },
});
@@ -36,7 +39,10 @@ export class GroqLLM implements LLM {
model: this.model,
messages: messages.map((msg) => ({
role: msg.role as "system" | "user" | "assistant",
content: msg.content,
content:
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content),
})),
});
+39 -11
View File
@@ -8,30 +8,58 @@ export class OpenAILLM implements LLM {
constructor(config: LLMConfig) {
this.openai = new OpenAI({ apiKey: config.apiKey });
this.model = config.model || "gpt-4-turbo-preview";
this.model = config.model || "gpt-4o-mini";
}
async generateResponse(
messages: Message[],
responseFormat?: { type: string },
): Promise<string> {
tools?: any[],
): Promise<string | LLMResponse> {
const completion = await this.openai.chat.completions.create({
messages: messages.map((msg) => ({
role: msg.role as "system" | "user" | "assistant",
content: msg.content,
})),
messages: messages.map((msg) => {
const role = msg.role as "system" | "user" | "assistant";
return {
role,
content:
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content),
};
}),
model: this.model,
response_format: responseFormat as { type: "text" | "json_object" },
...(tools && { tools, tool_choice: "auto" }),
});
return completion.choices[0].message.content || "";
const response = completion.choices[0].message;
if (response.tool_calls) {
return {
content: response.content || "",
role: response.role,
toolCalls: response.tool_calls.map((call) => ({
name: call.function.name,
arguments: call.function.arguments,
})),
};
}
return response.content || "";
}
async generateChat(messages: Message[]): Promise<LLMResponse> {
const completion = await this.openai.chat.completions.create({
messages: messages.map((msg) => ({
role: msg.role as "system" | "user" | "assistant",
content: msg.content,
})),
messages: messages.map((msg) => {
const role = msg.role as "system" | "user" | "assistant";
return {
role,
content:
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content),
};
}),
model: this.model,
});
const response = completion.choices[0].message;
+54 -22
View File
@@ -3,48 +3,80 @@ import { LLM, LLMResponse } from "./base";
import { LLMConfig, Message } from "../types";
export class OpenAIStructuredLLM implements LLM {
private client: OpenAI;
private openai: OpenAI;
private model: string;
constructor(config: LLMConfig) {
const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("OpenAI API key is required");
}
const baseUrl = process.env.OPENAI_API_BASE || "https://api.openai.com/v1";
this.client = new OpenAI({ apiKey, baseURL: baseUrl });
this.model = config.model || "gpt-4-0125-preview";
this.openai = new OpenAI({ apiKey: config.apiKey });
this.model = config.model || "gpt-4-turbo-preview";
}
async generateResponse(
messages: Message[],
responseFormat?: { type: string },
): Promise<string> {
const response = await this.client.chat.completions.create({
model: this.model,
responseFormat?: { type: string } | null,
tools?: any[],
): Promise<string | LLMResponse> {
const completion = await this.openai.chat.completions.create({
messages: messages.map((msg) => ({
role: msg.role as "system" | "user" | "assistant",
content: msg.content,
content:
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content),
})),
response_format: responseFormat as { type: "text" | "json_object" },
model: this.model,
...(tools
? {
tools: tools.map((tool) => ({
type: "function",
function: {
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
},
})),
tool_choice: "auto" as const,
}
: responseFormat
? {
response_format: {
type: responseFormat.type as "text" | "json_object",
},
}
: {}),
});
return response.choices[0].message.content || "";
const response = completion.choices[0].message;
if (response.tool_calls) {
return {
content: response.content || "",
role: response.role,
toolCalls: response.tool_calls.map((call) => ({
name: call.function.name,
arguments: call.function.arguments,
})),
};
}
return response.content || "";
}
async generateChat(messages: Message[]): Promise<LLMResponse> {
const response = await this.client.chat.completions.create({
model: this.model,
const completion = await this.openai.chat.completions.create({
messages: messages.map((msg) => ({
role: msg.role as "system" | "user" | "assistant",
content: msg.content,
content:
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content),
})),
model: this.model,
});
const message = response.choices[0].message;
const response = completion.choices[0].message;
return {
content: message.content || "",
role: message.role,
content: response.content || "",
role: response.role,
};
}
}
+675
View File
@@ -0,0 +1,675 @@
import neo4j, { Driver } from "neo4j-driver";
import { BM25 } from "../utils/bm25";
import { GraphStoreConfig } from "../graphs/configs";
import { MemoryConfig } from "../types";
import { EmbedderFactory, LLMFactory } from "../utils/factory";
import { Embedder } from "../embeddings/base";
import { LLM } from "../llms/base";
import {
DELETE_MEMORY_TOOL_GRAPH,
EXTRACT_ENTITIES_TOOL,
RELATIONS_TOOL,
} from "../graphs/tools";
import { EXTRACT_RELATIONS_PROMPT, getDeleteMessages } from "../graphs/utils";
import { logger } from "../utils/logger";
interface SearchOutput {
source: string;
source_id: string;
relationship: string;
relation_id: string;
destination: string;
destination_id: string;
similarity: number;
}
interface ToolCall {
name: string;
arguments: string;
}
interface LLMResponse {
toolCalls?: ToolCall[];
}
interface Tool {
type: string;
function: {
name: string;
description: string;
parameters: Record<string, any>;
};
}
interface GraphMemoryResult {
deleted_entities: any[];
added_entities: any[];
relations?: any[];
}
export class MemoryGraph {
private config: MemoryConfig;
private graph: Driver;
private embeddingModel: Embedder;
private llm: LLM;
private structuredLlm: LLM;
private llmProvider: string;
private threshold: number;
constructor(config: MemoryConfig) {
this.config = config;
if (
!config.graphStore?.config?.url ||
!config.graphStore?.config?.username ||
!config.graphStore?.config?.password
) {
throw new Error("Neo4j configuration is incomplete");
}
this.graph = neo4j.driver(
config.graphStore.config.url,
neo4j.auth.basic(
config.graphStore.config.username,
config.graphStore.config.password,
),
);
this.embeddingModel = EmbedderFactory.create(
this.config.embedder.provider,
this.config.embedder.config,
);
this.llmProvider = "openai";
if (this.config.llm?.provider) {
this.llmProvider = this.config.llm.provider;
}
if (this.config.graphStore?.llm?.provider) {
this.llmProvider = this.config.graphStore.llm.provider;
}
this.llm = LLMFactory.create(this.llmProvider, this.config.llm.config);
this.structuredLlm = LLMFactory.create(
"openai_structured",
this.config.llm.config,
);
this.threshold = 0.7;
}
async add(
data: string,
filters: Record<string, any>,
): Promise<GraphMemoryResult> {
const entityTypeMap = await this._retrieveNodesFromData(data, filters);
const toBeAdded = await this._establishNodesRelationsFromData(
data,
filters,
entityTypeMap,
);
const searchOutput = await this._searchGraphDb(
Object.keys(entityTypeMap),
filters,
);
const toBeDeleted = await this._getDeleteEntitiesFromSearchOutput(
searchOutput,
data,
filters,
);
const deletedEntities = await this._deleteEntities(
toBeDeleted,
filters["userId"],
);
const addedEntities = await this._addEntities(
toBeAdded,
filters["userId"],
entityTypeMap,
);
return {
deleted_entities: deletedEntities,
added_entities: addedEntities,
relations: toBeAdded,
};
}
async search(query: string, filters: Record<string, any>, limit = 100) {
const entityTypeMap = await this._retrieveNodesFromData(query, filters);
const searchOutput = await this._searchGraphDb(
Object.keys(entityTypeMap),
filters,
);
if (!searchOutput.length) {
return [];
}
const searchOutputsSequence = searchOutput.map((item) => [
item.source,
item.relationship,
item.destination,
]);
const bm25 = new BM25(searchOutputsSequence);
const tokenizedQuery = query.split(" ");
const rerankedResults = bm25.search(tokenizedQuery).slice(0, 5);
const searchResults = rerankedResults.map((item) => ({
source: item[0],
relationship: item[1],
destination: item[2],
}));
logger.info(`Returned ${searchResults.length} search results`);
return searchResults;
}
async deleteAll(filters: Record<string, any>) {
const session = this.graph.session();
try {
await session.run("MATCH (n {user_id: $user_id}) DETACH DELETE n", {
user_id: filters["userId"],
});
} finally {
await session.close();
}
}
async getAll(filters: Record<string, any>, limit = 100) {
const session = this.graph.session();
try {
const result = await session.run(
`
MATCH (n {user_id: $user_id})-[r]->(m {user_id: $user_id})
RETURN n.name AS source, type(r) AS relationship, m.name AS target
LIMIT toInteger($limit)
`,
{ user_id: filters["userId"], limit: Math.floor(Number(limit)) },
);
const finalResults = result.records.map((record) => ({
source: record.get("source"),
relationship: record.get("relationship"),
target: record.get("target"),
}));
logger.info(`Retrieved ${finalResults.length} relationships`);
return finalResults;
} finally {
await session.close();
}
}
private async _retrieveNodesFromData(
data: string,
filters: Record<string, any>,
) {
const tools = [EXTRACT_ENTITIES_TOOL] as Tool[];
const searchResults = await this.structuredLlm.generateResponse(
[
{
role: "system",
content: `You are a smart assistant who understands entities and their types in a given text. If user message contains self reference such as 'I', 'me', 'my' etc. then use ${filters["userId"]} as the source entity. Extract all the entities from the text. ***DO NOT*** answer the question itself if the given text is a question.`,
},
{ role: "user", content: data },
],
{ type: "json_object" },
tools,
);
let entityTypeMap: Record<string, string> = {};
try {
if (typeof searchResults !== "string" && searchResults.toolCalls) {
for (const call of searchResults.toolCalls) {
if (call.name === "extract_entities") {
const args = JSON.parse(call.arguments);
for (const item of args.entities) {
entityTypeMap[item.entity] = item.entity_type;
}
}
}
}
} catch (e) {
logger.error(`Error in search tool: ${e}`);
}
entityTypeMap = Object.fromEntries(
Object.entries(entityTypeMap).map(([k, v]) => [
k.toLowerCase().replace(/ /g, "_"),
v.toLowerCase().replace(/ /g, "_"),
]),
);
logger.debug(`Entity type map: ${JSON.stringify(entityTypeMap)}`);
return entityTypeMap;
}
private async _establishNodesRelationsFromData(
data: string,
filters: Record<string, any>,
entityTypeMap: Record<string, string>,
) {
let messages;
if (this.config.graphStore?.customPrompt) {
messages = [
{
role: "system",
content:
EXTRACT_RELATIONS_PROMPT.replace(
"USER_ID",
filters["userId"],
).replace(
"CUSTOM_PROMPT",
`4. ${this.config.graphStore.customPrompt}`,
) + "\nPlease provide your response in JSON format.",
},
{ role: "user", content: data },
];
} else {
messages = [
{
role: "system",
content:
EXTRACT_RELATIONS_PROMPT.replace("USER_ID", filters["userId"]) +
"\nPlease provide your response in JSON format.",
},
{
role: "user",
content: `List of entities: ${Object.keys(entityTypeMap)}. \n\nText: ${data}`,
},
];
}
const tools = [RELATIONS_TOOL] as Tool[];
const extractedEntities = await this.structuredLlm.generateResponse(
messages,
{ type: "json_object" },
tools,
);
let entities: any[] = [];
if (typeof extractedEntities !== "string" && extractedEntities.toolCalls) {
const toolCall = extractedEntities.toolCalls[0];
if (toolCall && toolCall.arguments) {
const args = JSON.parse(toolCall.arguments);
entities = args.entities || [];
}
}
entities = this._removeSpacesFromEntities(entities);
logger.debug(`Extracted entities: ${JSON.stringify(entities)}`);
return entities;
}
private async _searchGraphDb(
nodeList: string[],
filters: Record<string, any>,
limit = 100,
): Promise<SearchOutput[]> {
const resultRelations: SearchOutput[] = [];
const session = this.graph.session();
try {
for (const node of nodeList) {
const nEmbedding = await this.embeddingModel.embed(node);
const cypher = `
MATCH (n)
WHERE n.embedding IS NOT NULL AND n.user_id = $user_id
WITH n,
round(reduce(dot = 0.0, i IN range(0, size(n.embedding)-1) | dot + n.embedding[i] * $n_embedding[i]) /
(sqrt(reduce(l2 = 0.0, i IN range(0, size(n.embedding)-1) | l2 + n.embedding[i] * n.embedding[i])) *
sqrt(reduce(l2 = 0.0, i IN range(0, size($n_embedding)-1) | l2 + $n_embedding[i] * $n_embedding[i]))), 4) AS similarity
WHERE similarity >= $threshold
MATCH (n)-[r]->(m)
RETURN n.name AS source, elementId(n) AS source_id, type(r) AS relationship, elementId(r) AS relation_id, m.name AS destination, elementId(m) AS destination_id, similarity
UNION
MATCH (n)
WHERE n.embedding IS NOT NULL AND n.user_id = $user_id
WITH n,
round(reduce(dot = 0.0, i IN range(0, size(n.embedding)-1) | dot + n.embedding[i] * $n_embedding[i]) /
(sqrt(reduce(l2 = 0.0, i IN range(0, size(n.embedding)-1) | l2 + n.embedding[i] * n.embedding[i])) *
sqrt(reduce(l2 = 0.0, i IN range(0, size($n_embedding)-1) | l2 + $n_embedding[i] * $n_embedding[i]))), 4) AS similarity
WHERE similarity >= $threshold
MATCH (m)-[r]->(n)
RETURN m.name AS source, elementId(m) AS source_id, type(r) AS relationship, elementId(r) AS relation_id, n.name AS destination, elementId(n) AS destination_id, similarity
ORDER BY similarity DESC
LIMIT toInteger($limit)
`;
const result = await session.run(cypher, {
n_embedding: nEmbedding,
threshold: this.threshold,
user_id: filters["userId"],
limit: Math.floor(Number(limit)),
});
resultRelations.push(
...result.records.map((record) => ({
source: record.get("source"),
source_id: record.get("source_id").toString(),
relationship: record.get("relationship"),
relation_id: record.get("relation_id").toString(),
destination: record.get("destination"),
destination_id: record.get("destination_id").toString(),
similarity: record.get("similarity"),
})),
);
}
} finally {
await session.close();
}
return resultRelations;
}
private async _getDeleteEntitiesFromSearchOutput(
searchOutput: SearchOutput[],
data: string,
filters: Record<string, any>,
) {
const searchOutputString = searchOutput
.map(
(item) =>
`${item.source} -- ${item.relationship} -- ${item.destination}`,
)
.join("\n");
const [systemPrompt, userPrompt] = getDeleteMessages(
searchOutputString,
data,
filters["userId"],
);
const tools = [DELETE_MEMORY_TOOL_GRAPH] as Tool[];
const memoryUpdates = await this.structuredLlm.generateResponse(
[
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
{ type: "json_object" },
tools,
);
const toBeDeleted: any[] = [];
if (typeof memoryUpdates !== "string" && memoryUpdates.toolCalls) {
for (const item of memoryUpdates.toolCalls) {
if (item.name === "delete_graph_memory") {
toBeDeleted.push(JSON.parse(item.arguments));
}
}
}
const cleanedToBeDeleted = this._removeSpacesFromEntities(toBeDeleted);
logger.debug(
`Deleted relationships: ${JSON.stringify(cleanedToBeDeleted)}`,
);
return cleanedToBeDeleted;
}
private async _deleteEntities(toBeDeleted: any[], userId: string) {
const results: any[] = [];
const session = this.graph.session();
try {
for (const item of toBeDeleted) {
const { source, destination, relationship } = item;
const cypher = `
MATCH (n {name: $source_name, user_id: $user_id})
-[r:${relationship}]->
(m {name: $dest_name, user_id: $user_id})
DELETE r
RETURN
n.name AS source,
m.name AS target,
type(r) AS relationship
`;
const result = await session.run(cypher, {
source_name: source,
dest_name: destination,
user_id: userId,
});
results.push(result.records);
}
} finally {
await session.close();
}
return results;
}
private async _addEntities(
toBeAdded: any[],
userId: string,
entityTypeMap: Record<string, string>,
) {
const results: any[] = [];
const session = this.graph.session();
try {
for (const item of toBeAdded) {
const { source, destination, relationship } = item;
const sourceType = entityTypeMap[source] || "unknown";
const destinationType = entityTypeMap[destination] || "unknown";
const sourceEmbedding = await this.embeddingModel.embed(source);
const destEmbedding = await this.embeddingModel.embed(destination);
const sourceNodeSearchResult = await this._searchSourceNode(
sourceEmbedding,
userId,
);
const destinationNodeSearchResult = await this._searchDestinationNode(
destEmbedding,
userId,
);
let cypher: string;
let params: Record<string, any>;
if (
destinationNodeSearchResult.length === 0 &&
sourceNodeSearchResult.length > 0
) {
cypher = `
MATCH (source)
WHERE elementId(source) = $source_id
MERGE (destination:${destinationType} {name: $destination_name, user_id: $user_id})
ON CREATE SET
destination.created = timestamp(),
destination.embedding = $destination_embedding
MERGE (source)-[r:${relationship}]->(destination)
ON CREATE SET
r.created = timestamp()
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
`;
params = {
source_id: sourceNodeSearchResult[0].elementId,
destination_name: destination,
destination_embedding: destEmbedding,
user_id: userId,
};
} else if (
destinationNodeSearchResult.length > 0 &&
sourceNodeSearchResult.length === 0
) {
cypher = `
MATCH (destination)
WHERE elementId(destination) = $destination_id
MERGE (source:${sourceType} {name: $source_name, user_id: $user_id})
ON CREATE SET
source.created = timestamp(),
source.embedding = $source_embedding
MERGE (source)-[r:${relationship}]->(destination)
ON CREATE SET
r.created = timestamp()
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
`;
params = {
destination_id: destinationNodeSearchResult[0].elementId,
source_name: source,
source_embedding: sourceEmbedding,
user_id: userId,
};
} else if (
sourceNodeSearchResult.length > 0 &&
destinationNodeSearchResult.length > 0
) {
cypher = `
MATCH (source)
WHERE elementId(source) = $source_id
MATCH (destination)
WHERE elementId(destination) = $destination_id
MERGE (source)-[r:${relationship}]->(destination)
ON CREATE SET
r.created_at = timestamp(),
r.updated_at = timestamp()
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
`;
params = {
source_id: sourceNodeSearchResult[0]?.elementId,
destination_id: destinationNodeSearchResult[0]?.elementId,
user_id: userId,
};
} else {
cypher = `
MERGE (n:${sourceType} {name: $source_name, user_id: $user_id})
ON CREATE SET n.created = timestamp(), n.embedding = $source_embedding
ON MATCH SET n.embedding = $source_embedding
MERGE (m:${destinationType} {name: $dest_name, user_id: $user_id})
ON CREATE SET m.created = timestamp(), m.embedding = $dest_embedding
ON MATCH SET m.embedding = $dest_embedding
MERGE (n)-[rel:${relationship}]->(m)
ON CREATE SET rel.created = timestamp()
RETURN n.name AS source, type(rel) AS relationship, m.name AS target
`;
params = {
source_name: source,
dest_name: destination,
source_embedding: sourceEmbedding,
dest_embedding: destEmbedding,
user_id: userId,
};
}
const result = await session.run(cypher, params);
results.push(result.records);
}
} finally {
await session.close();
}
return results;
}
private _removeSpacesFromEntities(entityList: any[]) {
return entityList.map((item) => ({
...item,
source: item.source.toLowerCase().replace(/ /g, "_"),
relationship: item.relationship.toLowerCase().replace(/ /g, "_"),
destination: item.destination.toLowerCase().replace(/ /g, "_"),
}));
}
private async _searchSourceNode(
sourceEmbedding: number[],
userId: string,
threshold = 0.9,
) {
const session = this.graph.session();
try {
const cypher = `
MATCH (source_candidate)
WHERE source_candidate.embedding IS NOT NULL
AND source_candidate.user_id = $user_id
WITH source_candidate,
round(
reduce(dot = 0.0, i IN range(0, size(source_candidate.embedding)-1) |
dot + source_candidate.embedding[i] * $source_embedding[i]) /
(sqrt(reduce(l2 = 0.0, i IN range(0, size(source_candidate.embedding)-1) |
l2 + source_candidate.embedding[i] * source_candidate.embedding[i])) *
sqrt(reduce(l2 = 0.0, i IN range(0, size($source_embedding)-1) |
l2 + $source_embedding[i] * $source_embedding[i])))
, 4) AS source_similarity
WHERE source_similarity >= $threshold
WITH source_candidate, source_similarity
ORDER BY source_similarity DESC
LIMIT 1
RETURN elementId(source_candidate) as element_id
`;
const params = {
source_embedding: sourceEmbedding,
user_id: userId,
threshold,
};
const result = await session.run(cypher, params);
return result.records.map((record) => ({
elementId: record.get("element_id").toString(),
}));
} finally {
await session.close();
}
}
private async _searchDestinationNode(
destinationEmbedding: number[],
userId: string,
threshold = 0.9,
) {
const session = this.graph.session();
try {
const cypher = `
MATCH (destination_candidate)
WHERE destination_candidate.embedding IS NOT NULL
AND destination_candidate.user_id = $user_id
WITH destination_candidate,
round(
reduce(dot = 0.0, i IN range(0, size(destination_candidate.embedding)-1) |
dot + destination_candidate.embedding[i] * $destination_embedding[i]) /
(sqrt(reduce(l2 = 0.0, i IN range(0, size(destination_candidate.embedding)-1) |
l2 + destination_candidate.embedding[i] * destination_candidate.embedding[i])) *
sqrt(reduce(l2 = 0.0, i IN range(0, size($destination_embedding)-1) |
l2 + $destination_embedding[i] * $destination_embedding[i])))
, 4) AS destination_similarity
WHERE destination_similarity >= $threshold
WITH destination_candidate, destination_similarity
ORDER BY destination_similarity DESC
LIMIT 1
RETURN elementId(destination_candidate) as element_id
`;
const params = {
destination_embedding: destinationEmbedding,
user_id: userId,
threshold,
};
const result = await session.run(cypher, params);
return result.records.map((record) => ({
elementId: record.get("element_id").toString(),
}));
} finally {
await session.close();
}
}
}
+49 -3
View File
@@ -24,12 +24,14 @@ import { Embedder } from "../embeddings/base";
import { LLM } from "../llms/base";
import { VectorStore } from "../vector_stores/base";
import { ConfigManager } from "../config/manager";
import { MemoryGraph } from "./graph_memory";
import {
AddMemoryOptions,
SearchMemoryOptions,
DeleteAllMemoryOptions,
GetAllMemoryOptions,
} from "./memory.types";
import { parse_vision_messages } from "../utils/memory";
export class Memory {
private config: MemoryConfig;
@@ -40,6 +42,8 @@ export class Memory {
private db: SQLiteManager;
private collectionName: string;
private apiVersion: string;
private graphMemory?: MemoryGraph;
private enableGraph: boolean;
constructor(config: Partial<MemoryConfig> = {}) {
// Merge and validate config
@@ -61,6 +65,12 @@ export class Memory {
this.db = new SQLiteManager(this.config.historyDbPath || ":memory:");
this.collectionName = this.config.vectorStore.config.collectionName;
this.apiVersion = this.config.version || "v1.0";
this.enableGraph = this.config.enableGraph || false;
// Initialize graph memory if configured
if (this.enableGraph && this.config.graphStore) {
this.graphMemory = new MemoryGraph(this.config);
}
}
static fromConfig(configDict: Record<string, any>): Memory {
@@ -100,13 +110,32 @@ export class Memory {
? (messages as Message[])
: [{ role: "user", content: messages }];
const final_parsedMessages = await parse_vision_messages(parsedMessages);
// Add to vector store
const vectorStoreResult = await this.addToVectorStore(
parsedMessages,
final_parsedMessages,
metadata,
filters,
);
return { results: vectorStoreResult };
// Add to graph store if available
let graphResult;
if (this.graphMemory) {
try {
graphResult = await this.graphMemory.add(
final_parsedMessages.map((m) => m.content).join("\n"),
filters,
);
} catch (error) {
console.error("Error adding to graph memory:", error);
}
}
return {
results: vectorStoreResult,
relations: graphResult?.relations,
};
}
private async addToVectorStore(
@@ -284,6 +313,7 @@ export class Memory {
);
}
// Search vector store
const queryEmbedding = await this.embedder.embed(query);
const memories = await this.vectorStore.search(
queryEmbedding,
@@ -291,6 +321,16 @@ export class Memory {
filters,
);
// Search graph store if available
let graphResults;
if (this.graphMemory) {
try {
graphResults = await this.graphMemory.search(query, filters);
} catch (error) {
console.error("Error searching graph memory:", error);
}
}
const excludedKeys = new Set([
"userId",
"agentId",
@@ -315,7 +355,10 @@ export class Memory {
...(mem.payload.runId && { runId: mem.payload.runId }),
}));
return { results };
return {
results,
relations: graphResults,
};
}
async update(memoryId: string, data: string): Promise<{ message: string }> {
@@ -360,6 +403,9 @@ export class Memory {
async reset(): Promise<void> {
await this.db.reset();
await this.vectorStore.deleteCol();
if (this.graphMemory) {
await this.graphMemory.deleteAll({ userId: "default" });
}
this.vectorStore = VectorStoreFactory.create(
this.config.vectorStore.provider,
this.config.vectorStore.config,

Some files were not shown because too many files have changed in this diff Show More