Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4fb22a7c | ||
|
|
9c0954133f | ||
|
|
e9a0be66d8 | ||
|
|
192c33f190 | ||
|
|
75ca528666 | ||
|
|
e30e4967ae | ||
|
|
d3911b92cf | ||
|
|
92cfc1c8ef | ||
|
|
33fcc53e4b | ||
|
|
e761a1e865 | ||
|
|
f2ce92ebcc | ||
|
|
bbb812e0a0 | ||
|
|
9a302cef30 | ||
|
|
ae729da4d1 | ||
|
|
655ae794b6 | ||
|
|
1aef468ebe | ||
|
|
78baf7495d | ||
|
|
6cf7ac3e30 | ||
|
|
07d2f11081 | ||
|
|
c6fbba6a4d | ||
|
|
b701a50b51 | ||
|
|
5865d79de7 | ||
|
|
41a42da774 | ||
|
|
6d7ef3ae45 | ||
|
|
2c31a930a3 | ||
|
|
c7e2a71cd5 | ||
|
|
4237b9220b | ||
|
|
cabe29c7c7 | ||
|
|
80b7202db6 | ||
|
|
8c6d16a6f0 | ||
|
|
dd1f2989bc | ||
|
|
540ec1b816 | ||
|
|
728ef98d6e | ||
|
|
329d0cc945 | ||
|
|
0234c85be5 | ||
|
|
eca1e06711 | ||
|
|
2611343cbe | ||
|
|
8bde881e2c | ||
|
|
6fdc63504a | ||
|
|
23dbce4f59 |
@@ -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/
|
||||
|
||||
@@ -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/>
|
||||
|
||||
|
||||
@@ -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) |
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/)
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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>
|
||||

|
||||
|
||||
</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>
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
|
||||

|
||||
|
||||
</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>
|
||||
|
||||

|
||||
|
||||
</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
|
||||

|
||||
|
||||
<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': [...],
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|-------------|---------------------------------|------------------------------|
|
||||
|
||||
@@ -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")
|
||||
```
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"}
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -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?
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -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: {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,13 @@
|
||||
import Home from "./page"
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Home />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
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 |
|
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,
|
||||
};
|
||||
};
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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,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'
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||