Compare commits

..

11 Commits

Author SHA1 Message Date
Dev Khant 5327d6e50d version bump -> 0.1.89 (#2533) 2025-04-11 13:40:47 +05:30
Dev Khant 15a3e20371 Store user_id in vectordb (#2466) 2025-04-11 13:37:34 +05:30
Dev Khant 19d7beef43 Add support for Langchain VectorStores (#2518) 2025-04-11 13:37:18 +05:30
Vir Kothari 8b789adb15 Add YT assistant chrome extension (#2485) 2025-04-10 22:14:57 +05:30
Antaripa Saha fd065fe9cc Personal Study Buddy (#2531) 2025-04-10 08:12:39 -07:00
Antaripa Saha b5127f7c62 personal assistant (#2530) 2025-04-10 20:24:18 +05:30
Dev-Khant 37d9fed690 doc: update agno 2025-04-10 15:49:36 +05:30
Dev Khant 31861e9acb Doc: Add agno example (#2529) 2025-04-10 15:46:46 +05:30
Dev Khant 07462adc9a Formatting (#2526) 2025-04-10 11:42:25 +05:30
Dev Khant 616313b8b5 Add async support (#2492)
Co-authored-by: Deshraj Yadav <deshrajdry@gmail.com>
2025-04-10 11:16:44 +05:30
Dev Khant 44f2490667 Doc: modify 2 examples to show OpenAIResponses API (#2525) 2025-04-10 10:24:10 +05:30
43 changed files with 5373 additions and 53 deletions
@@ -0,0 +1,85 @@
---
title: LangChain
---
Mem0 supports LangChain as a provider for vector store integration. LangChain provides a unified interface to various vector databases, making it easy to integrate different vector store providers through a consistent API.
<Note>
When using LangChain as your vector store provider, you must set the collection name to "mem0". This is a required configuration for proper integration with Mem0.
</Note>
## Usage
<CodeGroup>
```python Python
import os
from mem0 import Memory
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# Initialize a LangChain vector store
embeddings = OpenAIEmbeddings()
vector_store = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings,
collection_name="mem0" # Required collection name
)
# Pass the initialized vector store to the config
config = {
"vector_store": {
"provider": "langchain",
"config": {
"client": vector_store
}
}
}
m = Memory.from_config(config)
messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about a thriller movies? They can be quite engaging."},
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
]
m.add(messages, user_id="alice", metadata={"category": "movies"})
```
</CodeGroup>
## Supported LangChain Vector Stores
LangChain supports a wide range of vector store providers, including:
- Chroma
- FAISS
- Pinecone
- Weaviate
- Milvus
- Qdrant
- And many more
You can use any of these vector store instances directly in your configuration. For a complete and up-to-date list of available providers, refer to the [LangChain Vector Stores documentation](https://python.langchain.com/docs/integrations/vectorstores).
## Limitations
When using LangChain as a vector store provider, there are some limitations to be aware of:
1. **Bulk Operations**: The `get_all` and `delete_all` operations are not supported when using LangChain as the vector store provider. This is because LangChain's vector store interface doesn't provide standardized methods for these bulk operations across all providers.
2. **Provider-Specific Features**: Some advanced features may not be available depending on the specific vector store implementation you're using through LangChain.
## Provider-Specific Configuration
When using LangChain as a vector store provider, you'll need to:
1. Set the appropriate environment variables for your chosen vector store provider
2. Import and initialize the specific vector store class you want to use
3. Pass the initialized vector store instance to the config
<Note>
Make sure to install the necessary LangChain packages and any provider-specific dependencies.
</Note>
## Config
All available parameters for the `langchain` vector store config are present in [Master List of All Params in Config](../config).
+1
View File
@@ -29,6 +29,7 @@ See the list of supported vector databases below.
<Card title="Vertex AI" href="/components/vectordbs/dbs/vertex_ai"></Card>
<Card title="Weaviate" href="/components/vectordbs/dbs/weaviate"></Card>
<Card title="FAISS" href="/components/vectordbs/dbs/faiss"></Card>
<Card title="LangChain" href="/components/vectordbs/dbs/langchain"></Card>
</CardGroup>
## Usage
+7 -3
View File
@@ -73,6 +73,7 @@
"group": "Features",
"icon": "wrench",
"pages": [
"open-source/features/async-memory",
"features/openai_compatibility",
"features/custom-fact-extraction-prompt",
"features/custom-update-memory-prompt",
@@ -139,7 +140,8 @@
"components/vectordbs/dbs/supabase",
"components/vectordbs/dbs/vertex_ai",
"components/vectordbs/dbs/weaviate",
"components/vectordbs/dbs/faiss"
"components/vectordbs/dbs/faiss",
"components/vectordbs/dbs/langchain"
]
}
]
@@ -202,7 +204,8 @@
"examples/mem0-agentic-tool",
"examples/openai-inbuilt-tools",
"examples/mem0-openai-voice-demo",
"examples/email_processing"
"examples/email_processing",
"examples/youtube-assistant"
]
}
]
@@ -227,7 +230,8 @@
"integrations/mcp-server",
"integrations/livekit",
"integrations/elevenlabs",
"integrations/pipecat"
"integrations/pipecat",
"integrations/agno"
]
}
]
+4
View File
@@ -47,6 +47,10 @@ Explore how **Mem0** can power real-world applications and bring personalized, i
Add **long-term memory** to ChatGPT, Claude, or Perplexity via the **Mem0 Chrome Extension** — personalize your AI chats anywhere.
</Card>
<Card title="YouTube Assistant" icon="puzzle-piece" href="/examples/youtube-assistant">
Integrate **Mem0** into **YouTube's** native UI, providing personalized responses with video context.
</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>
+11 -11
View File
@@ -20,6 +20,7 @@ pip install openai mem0ai
Below is the complete code to create and interact with a Personalized AI Tutor using Mem0:
```python
import os
from openai import OpenAI
from mem0 import Memory
@@ -54,22 +55,21 @@ class PersonalAITutor:
:param question: The question to ask the AI.
:param user_id: Optional user ID to associate with the memory.
"""
# Start a streaming chat completion request to the AI
stream = self.client.chat.completions.create(
model="gpt-4",
stream=True,
messages=[
{"role": "system", "content": "You are a personal AI Tutor."},
{"role": "user", "content": question}
]
# Start a streaming response request to the AI
response = self.client.responses.create(
model="gpt-4o",
instructions="You are a personal AI Tutor.",
input=question,
stream=True
)
# Store the question in memory
self.memory.add(question, user_id=user_id, metadata={"app_id": self.app_id})
# Print the response from the AI in real-time
for chunk in stream:
if chunk.choices[0].delta.content is not None:
print(chunk.choices[0].delta.content, end="")
for event in response:
if event.type == "response.output_text.delta":
print(event.delta, end="")
def get_memories(self, user_id=None):
"""
+14 -9
View File
@@ -63,18 +63,23 @@ class PersonalTravelAssistant:
def ask_question(self, question, user_id):
# Fetch previous related memories
previous_memories = self.search_memories(question, user_id=user_id)
prompt = question
if previous_memories:
prompt = f"User input: {question}\n Previous memories: {previous_memories}"
self.messages.append({"role": "user", "content": prompt})
# Generate response using GPT-4o
response = self.client.chat.completions.create(
# Build the prompt
system_message = "You are a personal AI Assistant."
if previous_memories:
prompt = f"{system_message}\n\nUser input: {question}\nPrevious memories: {', '.join(previous_memories)}"
else:
prompt = f"{system_message}\n\nUser input: {question}"
# Generate response using Responses API
response = self.client.responses.create(
model="gpt-4o",
messages=self.messages
input=prompt
)
answer = response.choices[0].message.content
self.messages.append({"role": "assistant", "content": answer})
# Extract answer from the response
answer = response.output[0].content[0].text
# Store the question in memory
self.memory.add(question, user_id=user_id)
+56
View File
@@ -0,0 +1,56 @@
---
title: YouTube Assistant Extension
---
Enhance your YouTube experience with Mem0's **YouTube Assistant**, a Chrome extension that brings AI-powered chat directly to your YouTube videos. Get instant, personalized answers about video content while leveraging your own knowledge and memories - all without leaving the page.
## Features
- **Contextual AI Chat**: Ask questions about videos you're watching
- **Seamless Integration**: Chat interface sits alongside YouTube's native UI
- **Memory Integration**: Personalized responses based on your knowledge through Mem0
- **Real-Time Memory**: Memories are updated in real-time based on your interactions
## Installation
This extension is not available on the Chrome Web Store yet. You can install it manually using below method:
### Manual Installation (Developer Mode)
1. **Download the Extension**: Clone or download the extension files from the [Mem0 GitHub repository](https://github.com/mem0ai/mem0/tree/main/examples).
2. **Build**: Run `npm install` followed by `npm run build` to install the dependencies and build the extension.
3. **Access Chrome Extensions**: Open Google Chrome and navigate to `chrome://extensions`.
4. **Enable Developer Mode**: Toggle the "Developer mode" switch in the top right corner.
5. **Load Unpacked Extension**: Click "Load unpacked" and select the directory containing the extension files.
6. **Confirm Installation**: The Mem0 YouTube Assistant Extension should now appear in your Chrome toolbar.
## Setup
1. **Configure API Settings**: Click the extension icon and enter your OpenAI API key (required to use the extension)
2. **Customize Settings**: Configure additional settings such as model, temperature, and memory settings
3. **Navigate to YouTube**: Start using the assistant on any YouTube video
4. **Memories**: Enter your Mem0 API key to enable personalized responses, and feed initial memories from settings
## Demo Video
<video
autoPlay
muted
loop
playsInline
width="700"
height="400"
src="https://github.com/user-attachments/assets/c0334ccd-311b-4dd7-8034-ef88204fc751"
></video>
## Example Prompts
- "Can you summarize the main points of this video?"
- "Explain the concept they just mentioned"
- "How does this relate to what I already know?"
- "What are some practical applications of this topic related to my work?"
## Privacy and Data Security
Your API keys are stored locally in your browser. Your messages are sent to the Mem0 API for extracting and retrieving memories. Mem0 is committed to ensuring your data's privacy and security.
+173
View File
@@ -0,0 +1,173 @@
---
title: Agno
---
Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [Agno](https://github.com/agno-ai/agno), a Python framework for building autonomous agents. This integration enables Agno agents to access persistent memory across conversations, enhancing context retention and personalization.
## Overview
1. 🧠 Store and retrieve memories from Mem0 within Agno agents
2. 🖼️ Support for multimodal interactions (text and images)
3. 🔍 Semantic search for relevant past conversations
4. 🌐 Personalized responses based on user history
## Prerequisites
Before setting up Mem0 with Agno, ensure you have:
1. Installed the required packages:
```bash
pip install agno-ai mem0ai
```
2. Valid API keys:
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys)
- OpenAI API Key (for the agent model)
## Integration Example
The following example demonstrates how to create an Agno agent with Mem0 memory integration, including support for image processing:
```python
import base64
from pathlib import Path
from typing import Optional
from agno.agent import Agent
from agno.media import Image
from agno.models.openai import OpenAIChat
from mem0 import MemoryClient
# Initialize the Mem0 client
client = MemoryClient()
# Define the agent
agent = Agent(
name="Personal Agent",
model=OpenAIChat(id="gpt-4"),
description="You are a helpful personal agent that helps me with day to day activities."
"You can process both text and images.",
markdown=True
)
def chat_user(
user_input: Optional[str] = None,
user_id: str = "user_123",
image_path: Optional[str] = None
) -> str:
"""
Handle user input with memory integration, supporting both text and images.
Args:
user_input: The user's text input
user_id: Unique identifier for the user
image_path: Path to an image file if provided
Returns:
The agent's response as a string
"""
if image_path:
# Convert image to base64
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
# Create message objects for text and image
messages = []
if user_input:
messages.append({
"role": "user",
"content": user_input
})
messages.append({
"role": "user",
"content": {
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
}
})
# Store messages in memory
client.add(messages, user_id=user_id)
print("✅ Image and text stored in memory.")
if user_input:
# Search for relevant memories
memories = client.search(user_input, user_id=user_id)
memory_context = "\n".join(f"- {m['memory']}" for m in memories)
# Construct the prompt
prompt = f"""
You are a helpful personal assistant who helps users with their day-to-day activities and keeps track of everything.
Your task is to:
1. Analyze the given image (if present) and extract meaningful details to answer the user's question.
2. Use your past memory of the user to personalize your answer.
3. Combine the image content and memory to generate a helpful, context-aware response.
Here is what I remember about the user:
{memory_context}
User question:
{user_input}
"""
# Get response from agent
if image_path:
response = agent.run(prompt, images=[Image(filepath=Path(image_path))])
else:
response = agent.run(prompt)
# Store the interaction in memory
client.add(f"User: {user_input}\nAssistant: {response.content}", user_id=user_id)
return response.content
return "No user input or image provided."
# Example Usage
if __name__ == "__main__":
response = chat_user(
"This is the picture of what I brought with me in the trip to Bahamas",
image_path="travel_items.jpeg",
user_id="user_123"
)
print(response)
```
## Key Features
### 1. Multimodal Memory Storage
The integration supports storing both text and image data:
- **Text Storage**: Conversation history is saved in a structured format
- **Image Analysis**: Agents can analyze images and store visual information
- **Combined Context**: Memory retrieval combines both text and visual data
### 2. Personalized Agent Responses
Improve your agent's context awareness:
- **Memory Retrieval**: Semantic search finds relevant past interactions
- **User Preferences**: Personalize responses based on stored user information
- **Continuity**: Maintain conversation threads across multiple sessions
### 3. Flexible Configuration
Customize the integration to your needs:
- **User Identification**: Organize memories by user ID
- **Memory Search**: Configure search relevance and result count
- **Memory Formatting**: Support for various OpenAI message formats
## Help & Resources
- [Agno Documentation](https://docs.agno.com/introduction)
- [Mem0 Platform](https://app.mem0.ai/)
<Snippet file="get-help.mdx" />
+17
View File
@@ -286,4 +286,21 @@ Here are the available integrations for Mem0:
>
Build conversational AI agents with memory using Pipecat.
</Card>
<Card
title="Agno"
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path d="M8 4h8v12h8" stroke="currentColor" strokeWidth="2" fill="none" transform="rotate(15, 12, 12)"/>
</svg>
}
href="/integrations/agno"
>
Build autonomous agents with memory using Agno framework.
</Card>
</CardGroup>
+169
View File
@@ -0,0 +1,169 @@
---
title: Async Memory
description: 'Asynchronous memory for Mem0'
icon: "bolt"
iconType: "solid"
---
## AsyncMemory
The `AsyncMemory` class is a direct asynchronous interface to Mem0's in-process memory operations. Unlike the memory, which interacts with an API, `AsyncMemory` works directly with the underlying storage systems. This makes it ideal for applications where you want to embed Mem0 directly into your codebase.
### Initialization
To use `AsyncMemory`, import it from the `mem0.memory` module:
```python Python
import asyncio
from mem0 import AsyncMemory
# Initialize with default configuration
memory = AsyncMemory()
# Or initialize with custom configuration
from mem0.configs.base import MemoryConfig
custom_config = MemoryConfig(
# Your custom configuration here
)
memory = AsyncMemory(config=custom_config)
```
### Key Features
1. **Non-blocking Operations** - All memory operations use `asyncio` to avoid blocking the event loop
2. **Concurrent Processing** - Parallel execution of vector store and graph operations
3. **Efficient Resource Utilization** - Better handling of I/O bound operations
4. **Compatible with Async Frameworks** - Seamless integration with FastAPI, aiohttp, and other async frameworks
### Methods
All methods in `AsyncMemory` have the same parameters as the synchronous `Memory` class but are designed to be used with `async/await`.
#### Create memories
Add a new memory asynchronously:
```python Python
await memory.add(
messages=[
{"role": "user", "content": "I'm travelling to SF"},
{"role": "assistant", "content": "That's great to hear!"}
],
user_id="alice"
)
```
#### Retrieve memories
Retrieve memories related to a query:
```python Python
await memory.search(
query="Where am I travelling?",
user_id="alice"
)
```
#### List memories
List all memories for a `user_id`, `agent_id`, or `run_id`:
```python Python
await memory.get_all(user_id="alice")
```
#### Get specific memory
Retrieve a specific memory by its ID:
```python Python
await memory.get(memory_id="memory-id-here")
```
#### Update memory
Update an existing memory by ID:
```python Python
await memory.update(
memory_id="memory-id-here",
data="I'm travelling to Seattle"
)
```
#### Delete memory
Delete a specific memory by ID:
```python Python
await memory.delete(memory_id="memory-id-here")
```
#### Delete all memories
Delete all memories for a specific user, agent, or run:
```python Python
await memory.delete_all(user_id="alice")
```
Note: At least one filter (user_id, agent_id, or run_id) is required when using delete_all.
#### Memory History
Get the history of changes for a specific memory:
```python Python
await memory.history(memory_id="memory-id-here")
```
### Example: Concurrent Usage with Other APIs
`AsyncMemory` can be effectively combined with other async operations. Here's an example showing how to use it alongside OpenAI API calls in separate threads:
```python Python
import asyncio
from openai import AsyncOpenAI
from mem0 import AsyncMemory
async_openai_client = AsyncOpenAI()
async_memory = AsyncMemory()
async def chat_with_memories(message: str, user_id: str = "default_user") -> str:
# Retrieve relevant memories
search_result = await async_memory.search(query=message, user_id=user_id, limit=3)
relevant_memories = search_result["results"]
memories_str = "\n".join(f"- {entry['memory']}" for entry in relevant_memories)
# Generate Assistant response
system_prompt = f"You are a helpful AI. Answer the question based on query and memories.\nUser Memories:\n{memories_str}"
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": message}]
response = await async_openai_client.chat.completions.create(model="gpt-4o-mini", messages=messages)
assistant_response = response.choices[0].message.content
# Create new memories from the conversation
messages.append({"role": "assistant", "content": assistant_response})
await async_memory.add(messages, user_id=user_id)
return assistant_response
async def async_main():
print("Chat with AI (type 'exit' to quit)")
while True:
user_input = input("You: ").strip()
if user_input.lower() == 'exit':
print("Goodbye!")
break
response = await chat_with_memories(user_input)
print(f"AI: {response}")
def main():
asyncio.run(async_main())
if __name__ == "__main__":
main()
```
If you have any questions or need further assistance, please don't hesitate to reach out:
<Snippet file="get-help.mdx" />
+10
View File
@@ -28,6 +28,16 @@ from mem0 import Memory
os.environ["OPENAI_API_KEY"] = "your-api-key"
m = Memory()
```
</Tab>
<Tab title="Async">
```python
import os
from mem0 import AsyncMemory
os.environ["OPENAI_API_KEY"] = "your-api-key"
m = AsyncMemory()
```
</Tab>
<Tab title="Advanced">
+101
View File
@@ -0,0 +1,101 @@
"""
Create your personal AI Assistant powered by memory that supports both text and images and remembers your preferences
In order to run this file, you need to set up your Mem0 API at Mem0 platform and also need a OpenAI API key.
export OPENAI_API_KEY="your_openai_api_key"
export MEM0_API_KEY="your_mem0_api_key"
"""
import base64
from pathlib import Path
from agno.agent import Agent
from agno.media import Image
from agno.models.openai import OpenAIChat
from mem0 import MemoryClient
# Initialize the Mem0 client
client = MemoryClient()
# Define the agent
agent = Agent(
name="Personal Agent",
model=OpenAIChat(id="gpt-4o"),
description="You are a helpful personal agent that helps me with day to day activities."
"You can process both text and images.",
markdown=True
)
# Function to handle user input with memory integration with support for images
def chat_user(user_input: str = None, user_id: str = "user_123", image_path: str = None):
if image_path:
with open(image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
# First: the text message
text_msg = {
"role": "user",
"content": user_input
}
# Second: the image message
image_msg = {
"role": "user",
"content": {
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
}
}
}
# Send both as separate message objects
client.add([text_msg, image_msg], user_id=user_id, output_format='v1.1')
print("✅ Image uploaded and stored in memory.")
if user_input:
memories = client.search(user_input, user_id=user_id)
memory_context = "\n".join(f"- {m['memory']}" for m in memories)
prompt = f"""
You are a helpful personal assistant who helps user with his day-to-day activities and keep track of everything.
Your task is to:
1. Analyze the given image (if present) and extract meaningful details to answer the user's question.
2. Use your past memory of the user to personalize your answer.
3. Combine the image content and memory to generate a helpful, context-aware response.
Here is what remember about the user:
{memory_context}
User question:
{user_input}
"""
if image_path:
response = agent.run(prompt, images=[Image(filepath=Path(image_path))])
else:
response = agent.run(prompt)
client.add(f"User: {user_input}\nAssistant: {response.content}", user_id=user_id)
return response.content
return "No user input or image provided."
# Example Usage
user_id = "user_123"
print(chat_user("What did I ask you to remind me about?", user_id))
# # OUTPUT: You asked me to remind you to call your mom tomorrow. 📞
#
print(chat_user("When is my test?", user_id=user_id))
# OUTPUT: Your pilot's test is on your birthday, which is in five days. You're turning 25!
# Good luck with your preparations, and remember to take some time to relax amidst the studying.
print(chat_user("This is the picture of what I brought with me in the trip to Bahamas",
image_path="travel_items.jpeg", # this will be added to Mem0 memory
user_id=user_id))
print(chat_user("hey can you quickly tell me if brought my sunglasses to my trip, not able to find",
user_id=user_id))
# OUTPUT: Yes, you did bring your sunglasses on your trip to the Bahamas along with your laptop, face masks and other items..
# Since you can't find them now, perhaps check the pockets of jackets you wore or in your luggage compartments.
+84
View File
@@ -0,0 +1,84 @@
"""
Create your personal AI Study Buddy that remembers what you’ve studied (and where you struggled),
helps with spaced repetition and topic review, personalizes responses using your past interactions.
Supports both text and PDF/image inputs.
In order to run this file, you need to set up your Mem0 API at Mem0 platform and also need a OpenAI API key.
export OPENAI_API_KEY="your_openai_api_key"
export MEM0_API_KEY="your_mem0_api_key"
"""
import asyncio
from mem0 import MemoryClient
from agents import Agent, Runner
client = MemoryClient()
# Define your study buddy agent
study_agent = Agent(
name="StudyBuddy",
instructions="""You are a helpful study coach. You:
- Track what the user has studied before
- Identify topics the user has struggled with (e.g., "I'm confused", "this is hard")
- Help with spaced repetition by suggesting topics to revisit based on last review time
- Personalize answers using stored memories
- Summarize PDFs or notes the user uploads""")
# Upload and store PDF to Mem0
def upload_pdf(pdf_url: str, user_id: str):
pdf_message = {
"role": "user",
"content": {
"type": "pdf_url",
"pdf_url": {"url": pdf_url}
}
}
client.add([pdf_message], user_id=user_id)
print("✅ PDF uploaded and processed into memory.")
# Main interaction loop with your personal study buddy
async def study_buddy(user_id: str, topic: str, user_input: str):
memories = client.search(f"{topic}", user_id=user_id)
memory_context = "n".join(f"- {m['memory']}" for m in memories)
prompt = f"""
You are helping the user study the topic: {topic}.
Here are past memories from previous sessions:
{memory_context}
Now respond to the user's new question or comment:
{user_input}
"""
result = await Runner.run(study_agent, prompt)
response = result.final_output
client.add([
{"role": "user", "content": f'''Topic: {topic}nUser: {user_input}nnStudy Assistant: {response}'''}
], user_id=user_id, metadata={"topic": topic})
return response
# Example usage
async def main():
user_id = "Ajay"
pdf_url = "https://pages.physics.ua.edu/staff/fabi/ph101/classnotes/8RotD101.pdf"
upload_pdf(pdf_url, user_id) # Upload a relevant lecture PDF to memory
topic = "Lagrangian Mechanics"
# Demonstrate tracking previously learned topics
print(await study_buddy(user_id, topic, "Can you remind me of what we discussed about generalized coordinates?"))
# Demonstrate weakness detection
print(await study_buddy(user_id, topic, "I still don’t get what frequency domain really means."))
# Demonstrate spaced repetition prompting
topic = "Momentum Conservation"
print(await study_buddy(user_id, topic, "I think we covered this last week. Is it time to review momentum conservation again?"))
if __name__ == "__main__":
asyncio.run(main())
+4
View File
@@ -0,0 +1,4 @@
node_modules
.env*
dist
package-lock.json
+88
View File
@@ -0,0 +1,88 @@
# Mem0 Assistant Chrome Extension
A powerful Chrome extension that combines AI chat with your personal knowledge base through mem0. Get instant, personalized answers about video content while leveraging your own knowledge and memories - all without leaving the page.
## Development
1. Install dependencies:
```bash
npm install
```
2. Start development mode:
```bash
npm run watch
```
3. Build for production:
```bash
npm run build
```
## Features
- AI-powered chat interface directly in YouTube
- Memory capabilities powered by Mem0
- Dark mode support
- Customizable options
## Permissions
- activeTab: For accessing the current tab
- storage: For saving user preferences
- scripting: For injecting content scripts
## Host Permissions
- youtube.com
- openai.com
- mem0.ai
## Features
- **Contextual AI Chat**: Ask questions about videos you're watching
- **Seamless Integration**: Chat interface sits alongside YouTube's native UI
- **OpenAI-Powered**: Uses GPT models for intelligent responses
- **Customizable**: Configure model settings, appearance, and behavior
- **Future mem0 Integration**: Personalized responses based on your knowledge (coming soon)
## Installation
### From Source (Developer Mode)
1. Download or clone this repository
2. Open Chrome and navigate to `chrome://extensions/`
3. Enable "Developer mode" (toggle in the top-right corner)
4. Click "Load unpacked" and select the extension directory
5. The extension should now be installed and visible in your toolbar
### Setup
1. Click the extension icon in your toolbar
2. Enter your OpenAI API key (required to use the extension)
3. Configure additional settings if desired
4. Navigate to YouTube to start using the assistant
## Usage
1. Visit any YouTube video
2. Click the AI assistant icon in the corner of the page to open the chat interface
3. Ask questions about the video content
4. The AI will respond with contextual information
### Example Prompts
- "Can you summarize the main points of this video?"
- "What is the speaker explaining at 5:23?"
- "Explain the concept they just mentioned"
- "How does this relate to [topic I'm learning about]?"
- "What are some practical applications of what's being discussed?"
- **API Settings**: Change model, adjust tokens, modify temperature
- **Interface Settings**: Control where and how the chat appears
- **Behavior Settings**: Configure auto-context extraction
## Privacy & Data
- Your API keys are stored locally in your browser
- Video context and transcript is processed locally and only sent to OpenAI when you ask questions
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,45 @@
{
"manifest_version": 3,
"name": "YouTube Assistant powered by Mem0",
"version": "1.0",
"description": "An AI-powered YouTube assistant with memory capabilities from Mem0",
"permissions": [
"activeTab",
"storage",
"scripting"
],
"host_permissions": [
"https://*.youtube.com/*",
"https://*.openai.com/*",
"https://*.mem0.ai/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'",
"sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self'"
},
"action": {
"default_popup": "public/popup.html"
},
"options_page": "public/options.html",
"content_scripts": [
{
"matches": ["https://*.youtube.com/*"],
"js": ["dist/content.bundle.js"],
"css": ["styles/content.css"]
}
],
"background": {
"service_worker": "src/background.js"
},
"web_accessible_resources": [
{
"resources": [
"assets/*",
"dist/*",
"styles/*",
"node_modules/mem0ai/dist/*"
],
"matches": ["https://*.youtube.com/*"]
}
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "mem0-assistant",
"version": "1.0.0",
"description": "A Chrome extension that integrates AI chat functionality directly into YouTube and other sites. Get instant answers about video content without leaving the page.",
"main": "background.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"watch": "webpack --config webpack.config.js --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"babel-loader": "^9.1.2",
"css-loader": "^7.1.2",
"style-loader": "^4.0.0",
"webpack": "^5.85.0",
"webpack-cli": "^5.1.1",
"youtube-transcript": "^1.0.6"
},
"dependencies": {
"mem0ai": "^2.1.15"
}
}
@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YouTube Assistant powered by Mem0</title>
<link rel="stylesheet" href="../styles/options.css">
</head>
<body>
<div class="main-content">
<header>
<div class="title-container">
<h1>YouTube Assistant</h1>
<div class="branding-container">
<span class="powered-by">powered by</span>
<a href="https://mem0.ai" target="_blank">
<img src="../assets/dark.svg" alt="Mem0 Logo" class="logo-img">
</a>
</div>
</div>
<div class="description">
Configure your YouTube Assistant preferences.
</div>
</header>
<div id="status-container"></div>
<div class="section">
<h2>Model Settings</h2>
<div class="form-group">
<label for="model">OpenAI Model</label>
<select id="model">
<option value="o3">o3</option>
<option value="o1">o1</option>
<option value="o1-mini">o1-mini</option>
<option value="o1-pro">o1-pro</option>
<option value="gpt-4o">GPT-4o</option>
<option value="gpt-4o-mini">GPT-4o mini</option>
</select>
<div class="description" style="margin-top: 8px; font-size: 13px">
Choose the OpenAI model to use depending on your needs.
</div>
</div>
<div class="form-group">
<label for="max-tokens">Maximum Response Length</label>
<input
type="number"
id="max-tokens"
min="50"
max="4000"
value="2000"
/>
<div class="description" style="margin-top: 8px; font-size: 13px">
Maximum number of tokens in the AI's response. Higher values allow
for longer responses but may increase processing time.
</div>
</div>
<div class="form-group">
<label for="temperature">Response Creativity</label>
<input
type="range"
id="temperature"
min="0"
max="1"
step="0.1"
value="0.7"
/>
<div
id="temperature-value"
style="display: inline-block; margin-left: 10px"
>
0.7
</div>
<div class="description" style="margin-top: 8px; font-size: 13px">
Controls response randomness. Lower values (0.1-0.3) are more
focused and deterministic, higher values (0.7-0.9) are more creative
and diverse.
</div>
</div>
</div>
<div class="section">
<h2>Create Memories</h2>
<div class="description">
Add information about yourself that you want the AI to remember. This
information will be used to provide more personalized responses.
</div>
<div class="form-group">
<label for="memory-input">Your Information</label>
<textarea
id="memory-input"
class="memory-input"
placeholder="Enter information about yourself that you want the AI to remember..."
></textarea>
</div>
<div class="actions">
<button id="add-memory" class="primary">
<span class="button-text">Add Memory</span>
</button>
</div>
<div id="memory-result" class="memory-result"></div>
</div>
<div class="actions">
<button id="reset-defaults" class="secondary-button">
Reset to Defaults
</button>
<button id="save-options">Save Changes</button>
</div>
</div>
<!-- Memories Sidebar -->
<div class="memories-sidebar" id="memories-sidebar">
<div class="memories-header">
<h2 class="memories-title">Your Memories</h2>
<div class="memories-actions">
<button
id="refresh-memories"
class="memory-action-btn"
title="Refresh Memories"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M23 4v6h-6"></path>
<path d="M1 20v-6h6"></path>
<path
d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"
></path>
</svg>
</button>
<button
id="delete-all-memories"
class="memory-action-btn delete"
title="Delete All Memories"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
</svg>
</button>
</div>
</div>
<div class="memories-list" id="memories-list">
<!-- Memories will be populated here -->
</div>
</div>
<!-- Edit Memory Modal -->
<div class="edit-memory-modal" id="edit-memory-modal">
<div class="edit-memory-content">
<div class="edit-memory-header">
<h3 class="edit-memory-title">Edit Memory</h3>
<button class="edit-memory-close" id="close-edit-modal">
&times;
</button>
</div>
<textarea class="edit-memory-textarea" id="edit-memory-text"></textarea>
<div class="edit-memory-actions">
<button class="memory-action-btn delete" id="delete-memory">
Delete
</button>
<button class="memory-action-btn" id="save-memory">
Save Changes
</button>
</div>
</div>
</div>
<script src="../dist/options.bundle.js"></script>
</body>
</html>
@@ -0,0 +1,165 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YouTube Assistant powered by Mem0</title>
<link rel="stylesheet" href="../styles/popup.css">
</head>
<body>
<header>
<h1>YouTube Assistant</h1>
<div class="branding-container">
<span class="powered-by">powered by</span>
<a href="https://mem0.ai" target="_blank">
<img src="../assets/dark.svg" alt="Mem0 Logo" class="logo-img">
</a>
</div>
</header>
<div class="content">
<!-- Status area -->
<div id="status-container"></div>
<!-- API key input, only shown if not set -->
<div id="api-key-section" class="api-key-section">
<label for="api-key">OpenAI API Key</label>
<div class="api-key-input-wrapper">
<input type="password" id="api-key" placeholder="sk-..." />
<button class="toggle-password" id="toggle-openai-key">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<button id="save-api-key" class="save-button">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
Save OpenAI Key
</button>
</div>
<!-- mem0 API key input -->
<div id="mem0-api-key-section" class="api-key-section">
<label for="mem0-api-key">Mem0 API Key</label>
<div class="api-key-input-wrapper">
<input
type="password"
id="mem0-api-key"
placeholder="Enter your mem0 API key"
/>
<button class="toggle-password" id="toggle-mem0-key">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<div class="api-key-actions">
<p>Get your API key from <a href="https://mem0.ai" target="_blank" class="get-key-link">mem0.ai</a> to integrate memory features in the chat.</p>
<button id="save-mem0-api-key" class="save-button">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
Save Mem0 Key
</button>
</div>
</div>
<!-- Action buttons -->
<div class="actions">
<button id="toggle-chat">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
></path>
</svg>
Chat
</button>
<button id="open-options">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="3"></circle>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
></path>
</svg>
Settings
</button>
</div>
<!-- Future mem0 integration status -->
<div class="mem0-status">
<p>
Mem0 integration:
<span id="mem0-status-text">Not configured</span>
</p>
</div>
</div>
<script src="../src/popup.js"></script>
</body>
</html>
@@ -0,0 +1,255 @@
// Background script to handle API calls to OpenAI and manage extension state
// Configuration (will be stored in sync storage eventually)
let config = {
apiKey: "", // Will be set by user in options
mem0ApiKey: "", // Will be set by user in options
model: "gpt-4",
maxTokens: 2000,
temperature: 0.7,
enabledSites: ["youtube.com"],
};
// Track if config is loaded
let isConfigLoaded = false;
// Initialize configuration from storage
chrome.storage.sync.get(
["apiKey", "mem0ApiKey", "model", "maxTokens", "temperature", "enabledSites"],
(result) => {
if (result.apiKey) config.apiKey = result.apiKey;
if (result.mem0ApiKey) config.mem0ApiKey = result.mem0ApiKey;
if (result.model) config.model = result.model;
if (result.maxTokens) config.maxTokens = result.maxTokens;
if (result.temperature) config.temperature = result.temperature;
if (result.enabledSites) config.enabledSites = result.enabledSites;
isConfigLoaded = true;
}
);
// Listen for messages from content script or popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// Handle different message types
switch (request.action) {
case "sendChatRequest":
sendChatRequest(request.messages, request.model || config.model)
.then((response) => sendResponse(response))
.catch((error) => sendResponse({ error: error.message }));
return true; // Required for async response
case "saveConfig":
saveConfig(request.config)
.then(() => sendResponse({ success: true }))
.catch((error) => sendResponse({ error: error.message }));
return true;
case "getConfig":
// If config isn't loaded yet, load it first
if (!isConfigLoaded) {
chrome.storage.sync.get(
[
"apiKey",
"mem0ApiKey",
"model",
"maxTokens",
"temperature",
"enabledSites",
],
(result) => {
if (result.apiKey) config.apiKey = result.apiKey;
if (result.mem0ApiKey) config.mem0ApiKey = result.mem0ApiKey;
if (result.model) config.model = result.model;
if (result.maxTokens) config.maxTokens = result.maxTokens;
if (result.temperature) config.temperature = result.temperature;
if (result.enabledSites) config.enabledSites = result.enabledSites;
isConfigLoaded = true;
sendResponse({ config });
}
);
return true;
}
sendResponse({ config });
return false;
case "openOptions":
// Open options page
chrome.runtime.openOptionsPage(() => {
if (chrome.runtime.lastError) {
console.error(
"Error opening options page:",
chrome.runtime.lastError
);
// Fallback: Try to open directly in a new tab
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
}
sendResponse({ success: true });
});
return true;
case "toggleChat":
// Forward the toggle request to the active tab
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs
.sendMessage(tabs[0].id, { action: "toggleChat" })
.then((response) => sendResponse(response))
.catch((error) => sendResponse({ error: error.message }));
} else {
sendResponse({ error: "No active tab found" });
}
});
return true;
}
});
// Handle extension icon click - toggle chat visibility
chrome.action.onClicked.addListener((tab) => {
chrome.tabs
.sendMessage(tab.id, { action: "toggleChat" })
.catch((error) => console.error("Error toggling chat:", error));
});
// Save configuration to sync storage
async function saveConfig(newConfig) {
// Validate API key if provided
if (newConfig.apiKey) {
try {
const isValid = await validateApiKey(newConfig.apiKey);
if (!isValid) {
throw new Error("Invalid API key");
}
} catch (error) {
throw new Error(`API key validation failed: ${error.message}`);
}
}
// Update local config
config = { ...config, ...newConfig };
// Save to sync storage
return chrome.storage.sync.set(newConfig);
}
// Validate OpenAI API key with a simple request
async function validateApiKey(apiKey) {
try {
const response = await fetch("https://api.openai.com/v1/models", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`API returned ${response.status}`);
}
return true;
} catch (error) {
console.error("API key validation error:", error);
return false;
}
}
// Send a chat request to OpenAI API
async function sendChatRequest(messages, model) {
// Check if API key is set
if (!config.apiKey) {
return {
error:
"API key not configured. Please set your OpenAI API key in the extension options.",
};
}
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: model || config.model,
messages: messages.map((msg) => ({
role: msg.role,
content: msg.content,
})),
max_tokens: config.maxTokens,
temperature: config.temperature,
stream: true, // Enable streaming
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.error?.message || `API returned ${response.status}`
);
}
// Create a ReadableStream from the response
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
// Process the stream
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Decode the chunk and add to buffer
buffer += decoder.decode(value, { stream: true });
// Process complete lines
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // Keep the last incomplete line in the buffer
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
// Stream complete
return { done: true };
}
try {
const parsed = JSON.parse(data);
if (parsed.choices[0].delta.content) {
// Send the chunk to the content script
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, {
action: "streamChunk",
chunk: parsed.choices[0].delta.content,
});
}
});
}
} catch (e) {
console.error("Error parsing chunk:", e);
}
}
}
}
return { done: true };
} catch (error) {
console.error("Error sending chat request:", error);
return { error: error.message };
}
}
// Future: Add mem0 integration functions here
// When ready, replace with actual implementation
function mem0Integration() {
// Placeholder for future mem0 integration
return {
getUserMemories: async (userId) => {
return { memories: [] };
},
saveMemory: async (userId, memory) => {
return { success: true };
},
};
}
+657
View File
@@ -0,0 +1,657 @@
// Main content script that injects the AI chat into YouTube
import { YoutubeTranscript } from "youtube-transcript";
import { MemoryClient } from "mem0ai";
// Configuration
const config = {
apiEndpoint: "https://api.openai.com/v1/chat/completions",
model: "gpt-4o",
chatPosition: "right", // Where to display the chat panel
autoExtract: true, // Automatically extract video context
mem0ApiKey: "", // Will be set through extension options
};
// Initialize Mem0AI - will be initialized properly when API key is available
let mem0client = null;
let mem0Initializing = false;
// Function to initialize Mem0AI with API key from storage
async function initializeMem0AI() {
if (mem0Initializing) return; // Prevent multiple simultaneous initialization attempts
mem0Initializing = true;
try {
// Get API key from storage
const items = await chrome.storage.sync.get(["mem0ApiKey"]);
if (items.mem0ApiKey) {
try {
// Create new client instance with v2.1.11 configuration
mem0client = new MemoryClient({
apiKey: items.mem0ApiKey,
projectId: "youtube-assistant", // Add a project ID for organization
isExtension: true,
});
// Set up custom instructions for the YouTube educational assistant
await mem0client.updateProject({
custom_instructions: `Your task: Create memories for a YouTube AI assistant. Focus on capturing:
1. User's Knowledge & Experience:
- Direct statements about their skills, knowledge, or experience
- Their level of expertise in specific areas
- Technologies, frameworks, or tools they work with
- Their learning journey or background
2. User's Interests & Goals:
- What they're trying to learn or understand (user messages may include the video title)
- Their specific questions or areas of confusion
- Their learning objectives or career goals
- Topics they want to explore further
3. Personal Context:
- Their current role or position
- Their learning style or preferences
- Their experience level in the video's topic
- Any challenges or difficulties they're facing
4. Video Engagement:
- Their reactions to the content
- Points they agree or disagree with
- Areas they want to discuss further
- Connections they make to other topics
For each message:
- Extract both explicit statements and implicit knowledge
- Capture both video-related and personal context
- Note any relationships between user's knowledge and video content
Remember: The goal is to build a comprehensive understanding of both the user's knowledge and their learning journey through YouTube.`,
});
return true;
} catch (error) {
console.error("Error initializing Mem0AI:", error);
return false;
}
} else {
console.log("No Mem0AI API key found in storage");
return false;
}
} catch (error) {
console.error("Error accessing storage:", error);
return false;
} finally {
mem0Initializing = false;
}
}
// Global state
let chatState = {
messages: [],
isVisible: false,
isLoading: false,
videoContext: null,
transcript: null, // Add transcript to state
userMemories: null, // Will store retrieved memories
currentStreamingMessage: null, // Track the current streaming message
};
// Function to extract video ID from YouTube URL
function getYouTubeVideoId(url) {
const urlObj = new URL(url);
const searchParams = new URLSearchParams(urlObj.search);
return searchParams.get("v");
}
// Function to fetch and log transcript
async function fetchAndLogTranscript() {
try {
// Check if we're on a YouTube video page
if (
window.location.hostname.includes("youtube.com") &&
window.location.pathname.includes("/watch")
) {
const videoId = getYouTubeVideoId(window.location.href);
if (videoId) {
// Fetch transcript using youtube-transcript package
const transcript = await YoutubeTranscript.fetchTranscript(videoId);
// Decode HTML entities in transcript text
const decodedTranscript = transcript.map((entry) => ({
...entry,
text: entry.text
.replace(/&amp;#39;/g, "'")
.replace(/&amp;quot;/g, '"')
.replace(/&amp;lt;/g, "<")
.replace(/&amp;gt;/g, ">")
.replace(/&amp;amp;/g, "&"),
}));
// Store transcript in state
chatState.transcript = decodedTranscript;
} else {
return;
}
}
} catch (error) {
console.error("Error fetching transcript:", error);
chatState.transcript = null;
}
}
// Initialize when the DOM is fully loaded
document.addEventListener("DOMContentLoaded", async () => {
init();
fetchAndLogTranscript();
await initializeMem0AI(); // Initialize Mem0AI
});
// Also attempt to initialize on window load to handle YouTube's SPA behavior
window.addEventListener("load", async () => {
init();
fetchAndLogTranscript();
await initializeMem0AI(); // Initialize Mem0AI
});
// Add another listener for YouTube's navigation events
window.addEventListener("yt-navigate-finish", () => {
init();
fetchAndLogTranscript();
});
// Main initialization function
function init() {
// Check if we're on a YouTube page
if (
!window.location.hostname.includes("youtube.com") ||
!window.location.pathname.includes("/watch")
) {
return;
}
// Give YouTube's DOM a moment to settle
setTimeout(() => {
// Only inject if not already present
if (!document.getElementById("ai-chat-assistant-container")) {
injectChatInterface();
setupEventListeners();
extractVideoContext();
}
}, 1500);
}
// Extract context from the current YouTube video
function extractVideoContext() {
if (!config.autoExtract) return;
try {
const videoTitle =
document.querySelector(
"h1.title.style-scope.ytd-video-primary-info-renderer"
)?.textContent ||
document.querySelector("h1.title")?.textContent ||
"Unknown Video";
const channelName =
document.querySelector("ytd-channel-name yt-formatted-string")
?.textContent ||
document.querySelector("ytd-channel-name")?.textContent ||
"Unknown Channel";
// Video ID from URL
const videoId = new URLSearchParams(window.location.search).get("v");
// Update state with basic video context first
chatState.videoContext = {
title: videoTitle,
channel: channelName,
videoId: videoId,
url: window.location.href,
};
} catch (error) {
console.error("Error extracting video context:", error);
chatState.videoContext = {
title: "Error extracting video information",
url: window.location.href,
};
}
}
// Inject the chat interface into the YouTube page
function injectChatInterface() {
// Create main container
const container = document.createElement("div");
container.id = "ai-chat-assistant-container";
container.className = "ai-chat-container";
// Set up basic HTML structure
container.innerHTML = `
<div class="ai-chat-header">
<div class="ai-chat-tabs">
<button class="ai-chat-tab active" data-tab="chat">Chat</button>
<button class="ai-chat-tab" data-tab="memories">Memories</button>
</div>
<div class="ai-chat-controls">
<button id="ai-chat-minimize" class="ai-chat-btn" title="Minimize">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<button id="ai-chat-close" class="ai-chat-btn" title="Close">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="ai-chat-body">
<div id="ai-chat-content" class="ai-chat-content">
<div id="ai-chat-messages" class="ai-chat-messages"></div>
<div class="ai-chat-input-container">
<textarea id="ai-chat-input" placeholder="Ask about this video..."></textarea>
<button id="ai-chat-send" class="ai-chat-send-btn" title="Send message">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
</div>
<div id="ai-chat-memories" class="ai-chat-memories" style="display: none;">
<div class="memories-header">
<div class="memories-title">
Manage memories <a href="#" id="manage-memories-link" title="Open options page">here <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg></a>
</div>
<button id="refresh-memories" class="ai-chat-btn" title="Refresh memories">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M23 4v6h-6"></path>
<path d="M1 20v-6h6"></path>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
</button>
</div>
<div id="memories-list" class="memories-list"></div>
</div>
</div>
`;
// Append to body
document.body.appendChild(container);
// Add welcome message
addMessage(
"assistant",
"Hello! I can help answer questions about this video. What would you like to know?"
);
}
// Set up event listeners for the chat interface
function setupEventListeners() {
// Tab switching
const tabs = document.querySelectorAll(".ai-chat-tab");
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
// Update active tab
tabs.forEach((t) => t.classList.remove("active"));
tab.classList.add("active");
// Show corresponding content
const tabName = tab.dataset.tab;
document.getElementById("ai-chat-content").style.display =
tabName === "chat" ? "flex" : "none";
document.getElementById("ai-chat-memories").style.display =
tabName === "memories" ? "flex" : "none";
// Load memories if switching to memories tab
if (tabName === "memories") {
loadMemories();
}
});
});
// Refresh memories button
document
.getElementById("refresh-memories")
?.addEventListener("click", loadMemories);
// Toggle chat visibility
document.getElementById("ai-chat-toggle")?.addEventListener("click", () => {
const container = document.getElementById("ai-chat-assistant-container");
chatState.isVisible = !chatState.isVisible;
if (chatState.isVisible) {
container.classList.add("visible");
} else {
container.classList.remove("visible");
}
});
// Close button
document.getElementById("ai-chat-close")?.addEventListener("click", () => {
const container = document.getElementById("ai-chat-assistant-container");
container.classList.remove("visible");
chatState.isVisible = false;
});
// Minimize button
document.getElementById("ai-chat-minimize")?.addEventListener("click", () => {
const container = document.getElementById("ai-chat-assistant-container");
container.classList.toggle("minimized");
});
// Send message on button click
document
.getElementById("ai-chat-send")
?.addEventListener("click", sendMessage);
// Send message on Enter key (but allow Shift+Enter for new lines)
document.getElementById("ai-chat-input")?.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Add click handler for manage memories link
document
.getElementById("manage-memories-link")
.addEventListener("click", (e) => {
e.preventDefault();
chrome.runtime.sendMessage({ action: "openOptions" }, (response) => {
if (chrome.runtime.lastError) {
console.error("Error opening options:", chrome.runtime.lastError);
// Fallback: Try to open directly in a new tab
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
}
});
});
}
// Add a message to the chat
function addMessage(role, text, isStreaming = false) {
const messagesContainer = document.getElementById("ai-chat-messages");
if (!messagesContainer) return;
const messageElement = document.createElement("div");
messageElement.className = `ai-chat-message ${role}`;
// Enhanced markdown-like formatting
let formattedText = text
// Code blocks
.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>")
// Inline code
.replace(/`([^`]+)`/g, "<code>$1</code>")
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
// Bold text
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
// Italic text
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
// Lists
.replace(/^\s*[-*]\s+(.+)$/gm, "<li>$1</li>")
.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>")
// Line breaks
.replace(/\n/g, "<br>");
messageElement.innerHTML = formattedText;
messagesContainer.appendChild(messageElement);
// Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Add to messages array if not streaming
if (!isStreaming) {
chatState.messages.push({ role, content: text });
}
return messageElement;
}
// Format streaming text with markdown
function formatStreamingText(text) {
return text
// Code blocks
.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>")
// Inline code
.replace(/`([^`]+)`/g, "<code>$1</code>")
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
// Bold text
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
// Italic text
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
// Lists
.replace(/^\s*[-*]\s+(.+)$/gm, "<li>$1</li>")
.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>")
// Line breaks
.replace(/\n/g, "<br>");
}
// Send a message to the AI
async function sendMessage() {
const inputElement = document.getElementById("ai-chat-input");
if (!inputElement) return;
const userMessage = inputElement.value.trim();
if (!userMessage) return;
// Clear input
inputElement.value = "";
// Add user message to chat
addMessage("user", userMessage);
// Show loading indicator
chatState.isLoading = true;
const loadingMessage = document.createElement("div");
loadingMessage.className = "ai-chat-message assistant loading";
loadingMessage.textContent = "Thinking...";
document.getElementById("ai-chat-messages").appendChild(loadingMessage);
try {
// If mem0client is available, store the message as a memory and search for relevant memories
if (mem0client) {
try {
// Store the message as a memory
await mem0client.add(
[
{
role: "user",
content: `${userMessage}\n\nVideo title: ${chatState.videoContext?.title}`,
},
],
{
user_id: "youtube-assistant-mem0", // Required parameter
metadata: {
videoId: chatState.videoContext?.videoId || "",
videoTitle: chatState.videoContext?.title || "",
},
}
);
// Search for relevant memories
const searchResults = await mem0client.search(userMessage, {
user_id: "youtube-assistant-mem0", // Required parameter
limit: 5,
});
// Store the retrieved memories
chatState.userMemories = searchResults || null;
} catch (memoryError) {
console.error("Error with Mem0AI operations:", memoryError);
// Continue with the chat process even if memory operations fail
}
}
// Prepare messages with context (now includes memories if available)
const contextualizedMessages = prepareMessagesWithContext();
// Remove loading message
document.getElementById("ai-chat-messages").removeChild(loadingMessage);
// Create a new message element for streaming
chatState.currentStreamingMessage = addMessage("assistant", "", true);
// Send to background script to handle API call
chrome.runtime.sendMessage(
{
action: "sendChatRequest",
messages: contextualizedMessages,
model: config.model,
},
(response) => {
chatState.isLoading = false;
if (response.error) {
addMessage("system", `Error: ${response.error}`);
}
}
);
} catch (error) {
// Remove loading indicator
document.getElementById("ai-chat-messages").removeChild(loadingMessage);
chatState.isLoading = false;
// Show error
addMessage("system", `Error: ${error.message}`);
}
}
// Prepare messages with added context
function prepareMessagesWithContext() {
const messages = [...chatState.messages];
// If we have video context, add it as system message at the beginning
if (chatState.videoContext) {
let transcriptSection = "";
// Add transcript if available
if (chatState.transcript) {
// Format transcript into a readable string
const formattedTranscript = chatState.transcript
.map((entry) => `${entry.text}`)
.join("\n");
transcriptSection = `\n\nTranscript:\n${formattedTranscript}`;
}
// Add user memories if available
let userMemoriesSection = "";
if (chatState.userMemories && chatState.userMemories.length > 0) {
const formattedMemories = chatState.userMemories
.map((memory) => `${memory.memory}`)
.join("\n");
userMemoriesSection = `\n\nUser Memories:\n${formattedMemories}\n\n`;
}
const systemContent = `You are an AI assistant helping with a YouTube video. Here's the context:
Title: ${chatState.videoContext.title}
Channel: ${chatState.videoContext.channel}
URL: ${chatState.videoContext.url}
${
userMemoriesSection
? `Use the user memories below to personalize your response based on their past interactions and interests. These memories represent relevant past conversations and information about the user.
${userMemoriesSection}
`
: ""
}
Please provide helpful, relevant information based on the video's content.
${
transcriptSection
? `"Use the transcript below to provide accurate answers about the video. Ignore if the transcript doesn't make sense."
${transcriptSection}
`
: "Since the transcript is not available, focus on general questions about the topic and use the video title for context. If asked about specific parts of the video content, politely explain that the video doesn't have a transcript."
}
Be concise and helpful in your responses.
`;
messages.unshift({
role: "system",
content: systemContent,
});
}
return messages;
}
// Listen for commands from the background script or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "toggleChat") {
const container = document.getElementById("ai-chat-assistant-container");
chatState.isVisible = !chatState.isVisible;
if (chatState.isVisible) {
container.classList.add("visible");
} else {
container.classList.remove("visible");
}
sendResponse({ success: true });
} else if (message.action === "streamChunk") {
// Handle streaming chunks
if (chatState.currentStreamingMessage) {
const currentContent = chatState.currentStreamingMessage.innerHTML;
chatState.currentStreamingMessage.innerHTML = formatStreamingText(currentContent + message.chunk);
// Scroll to bottom
const messagesContainer = document.getElementById("ai-chat-messages");
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
});
// Load memories from mem0
async function loadMemories() {
try {
const memoriesContainer = document.getElementById("memories-list");
memoriesContainer.innerHTML =
'<div class="loading">Loading memories...</div>';
// If client isn't initialized, try to initialize it
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) {
memoriesContainer.innerHTML =
'<div class="error">Please set your Mem0 API key in the extension options.</div>';
return;
}
}
const response = await mem0client.getAll({
user_id: "youtube-assistant-mem0",
page: 1,
page_size: 50,
});
if (response && response.results) {
memoriesContainer.innerHTML = "";
response.results.forEach((memory) => {
const memoryElement = document.createElement("div");
memoryElement.className = "memory-item";
memoryElement.textContent = memory.memory;
memoriesContainer.appendChild(memoryElement);
});
if (response.results.length === 0) {
memoriesContainer.innerHTML =
'<div class="no-memories">No memories found</div>';
}
} else {
memoriesContainer.innerHTML =
'<div class="no-memories">No memories found</div>';
}
} catch (error) {
console.error("Error loading memories:", error);
document.getElementById("memories-list").innerHTML =
'<div class="error">Error loading memories. Please try again.</div>';
}
}
+452
View File
@@ -0,0 +1,452 @@
// Options page functionality for AI Chat Assistant
import { MemoryClient } from "mem0ai";
// Default configuration
const defaultConfig = {
model: "gpt-4o",
maxTokens: 2000,
temperature: 0.7,
enabledSites: ["youtube.com"],
};
// Initialize Mem0AI client
let mem0client = null;
// Initialize when the DOM is fully loaded
document.addEventListener("DOMContentLoaded", init);
// Initialize options page
async function init() {
// Set up event listeners
document
.getElementById("save-options")
.addEventListener("click", saveOptions);
document
.getElementById("reset-defaults")
.addEventListener("click", resetToDefaults);
document.getElementById("add-memory").addEventListener("click", addMemory);
// Set up slider value display
const temperatureSlider = document.getElementById("temperature");
const temperatureValue = document.getElementById("temperature-value");
temperatureSlider.addEventListener("input", () => {
temperatureValue.textContent = temperatureSlider.value;
});
// Set up memories sidebar functionality
document
.getElementById("refresh-memories")
.addEventListener("click", fetchMemories);
document
.getElementById("delete-all-memories")
.addEventListener("click", deleteAllMemories);
document
.getElementById("close-edit-modal")
.addEventListener("click", closeEditModal);
document.getElementById("save-memory").addEventListener("click", saveMemory);
document
.getElementById("delete-memory")
.addEventListener("click", deleteMemory);
// Load current configuration
await loadConfig();
// Initialize Mem0AI and load memories
await initializeMem0AI();
await fetchMemories();
}
// Initialize Mem0AI with API key from storage
async function initializeMem0AI() {
try {
const response = await chrome.runtime.sendMessage({ action: "getConfig" });
const mem0ApiKey = response.config.mem0ApiKey;
if (!mem0ApiKey) {
showMemoriesError("Please configure your Mem0 API key in the popup");
return false;
}
mem0client = new MemoryClient({
apiKey: mem0ApiKey,
projectId: "youtube-assistant",
isExtension: true,
});
return true;
} catch (error) {
console.error("Error initializing Mem0AI:", error);
showMemoriesError("Failed to initialize Mem0AI");
return false;
}
}
// Load configuration from storage
async function loadConfig() {
try {
const response = await chrome.runtime.sendMessage({ action: "getConfig" });
const config = response.config;
// Update form fields with current values
if (config.model) {
document.getElementById("model").value = config.model;
}
if (config.maxTokens) {
document.getElementById("max-tokens").value = config.maxTokens;
}
if (config.temperature !== undefined) {
const temperatureSlider = document.getElementById("temperature");
temperatureSlider.value = config.temperature;
document.getElementById("temperature-value").textContent =
config.temperature;
}
} catch (error) {
showStatus(`Error loading configuration: ${error.message}`, "error");
}
}
// Save options to storage
async function saveOptions() {
// Get values from form
const model = document.getElementById("model").value;
const maxTokens = parseInt(document.getElementById("max-tokens").value);
const temperature = parseFloat(document.getElementById("temperature").value);
// Validate inputs
if (maxTokens < 50 || maxTokens > 4000) {
showStatus("Maximum tokens must be between 50 and 4000", "error");
return;
}
if (temperature < 0 || temperature > 1) {
showStatus("Temperature must be between 0 and 1", "error");
return;
}
// Prepare config object
const config = {
model,
maxTokens,
temperature,
};
// Show loading status
showStatus("Saving options...", "warning");
try {
// Send to background script for saving
const response = await chrome.runtime.sendMessage({
action: "saveConfig",
config,
});
if (response.error) {
showStatus(`Error: ${response.error}`, "error");
} else {
showStatus("Options saved successfully", "success");
loadConfig(); // Refresh the UI with the latest saved values
}
} catch (error) {
showStatus(`Error: ${error.message}`, "error");
}
}
// Reset options to defaults
function resetToDefaults() {
if (
confirm(
"Are you sure you want to reset all options to their default values?"
)
) {
// Set form fields to default values
document.getElementById("model").value = defaultConfig.model;
document.getElementById("max-tokens").value = defaultConfig.maxTokens;
const temperatureSlider = document.getElementById("temperature");
temperatureSlider.value = defaultConfig.temperature;
document.getElementById("temperature-value").textContent =
defaultConfig.temperature;
showStatus("Restored default values. Click Save to apply.", "warning");
}
}
// Memories functionality
let currentMemory = null;
async function fetchMemories() {
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
const memories = await mem0client.getAll({
user_id: "youtube-assistant-mem0",
page: 1,
page_size: 50,
});
displayMemories(memories.results);
} catch (error) {
console.error("Error fetching memories:", error);
showMemoriesError("Failed to load memories");
}
}
function displayMemories(memories) {
const memoriesList = document.getElementById("memories-list");
memoriesList.innerHTML = "";
if (memories.length === 0) {
memoriesList.innerHTML = `
<div class="memory-item">
<div class="memory-content">No memories found. Your memories will appear here.</div>
</div>
`;
return;
}
memories.forEach((memory) => {
const memoryElement = document.createElement("div");
memoryElement.className = "memory-item";
memoryElement.innerHTML = `
<div class="memory-content">${memory.memory}</div>
<div class="memory-meta">Last updated: ${new Date(
memory.updated_at
).toLocaleString()}</div>
<div class="memory-actions">
<button class="memory-action-btn edit" data-id="${
memory.id
}">Edit</button>
<button class="memory-action-btn delete" data-id="${
memory.id
}">Delete</button>
</div>
`;
// Add event listeners
memoryElement
.querySelector(".edit")
.addEventListener("click", () => editMemory(memory));
memoryElement
.querySelector(".delete")
.addEventListener("click", () => deleteMemory(memory.id));
memoriesList.appendChild(memoryElement);
});
}
function showMemoriesError(message) {
const memoriesList = document.getElementById("memories-list");
memoriesList.innerHTML = `
<div class="memory-item">
<div class="memory-content">${message}</div>
</div>
`;
}
async function deleteAllMemories() {
if (
!confirm(
"Are you sure you want to delete all memories? This action cannot be undone."
)
) {
return;
}
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
await mem0client.deleteAll({
user_id: "youtube-assistant-mem0",
});
showStatus("All memories deleted successfully", "success");
await fetchMemories();
} catch (error) {
console.error("Error deleting memories:", error);
showStatus("Failed to delete memories", "error");
}
}
function editMemory(memory) {
currentMemory = memory;
const modal = document.getElementById("edit-memory-modal");
const textarea = document.getElementById("edit-memory-text");
textarea.value = memory.memory;
modal.classList.add("open");
}
function closeEditModal() {
const modal = document.getElementById("edit-memory-modal");
modal.classList.remove("open");
currentMemory = null;
}
async function saveMemory() {
if (!currentMemory) return;
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
const textarea = document.getElementById("edit-memory-text");
const updatedMemory = textarea.value.trim();
if (!updatedMemory) {
showStatus("Memory cannot be empty", "error");
return;
}
await mem0client.update(currentMemory.id, updatedMemory);
showStatus("Memory updated successfully", "success");
closeEditModal();
await fetchMemories();
} catch (error) {
console.error("Error updating memory:", error);
showStatus("Failed to update memory", "error");
}
}
async function deleteMemory(memoryId) {
if (
!confirm(
"Are you sure you want to delete this memory? This action cannot be undone."
)
) {
return;
}
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
await mem0client.delete(memoryId);
showStatus("Memory deleted successfully", "success");
await fetchMemories();
} catch (error) {
console.error("Error deleting memory:", error);
showStatus("Failed to delete memory", "error");
}
}
// Show status message
function showStatus(message, type = "info") {
const statusContainer = document.getElementById("status-container");
// Clear previous status
statusContainer.innerHTML = "";
// Create status element
const statusElement = document.createElement("div");
statusElement.className = `status ${type}`;
statusElement.textContent = message;
// Add to container
statusContainer.appendChild(statusElement);
// Auto-clear success messages after 3 seconds
if (type === "success") {
setTimeout(() => {
statusElement.style.opacity = "0";
setTimeout(() => {
if (statusContainer.contains(statusElement)) {
statusContainer.removeChild(statusElement);
}
}, 300);
}, 3000);
}
}
// Add memory to Mem0
async function addMemory() {
const memoryInput = document.getElementById("memory-input");
const addButton = document.getElementById("add-memory");
const memoryResult = document.getElementById("memory-result");
const buttonText = addButton.querySelector(".button-text");
const content = memoryInput.value.trim();
if (!content) {
showMemoryResult(
"Please enter some information to add as a memory",
"error"
);
return;
}
// Show loading state
addButton.disabled = true;
buttonText.textContent = "Adding...";
addButton.innerHTML =
'<div class="loading-spinner"></div><span class="button-text">Adding...</span>';
memoryResult.style.display = "none";
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
const result = await mem0client.add(
[
{
role: "user",
content: content,
},
],
{
user_id: "youtube-assistant-mem0",
}
);
// Show success message with number of memories added
showMemoryResult(
`Added ${result.length || 0} new ${
result.length === 1 ? "memory" : "memories"
}`,
"success"
);
// Clear the input
memoryInput.value = "";
// Refresh the memories list
await fetchMemories();
} catch (error) {
showMemoryResult(`Error adding memory: ${error.message}`, "error");
} finally {
// Reset button state
addButton.disabled = false;
buttonText.textContent = "Add Memory";
addButton.innerHTML = '<span class="button-text">Add Memory</span>';
}
}
// Show memory result message
function showMemoryResult(message, type) {
const memoryResult = document.getElementById("memory-result");
memoryResult.textContent = message;
memoryResult.className = `memory-result ${type}`;
memoryResult.style.display = "block";
// Auto-clear success messages after 3 seconds
if (type === "success") {
setTimeout(() => {
memoryResult.style.opacity = "0";
setTimeout(() => {
memoryResult.style.display = "none";
memoryResult.style.opacity = "1";
}, 300);
}, 3000);
}
}
+241
View File
@@ -0,0 +1,241 @@
// Popup functionality for AI Chat Assistant
document.addEventListener("DOMContentLoaded", init);
// Initialize popup
async function init() {
try {
// Set up event listeners
document
.getElementById("toggle-chat")
.addEventListener("click", toggleChat);
document
.getElementById("open-options")
.addEventListener("click", openOptions);
document
.getElementById("save-api-key")
.addEventListener("click", saveApiKey);
document
.getElementById("save-mem0-api-key")
.addEventListener("click", saveMem0ApiKey);
// Set up password toggle listeners
document
.getElementById("toggle-openai-key")
.addEventListener("click", () => togglePasswordVisibility("api-key"));
document
.getElementById("toggle-mem0-key")
.addEventListener("click", () =>
togglePasswordVisibility("mem0-api-key")
);
// Load current configuration and wait for it to complete
await loadConfig();
} catch (error) {
console.error("Initialization error:", error);
showStatus("Error initializing popup", "error");
}
}
// Toggle chat visibility in the active tab
function toggleChat() {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
// First check if we can inject the content script
chrome.scripting
.executeScript({
target: { tabId: tabs[0].id },
files: ["dist/content.bundle.js"],
})
.then(() => {
// Now try to toggle the chat
chrome.tabs
.sendMessage(tabs[0].id, { action: "toggleChat" })
.then((response) => {
if (response && response.error) {
console.error("Error toggling chat:", response.error);
showStatus(
"Chat interface not available on this page",
"warning"
);
} else {
// Close the popup after successful toggle
window.close();
}
})
.catch((error) => {
console.error("Error toggling chat:", error);
showStatus(
"Chat interface not available on this page",
"warning"
);
});
})
.catch((error) => {
console.error("Error injecting content script:", error);
showStatus("Cannot inject chat interface on this page", "error");
});
}
});
}
// Open options page
function openOptions() {
// Send message to background script to handle opening options
chrome.runtime.sendMessage({ action: "openOptions" }, (response) => {
if (chrome.runtime.lastError) {
console.error("Error opening options:", chrome.runtime.lastError);
// Direct fallback if communication with background script fails
try {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
} catch (err) {
console.error("Fallback failed:", err);
// Last resort
window.open(chrome.runtime.getURL("options.html"), "_blank");
}
}
});
}
// Toggle password visibility
function togglePasswordVisibility(inputId) {
const input = document.getElementById(inputId);
const type = input.type === "password" ? "text" : "password";
input.type = type;
// Update the eye icon
const button = input.nextElementSibling;
const icon = button.querySelector(".icon");
if (type === "text") {
icon.innerHTML =
'<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>';
} else {
icon.innerHTML =
'<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>';
}
}
// Save API key to storage
async function saveApiKey() {
const apiKeyInput = document.getElementById("api-key");
const apiKey = apiKeyInput.value.trim();
// Show loading status
showStatus("Saving API key...", "warning");
try {
// Send to background script for validation and saving
const response = await chrome.runtime.sendMessage({
action: "saveConfig",
config: { apiKey },
});
if (response.error) {
showStatus(`Error: ${response.error}`, "error");
} else {
showStatus("API key saved successfully", "success");
loadConfig(); // Refresh the UI
}
} catch (error) {
showStatus(`Error: ${error.message}`, "error");
}
}
// Save mem0 API key to storage
async function saveMem0ApiKey() {
const apiKeyInput = document.getElementById("mem0-api-key");
const apiKey = apiKeyInput.value.trim();
// Show loading status
showStatus("Saving Mem0 API key...", "warning");
try {
// Send to background script for saving
const response = await chrome.runtime.sendMessage({
action: "saveConfig",
config: { mem0ApiKey: apiKey },
});
if (response.error) {
showStatus(`Error: ${response.error}`, "error");
} else {
showStatus("Mem0 API key saved successfully", "success");
loadConfig(); // Refresh the UI
}
} catch (error) {
showStatus(`Error: ${error.message}`, "error");
}
}
// Load configuration from storage
async function loadConfig() {
try {
// Add a small delay to ensure background script is ready
await new Promise((resolve) => setTimeout(resolve, 100));
const response = await chrome.runtime.sendMessage({ action: "getConfig" });
const config = response.config || {};
// Update OpenAI API key field
const apiKeyInput = document.getElementById("api-key");
if (config.apiKey) {
apiKeyInput.value = config.apiKey;
apiKeyInput.type = "password"; // Ensure it's hidden by default
document.getElementById("api-key-section").style.display = "block";
} else {
apiKeyInput.value = "";
document.getElementById("api-key-section").style.display = "block";
showStatus("Please set your OpenAI API key", "warning");
}
// Update mem0 API key field
const mem0ApiKeyInput = document.getElementById("mem0-api-key");
if (config.mem0ApiKey) {
mem0ApiKeyInput.value = config.mem0ApiKey;
mem0ApiKeyInput.type = "password"; // Ensure it's hidden by default
document.getElementById("mem0-api-key-section").style.display = "block";
document.getElementById("mem0-status-text").textContent = "Connected";
document.getElementById("mem0-status-text").style.color =
"var(--success-color)";
} else {
mem0ApiKeyInput.value = "";
document.getElementById("mem0-api-key-section").style.display = "block";
document.getElementById("mem0-status-text").textContent =
"Not configured";
document.getElementById("mem0-status-text").style.color =
"var(--warning-color)";
}
} catch (error) {
console.error("Error loading configuration:", error);
showStatus(`Error loading configuration: ${error.message}`, "error");
}
}
// Show status message
function showStatus(message, type = "info") {
const statusContainer = document.getElementById("status-container");
// Clear previous status
statusContainer.innerHTML = "";
// Create status element
const statusElement = document.createElement("div");
statusElement.className = `status ${type}`;
statusElement.textContent = message;
// Add to container
statusContainer.appendChild(statusElement);
// Auto-clear success messages after 3 seconds
if (type === "success") {
setTimeout(() => {
statusElement.style.opacity = "0";
setTimeout(() => {
if (statusContainer.contains(statusElement)) {
statusContainer.removeChild(statusElement);
}
}, 300);
}, 3000);
}
}
@@ -0,0 +1,492 @@
/* Styles for the AI Chat Assistant */
/* Modern Dark Theme with Blue Accents */
:root {
--chat-dark-bg: #1a1a1a;
--chat-darker-bg: #121212;
--chat-light-text: #f1f1f1;
--chat-blue-accent: #3d84f7;
--chat-blue-hover: #2d74e7;
--chat-blue-light: rgba(61, 132, 247, 0.15);
--chat-error: #ff4a4a;
--chat-border-radius: 12px;
--chat-message-radius: 12px;
--chat-transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Main container */
#ai-chat-assistant-container {
position: fixed;
right: 20px;
bottom: 20px;
width: 380px;
height: 550px;
background-color: var(--chat-dark-bg);
border-radius: var(--chat-border-radius);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
z-index: 9999;
overflow: hidden;
transition: var(--chat-transition);
opacity: 0;
transform: translateY(20px) scale(0.98);
pointer-events: none;
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif;
border: 1px solid rgba(255, 255, 255, 0.08);
}
/* When visible */
#ai-chat-assistant-container.visible {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: all;
}
/* When minimized */
#ai-chat-assistant-container.minimized {
height: 50px;
}
#ai-chat-assistant-container.minimized .ai-chat-body {
display: none;
}
/* Header */
.ai-chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: var(--chat-darker-bg);
color: var(--chat-light-text);
border-top-left-radius: var(--chat-border-radius);
border-top-right-radius: var(--chat-border-radius);
cursor: move;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.ai-chat-title {
font-weight: 500;
font-size: 15px;
display: flex;
align-items: center;
gap: 6px;
}
.ai-chat-title::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background-color: var(--chat-blue-accent);
border-radius: 50%;
box-shadow: 0 0 10px var(--chat-blue-accent);
}
.ai-chat-controls {
display: flex;
gap: 8px;
}
.ai-chat-btn {
background: none;
border: none;
color: var(--chat-light-text);
font-size: 18px;
cursor: pointer;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: var(--chat-transition);
}
.ai-chat-btn:hover {
background-color: rgba(255, 255, 255, 0.08);
}
/* Body */
.ai-chat-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--chat-dark-bg);
}
/* Messages container */
.ai-chat-messages {
flex: 1;
overflow-y: auto;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.ai-chat-messages::-webkit-scrollbar {
width: 5px;
}
.ai-chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.ai-chat-messages::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
/* Individual message */
.ai-chat-message {
max-width: 85%;
padding: 12px 16px;
border-radius: var(--chat-message-radius);
line-height: 1.5;
position: relative;
font-size: 14px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
animation: message-fade-in 0.3s ease;
word-break: break-word;
}
@keyframes message-fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* User message */
.ai-chat-message.user {
align-self: flex-end;
background-color: var(--chat-blue-accent);
color: white;
border-bottom-right-radius: 4px;
}
/* Assistant message */
.ai-chat-message.assistant {
align-self: flex-start;
background-color: rgba(255, 255, 255, 0.08);
color: var(--chat-light-text);
border-bottom-left-radius: 4px;
}
/* System message */
.ai-chat-message.system {
align-self: center;
background-color: rgba(255, 76, 76, 0.1);
color: var(--chat-error);
max-width: 90%;
font-size: 13px;
border-radius: 8px;
border: 1px solid rgba(255, 76, 76, 0.2);
}
/* Loading animation */
.ai-chat-message.loading {
background-color: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.7);
}
.ai-chat-message.loading:after {
content: "...";
animation: thinking 1.5s infinite;
}
@keyframes thinking {
0% { content: "."; }
33% { content: ".."; }
66% { content: "..."; }
}
/* Input area */
.ai-chat-input-container {
display: flex;
padding: 12px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
background-color: var(--chat-darker-bg);
}
#ai-chat-input {
flex: 1;
border: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(255, 255, 255, 0.05);
color: var(--chat-light-text);
border-radius: 20px;
padding: 10px 16px;
font-size: 14px;
resize: none;
max-height: 100px;
outline: none;
font-family: inherit;
transition: var(--chat-transition);
}
#ai-chat-input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
#ai-chat-input:focus {
border-color: var(--chat-blue-accent);
background-color: rgba(255, 255, 255, 0.07);
box-shadow: 0 0 0 1px rgba(61, 132, 247, 0.1);
}
.ai-chat-send-btn {
background: none;
border: none;
color: var(--chat-blue-accent);
cursor: pointer;
padding: 8px;
margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: var(--chat-transition);
}
.ai-chat-send-btn:hover {
background-color: var(--chat-blue-light);
transform: scale(1.05);
}
/* Toggle button */
.ai-chat-toggle {
position: fixed;
right: 20px;
bottom: 20px;
width: 56px;
height: 56px;
border-radius: 50%;
background-color: var(--chat-blue-accent);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 15px rgba(61, 132, 247, 0.35);
z-index: 9998;
transition: var(--chat-transition);
border: none;
}
.ai-chat-toggle:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(61, 132, 247, 0.45);
}
#ai-chat-assistant-container.visible + .ai-chat-toggle {
transform: scale(0);
opacity: 0;
}
/* Code formatting */
.ai-chat-message pre {
background-color: rgba(0, 0, 0, 0.3);
padding: 10px;
border-radius: 6px;
overflow-x: auto;
margin: 10px 0;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.ai-chat-message code {
font-family: 'Cascadia Code', 'Fira Code', 'Source Code Pro', monospace;
font-size: 12px;
}
.ai-chat-message.user code {
background-color: rgba(255, 255, 255, 0.2);
padding: 2px 5px;
border-radius: 3px;
}
.ai-chat-message.assistant code {
background-color: rgba(0, 0, 0, 0.3);
padding: 2px 5px;
border-radius: 3px;
color: #e2e2e2;
}
/* Links */
.ai-chat-message a {
color: var(--chat-blue-accent);
text-decoration: none;
border-bottom: 1px dotted rgba(61, 132, 247, 0.5);
transition: var(--chat-transition);
}
.ai-chat-message a:hover {
border-bottom: 1px solid var(--chat-blue-accent);
}
.ai-chat-message.user a {
color: white;
border-bottom: 1px dotted rgba(255, 255, 255, 0.5);
}
.ai-chat-message.user a:hover {
border-bottom: 1px solid white;
}
/* Responsive adjustments */
@media (max-width: 768px) {
#ai-chat-assistant-container {
width: calc(100% - 20px);
height: 60vh;
right: 10px;
bottom: 10px;
}
.ai-chat-toggle {
right: 10px;
bottom: 10px;
}
}
/* Tab styles */
.ai-chat-tabs {
display: flex;
gap: 10px;
margin-right: 10px;
}
.ai-chat-tab {
background: none;
border: none;
color: var(--chat-light-text);
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
border-radius: 4px;
transition: var(--chat-transition);
}
.ai-chat-tab:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.ai-chat-tab.active {
background-color: var(--chat-blue-accent);
color: white;
}
/* Content area */
.ai-chat-content {
display: flex;
flex-direction: column;
height: 100%;
}
/* Memories tab styles */
.ai-chat-memories {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--chat-dark-bg);
}
.memories-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
padding-left: 16px;
padding-right: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.memories-title {
display: inline;
align-items: center;
font-size: 14px;
color: var(--chat-light-text);
}
.memories-title a {
color: var(--chat-blue-accent);
text-decoration: none;
font-weight: 500;
transition: var(--chat-transition);
display: inline-flex;
align-items: center;
gap: 4px;
}
.memories-title a:hover {
color: var(--chat-blue-hover);
text-decoration: underline;
}
.memories-title a svg {
vertical-align: middle;
}
.memories-title svg {
vertical-align: middle;
margin-left: 4px;
}
.memories-list {
flex: 1;
overflow-y: auto;
padding: 10px;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.memories-list::-webkit-scrollbar {
width: 5px;
}
.memories-list::-webkit-scrollbar-track {
background: transparent;
}
.memories-list::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.memory-item {
background-color: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: var(--chat-message-radius);
padding: 12px 16px;
margin-bottom: 10px;
font-size: 14px;
line-height: 1.4;
color: var(--chat-light-text);
}
.memory-item:last-child {
margin-bottom: 0;
}
.loading, .no-memories, .error, .info {
text-align: center;
padding: 20px;
font-size: 14px;
color: var(--chat-light-text);
}
.error {
color: var(--chat-error);
font-size: 14px;
}
.info {
color: var(--chat-blue-accent);
}
@@ -0,0 +1,587 @@
:root {
--dark-bg: #1a1a1a;
--darker-bg: #121212;
--section-bg: #202020;
--light-text: #f1f1f1;
--dim-text: rgba(255, 255, 255, 0.7);
--dim-text-2: rgba(255, 255, 255, 0.5);
--blue-accent: #3d84f7;
--blue-hover: #2d74e7;
--blue-light: rgba(61, 132, 247, 0.15);
--error-color: #ff4a4a;
--warning-color: #ffaa33;
--success-color: #4caf50;
--border-radius: 8px;
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: "Roboto", -apple-system, BlinkMacSystemFont, sans-serif;
margin: 0;
padding: 20px 20px 40px;
color: var(--light-text);
background-color: var(--dark-bg);
max-width: 1200px;
margin: 0 auto;
}
header {
max-width: 800px;
padding-left: 28px;
padding-top: 10px;
color: #f1f1f1;
}
h1 {
font-size: 32px;
margin: 0 0 12px 0;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.title-container {
display: flex;
align-items: center;
gap: 10px;
}
.logo-img {
height: 20px;
width: auto;
margin-left: 8px;
position: relative;
top: 1px;
}
.powered-by {
font-size: 12px;
font-weight: normal;
color: rgba(255, 255, 255, 0.6);
line-height: 1;
}
.branding-container {
display: flex;
align-items: center;
justify-content: center;
}
.description {
color: var(--dim-text);
margin-bottom: 20px;
font-size: 15px;
line-height: 1.5;
}
.section {
margin-bottom: 30px;
background: var(--section-bg);
padding: 28px;
border-radius: var(--border-radius);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
h2 {
font-size: 18px;
margin-top: 0;
margin-bottom: 15px;
color: var(--light-text);
display: flex;
align-items: center;
gap: 8px;
}
h2::before {
content: "";
display: inline-block;
width: 5px;
height: 20px;
background-color: var(--blue-accent);
border-radius: 3px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--light-text);
}
input[type="text"],
input[type="password"],
input[type="number"],
select {
width: 100%;
padding: 12px;
background-color: rgba(255, 255, 255, 0.05);
color: var(--light-text);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
font-size: 14px;
box-sizing: border-box;
transition: var(--transition);
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
select:focus {
border-color: var(--blue-accent);
outline: none;
box-shadow: 0 0 0 1px rgba(61, 132, 247, 0.2);
}
select {
appearance: none;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%207l5%205%205-5%22%20stroke%3D%22%23fff%22%20stroke-width%3D%221.5%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
}
input[type="number"] {
width: 120px;
}
input[type="checkbox"] {
margin-right: 10px;
position: relative;
width: 18px;
height: 18px;
-webkit-appearance: none;
appearance: none;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
cursor: pointer;
transition: var(--transition);
}
input[type="checkbox"]:checked {
background-color: var(--blue-accent);
border-color: var(--blue-accent);
}
input[type="checkbox"]:checked::after {
content: "";
position: absolute;
left: 5px;
top: 2px;
width: 6px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
input[type="checkbox"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-label {
display: flex;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
color: var(--light-text);
}
.checkbox-label label {
margin-bottom: 0;
margin-left: 8px;
}
button {
background-color: var(--blue-accent);
color: white;
border: none;
padding: 12px 20px;
border-radius: var(--border-radius);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
button:hover {
background-color: var(--blue-hover);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
button:active {
transform: translateY(1px);
box-shadow: none;
}
button:disabled {
background-color: rgba(255, 255, 255, 0.1);
color: var(--dim-text-2);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.status {
padding: 15px;
border-radius: var(--border-radius);
margin-top: 20px;
font-size: 14px;
animation: fade-in 0.3s ease;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.status.error {
background-color: rgba(255, 74, 74, 0.1);
color: var(--error-color);
border: 1px solid rgba(255, 74, 74, 0.2);
}
.status.success {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success-color);
border: 1px solid rgba(76, 175, 80, 0.2);
}
.status.warning {
background-color: rgba(255, 170, 51, 0.1);
color: var(--warning-color);
border: 1px solid rgba(255, 170, 51, 0.2);
}
.actions {
display: flex;
gap: 10px;
}
.secondary-button {
background-color: rgba(255, 255, 255, 0.08);
color: var(--light-text);
}
.secondary-button:hover {
background-color: rgba(255, 255, 255, 0.12);
}
.api-key-container {
display: flex;
gap: 10px;
}
.api-key-container input {
flex: 1;
}
/* Slider styles */
.slider-container {
margin-top: 12px;
display: flex;
align-items: center;
}
.slider {
-webkit-appearance: none;
flex: 1;
height: 4px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--blue-accent);
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
transition: var(--transition);
}
.slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--blue-accent);
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
transition: var(--transition);
border: none;
}
.slider::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
}
/* Add styles for memory creation section */
.memory-input {
width: 100%;
min-height: 150px;
padding: 12px;
background-color: rgba(255, 255, 255, 0.05);
color: var(--light-text);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
font-size: 14px;
box-sizing: border-box;
transition: var(--transition);
resize: vertical;
font-family: inherit;
}
.memory-input:focus {
border-color: var(--blue-accent);
outline: none;
box-shadow: 0 0 0 1px rgba(61, 132, 247, 0.2);
}
.memory-result {
margin-top: 15px;
padding: 12px;
border-radius: var(--border-radius);
font-size: 14px;
display: none;
}
.memory-result.success {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success-color);
border: 1px solid rgba(76, 175, 80, 0.2);
display: block;
}
.memory-result.error {
background-color: rgba(255, 74, 74, 0.1);
color: var(--error-color);
border: 1px solid rgba(255, 74, 74, 0.2);
display: block;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--light-text);
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Add new styles for the memories sidebar */
.memories-sidebar {
position: fixed;
top: 0;
right: 0;
width: 384px;
height: 100vh;
background: var(--section-bg);
border-left: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
.memories-sidebar.collapsed {
transform: translateX(384px);
}
.memories-header {
padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.memories-title {
font-size: 16px;
font-weight: 500;
color: var(--light-text);
}
.memories-actions {
display: flex;
gap: 8px;
}
.memories-list {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.memory-item {
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: var(--border-radius);
margin-bottom: 12px;
cursor: pointer;
transition: var(--transition);
}
.memory-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.memory-content {
font-size: 14px;
color: var(--light-text);
margin-bottom: 8px;
text-align: center;
text-wrap-style: pretty;
}
.memory-item .memory-content {
text-align: left;
}
.memory-meta {
font-size: 12px;
color: var(--dim-text);
}
.memory-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.memory-action-btn {
padding: 8px;
font-size: 12px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: var(--light-text);
border: none;
cursor: pointer;
transition: var(--transition);
}
.memory-action-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.memory-action-btn.delete:hover {
background-color: var(--error-color);
}
.edit-memory-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1100;
align-items: center;
justify-content: center;
}
.edit-memory-modal.open {
display: flex;
}
.edit-memory-content {
display: flex;
flex-direction: column;
background: var(--section-bg);
padding: 24px;
border-radius: var(--border-radius);
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.edit-memory-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.edit-memory-title {
font-size: 18px;
font-weight: 500;
color: var(--light-text);
}
.edit-memory-close {
background: none;
border: none;
color: var(--dim-text);
cursor: pointer;
padding: 4px;
font-size: 20px;
width: 30px;
}
.edit-memory-textarea {
min-height: 20px;
max-height: 70px;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
color: var(--light-text);
font-family: inherit;
margin-bottom: 16px;
resize: vertical;
}
.edit-memory-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.main-content {
margin-right: 400px;
transition: margin-right 0.3s ease;
max-width: 800px;
}
.main-content.sidebar-collapsed {
margin-right: 0;
}
#status-container {
margin-bottom: 12px;
}
@@ -0,0 +1,259 @@
:root {
--dark-bg: #1a1a1a;
--darker-bg: #121212;
--light-text: #f1f1f1;
--blue-accent: #3d84f7;
--blue-hover: #2d74e7;
--blue-light: rgba(61, 132, 247, 0.15);
--error-color: #ff4a4a;
--warning-color: #ffaa33;
--success-color: #4caf50;
--border-radius: 8px;
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: "Roboto", -apple-system, BlinkMacSystemFont, sans-serif;
width: 320px;
margin: 0;
padding: 0;
color: var(--light-text);
background-color: var(--dark-bg);
}
header {
background-color: var(--darker-bg);
color: var(--light-text);
padding: 16px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
h1 {
font-size: 18px;
margin: 0 0 8px 0;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.logo-img {
height: 16px;
width: auto;
margin-left: 8px;
position: relative;
top: 1px;
}
.powered-by {
font-size: 12px;
font-weight: normal;
color: rgba(255, 255, 255, 0.6);
line-height: 1;
}
.branding-container {
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
}
.content {
padding: 16px;
}
.status {
padding: 12px;
border-radius: var(--border-radius);
margin-bottom: 16px;
font-size: 14px;
animation: fade-in 0.3s ease;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.status.error {
background-color: rgba(255, 74, 74, 0.1);
color: var(--error-color);
border: 1px solid rgba(255, 74, 74, 0.2);
}
.status.success {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success-color);
border: 1px solid rgba(76, 175, 80, 0.2);
}
.status.warning {
background-color: rgba(255, 170, 51, 0.1);
color: var(--warning-color);
border: 1px solid rgba(255, 170, 51, 0.2);
}
button {
background-color: var(--blue-accent);
color: white;
border: none;
padding: 12px 16px;
border-radius: 6px;
cursor: pointer;
width: 100%;
font-size: 14px;
font-weight: 500;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
button:hover {
background-color: var(--blue-hover);
transform: translateY(-1px);
}
button:active {
transform: translateY(1px);
}
button:disabled {
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.4);
cursor: not-allowed;
transform: none;
}
.actions {
display: flex;
flex-direction: row;
gap: 12px;
}
.api-key-section {
margin-bottom: 20px;
position: relative;
}
.api-key-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.toggle-password {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: rgba(255, 255, 255, 0.5);
width: auto;
display: flex;
align-items: center;
justify-content: center;
}
.toggle-password:hover {
color: rgba(255, 255, 255, 0.8);
background: none;
transform: translateY(-50%);
}
.toggle-password .icon {
width: 16px;
height: 16px;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px;
padding-right: 40px;
background-color: rgba(255, 255, 255, 0.05);
color: var(--light-text);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
margin-top: 6px;
box-sizing: border-box;
transition: var(--transition);
font-size: 14px;
}
input[type="text"]:focus,
input[type="password"]:focus {
border-color: var(--blue-accent);
outline: none;
box-shadow: 0 0 0 1px rgba(61, 132, 247, 0.2);
}
input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
label {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
display: block;
margin-bottom: 4px;
}
.save-button {
margin-top: 10px;
}
.mem0-status {
margin-top: 20px;
padding: 12px;
background-color: rgba(255, 255, 255, 0.03);
border-radius: var(--border-radius);
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
.mem0-status p {
margin: 0;
}
#mem0-status-text {
color: var(--blue-accent);
font-weight: 500;
}
/* Icons */
.icon {
display: inline-block;
width: 18px;
height: 18px;
fill: currentColor;
}
.get-key-link {
color: var(--blue-accent);
text-decoration: none;
font-size: 13px;
transition: color 0.2s ease;
}
.get-key-link:hover {
color: var(--blue-accent-hover);
text-decoration: underline;
}
.get-key-link:visited {
color: var(--blue-accent);
}
@@ -0,0 +1,40 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: {
content: './src/content.js',
options: './src/options.js',
popup: './src/popup.js',
background: './src/background.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
devtool: 'source-map',
optimization: {
minimize: false
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
resolve: {
extensions: ['.js']
}
};
+1 -1
View File
@@ -3,4 +3,4 @@ import importlib.metadata
__version__ = importlib.metadata.version("mem0ai")
from mem0.client.main import AsyncMemoryClient, MemoryClient # noqa
from mem0.memory.main import Memory # noqa
from mem0.memory.main import Memory, AsyncMemory # noqa
+4 -5
View File
@@ -5,17 +5,14 @@ from functools import wraps
from typing import Any, Dict, List, Optional, Union
import httpx
import hashlib
from mem0.memory.setup import get_user_id, setup_config
from mem0.memory.telemetry import capture_client_event
logger = logging.getLogger(__name__)
warnings.filterwarnings("default", category=DeprecationWarning)
# Setup user config
setup_config()
class APIError(Exception):
"""Exception raised for errors in the API."""
@@ -78,11 +75,13 @@ class MemoryClient:
self.host = host or "https://api.mem0.ai"
self.org_id = org_id
self.project_id = project_id
self.user_id = get_user_id()
if not self.api_key:
raise ValueError("Mem0 API Key not provided. Please provide an API Key.")
# Create MD5 hash of API key for user_id
self.user_id = hashlib.md5(self.api_key.encode()).hexdigest()
self.client = httpx.Client(
base_url=self.host,
headers={"Authorization": f"Token {self.api_key}", "Mem0-User-ID": self.user_id},
+4 -1
View File
@@ -6,9 +6,12 @@ from pydantic import BaseModel, Field
from mem0.embeddings.configs import EmbedderConfig
from mem0.graphs.configs import GraphStoreConfig
from mem0.llms.configs import LlmConfig
from mem0.memory.setup import mem0_dir
from mem0.vector_stores.configs import VectorStoreConfig
# Set up the directory path
home_dir = os.path.expanduser("~")
mem0_dir = os.environ.get("MEM0_DIR") or os.path.join(home_dir, ".mem0")
class MemoryItem(BaseModel):
id: str = Field(..., description="The unique identifier for the text data")
@@ -7,7 +7,7 @@ class AzureAISearchConfig(BaseModel):
collection_name: str = Field("mem0", description="Name of the collection")
service_name: str = Field(None, description="Azure AI Search service name")
api_key: str = Field(None, description="API key for the Azure AI Search service")
embedding_model_dims: int = Field(None, description="Dimension of the embedding vector")
embedding_model_dims: int = Field(1536, description="Dimension of the embedding vector")
compression_type: Optional[str] = Field(
None, description="Type of vector compression to use. Options: 'scalar', 'binary', or None"
)
+30
View File
@@ -0,0 +1,30 @@
from typing import Any, ClassVar, Dict
from pydantic import BaseModel, Field, model_validator
class LangchainConfig(BaseModel):
try:
from langchain_community.vectorstores import VectorStore
except ImportError:
raise ImportError("The 'langchain_community' library is required. Please install it using 'pip install langchain_community'.")
VectorStore: ClassVar[type] = VectorStore
client: VectorStore = Field(description="Existing VectorStore instance")
collection_name: str = Field("mem0", description="Name of the collection to use")
@model_validator(mode="before")
@classmethod
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
allowed_fields = set(cls.model_fields.keys())
input_fields = set(values.keys())
extra_fields = input_fields - allowed_fields
if extra_fields:
raise ValueError(
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
)
return values
model_config = {
"arbitrary_types_allowed": True,
}
+1 -1
View File
@@ -165,7 +165,7 @@ class MemoryGraph:
try:
for tool_call in search_results["tool_calls"]:
if tool_call['name'] != "extract_entities":
if tool_call["name"] != "extract_entities":
continue
for item in tool_call["arguments"]["entities"]:
entity_type_map[item["entity"]] = item["entity_type"]
+779 -4
View File
@@ -1,7 +1,9 @@
import asyncio
import concurrent
import hashlib
import json
import logging
import os
import uuid
import warnings
from datetime import datetime
@@ -17,7 +19,7 @@ from mem0.configs.prompts import (
get_update_memory_messages,
)
from mem0.memory.base import MemoryBase
from mem0.memory.setup import setup_config
from mem0.memory.setup import mem0_dir, setup_config
from mem0.memory.storage import SQLiteManager
from mem0.memory.telemetry import capture_event
from mem0.memory.utils import (
@@ -61,6 +63,15 @@ class Memory(MemoryBase):
self.graph = MemoryGraph(self.config)
self.enable_graph = True
self.config.vector_store.config.collection_name = "mem0_migrations"
if self.config.vector_store.provider in ["faiss", "qdrant"]:
provider_path = f"migrations_{self.config.vector_store.provider}"
self.config.vector_store.config.path = os.path.join(mem0_dir, provider_path)
os.makedirs(self.config.vector_store.config.path, exist_ok=True)
self._telemetry_vector_store = VectorStoreFactory.create(
self.config.vector_store.provider, self.config.vector_store.config
)
capture_event("mem0.init", self)
@classmethod
@@ -196,8 +207,8 @@ class Memory(MemoryBase):
parsed_messages = parse_messages(messages)
if self.custom_fact_extraction_prompt:
system_prompt = self.custom_fact_extraction_prompt
if self.config.custom_fact_extraction_prompt:
system_prompt = self.config.custom_fact_extraction_prompt
user_prompt = f"Input:\n{parsed_messages}"
else:
system_prompt, user_prompt = get_fact_retrieval_messages(parsed_messages)
@@ -243,7 +254,7 @@ class Memory(MemoryBase):
retrieved_old_memory[idx]["id"] = str(idx)
function_calling_prompt = get_update_memory_messages(
retrieved_old_memory, new_retrieved_facts, self.custom_update_memory_prompt
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
)
try:
@@ -755,3 +766,767 @@ class Memory(MemoryBase):
def chat(self, query):
raise NotImplementedError("Chat function not implemented yet.")
class AsyncMemory(MemoryBase):
def __init__(self, config: MemoryConfig = MemoryConfig()):
self.config = config
self.embedding_model = EmbedderFactory.create(
self.config.embedder.provider,
self.config.embedder.config,
self.config.vector_store.config,
)
self.vector_store = VectorStoreFactory.create(
self.config.vector_store.provider, self.config.vector_store.config
)
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
self.db = SQLiteManager(self.config.history_db_path)
self.collection_name = self.config.vector_store.config.collection_name
self.api_version = self.config.version
self.enable_graph = False
if self.config.graph_store.config:
from mem0.memory.graph_memory import MemoryGraph
self.graph = MemoryGraph(self.config)
self.enable_graph = True
capture_event("async_mem0.init", self)
@classmethod
async def from_config(cls, config_dict: Dict[str, Any]):
try:
config = cls._process_config(config_dict)
config = MemoryConfig(**config_dict)
except ValidationError as e:
logger.error(f"Configuration validation error: {e}")
raise
return cls(config)
@staticmethod
def _process_config(config_dict: Dict[str, Any]) -> Dict[str, Any]:
if "graph_store" in config_dict:
if "vector_store" not in config_dict and "embedder" in config_dict:
config_dict["vector_store"] = {}
config_dict["vector_store"]["config"] = {}
config_dict["vector_store"]["config"]["embedding_model_dims"] = config_dict["embedder"]["config"][
"embedding_dims"
]
try:
return config_dict
except ValidationError as e:
logger.error(f"Configuration validation error: {e}")
raise
async def add(
self,
messages,
user_id=None,
agent_id=None,
run_id=None,
metadata=None,
filters=None,
infer=True,
memory_type=None,
prompt=None,
llm=None,
):
"""
Create a new memory asynchronously.
Args:
messages (str or List[Dict[str, str]]): Messages to store in the memory.
user_id (str, optional): ID of the user creating the memory. Defaults to None.
agent_id (str, optional): ID of the agent creating the memory. Defaults to None.
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
filters (dict, optional): Filters to apply to the search. Defaults to None.
infer (bool, optional): Whether to infer the memories. Defaults to True.
memory_type (str, optional): Type of memory to create. Defaults to None. By default, it creates the short term memories and long term (semantic and episodic) memories. Pass "procedural_memory" to create procedural memories.
prompt (str, optional): Prompt to use for the memory creation. Defaults to None.
llm (BaseChatModel, optional): LLM class to use for generating procedural memories. Defaults to None. Useful when user is using LangChain ChatModel.
Returns:
dict: A dictionary containing the result of the memory addition operation.
result: dict of affected events with each dict has the following key:
'memories': affected memories
'graph': affected graph memories
'memories' and 'graph' is a dict, each with following subkeys:
'add': added memory
'update': updated memory
'delete': deleted memory
"""
if metadata is None:
metadata = {}
filters = filters or {}
if user_id:
filters["user_id"] = metadata["user_id"] = user_id
if agent_id:
filters["agent_id"] = metadata["agent_id"] = agent_id
if run_id:
filters["run_id"] = metadata["run_id"] = run_id
if not any(key in filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError("One of the filters: user_id, agent_id or run_id is required!")
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
raise ValueError(
f"Invalid 'memory_type'. Please pass {MemoryType.PROCEDURAL.value} to create procedural memories."
)
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
if agent_id is not None and memory_type == MemoryType.PROCEDURAL.value:
results = await self._create_procedural_memory(messages, metadata=metadata, llm=llm, prompt=prompt)
return results
if self.config.llm.config.get("enable_vision"):
messages = parse_vision_messages(messages, self.llm, self.config.llm.config.get("vision_details"))
else:
messages = parse_vision_messages(messages)
# Run vector store and graph operations concurrently
vector_store_task = asyncio.create_task(self._add_to_vector_store(messages, metadata, filters, infer))
graph_task = asyncio.create_task(self._add_to_graph(messages, filters))
vector_store_result, graph_result = await asyncio.gather(vector_store_task, graph_task)
if self.api_version == "v1.0":
warnings.warn(
"The current add API output format is deprecated. "
"To use the latest format, set `api_version='v1.1'`. "
"The current format will be removed in mem0ai 1.1.0 and later versions.",
category=DeprecationWarning,
stacklevel=2,
)
return vector_store_result
if self.enable_graph:
return {
"results": vector_store_result,
"relations": graph_result,
}
return {"results": vector_store_result}
async def _add_to_vector_store(self, messages, metadata, filters, infer):
if not infer:
returned_memories = []
for message in messages:
if message["role"] != "system":
message_embeddings = await asyncio.to_thread(self.embedding_model.embed, message["content"], "add")
memory_id = await self._create_memory(message["content"], message_embeddings, metadata)
returned_memories.append({"id": memory_id, "memory": message["content"], "event": "ADD"})
return returned_memories
parsed_messages = parse_messages(messages)
if self.config.custom_fact_extraction_prompt:
system_prompt = self.config.custom_fact_extraction_prompt
user_prompt = f"Input:\n{parsed_messages}"
else:
system_prompt, user_prompt = get_fact_retrieval_messages(parsed_messages)
response = await asyncio.to_thread(
self.llm.generate_response,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
response_format={"type": "json_object"},
)
try:
response = remove_code_blocks(response)
new_retrieved_facts = json.loads(response)["facts"]
except Exception as e:
logging.error(f"Error in new_retrieved_facts: {e}")
new_retrieved_facts = []
retrieved_old_memory = []
new_message_embeddings = {}
# Process all facts concurrently
async def process_fact(new_mem):
messages_embeddings = await asyncio.to_thread(self.embedding_model.embed, new_mem, "add")
new_message_embeddings[new_mem] = messages_embeddings
existing_memories = await asyncio.to_thread(
self.vector_store.search,
query=new_mem,
vectors=messages_embeddings,
limit=5,
filters=filters,
)
return [(mem.id, mem.payload["data"]) for mem in existing_memories]
fact_tasks = [process_fact(fact) for fact in new_retrieved_facts]
fact_results = await asyncio.gather(*fact_tasks)
# Flatten results and build retrieved_old_memory
for result in fact_results:
for mem_id, mem_data in result:
retrieved_old_memory.append({"id": mem_id, "text": mem_data})
unique_data = {}
for item in retrieved_old_memory:
unique_data[item["id"]] = item
retrieved_old_memory = list(unique_data.values())
logging.info(f"Total existing memories: {len(retrieved_old_memory)}")
# mapping UUIDs with integers for handling UUID hallucinations
temp_uuid_mapping = {}
for idx, item in enumerate(retrieved_old_memory):
temp_uuid_mapping[str(idx)] = item["id"]
retrieved_old_memory[idx]["id"] = str(idx)
function_calling_prompt = get_update_memory_messages(
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
)
try:
new_memories_with_actions = await asyncio.to_thread(
self.llm.generate_response,
messages=[{"role": "user", "content": function_calling_prompt}],
response_format={"type": "json_object"},
)
except Exception as e:
logging.error(f"Error in new_memories_with_actions: {e}")
new_memories_with_actions = []
try:
new_memories_with_actions = remove_code_blocks(new_memories_with_actions)
new_memories_with_actions = json.loads(new_memories_with_actions)
except Exception as e:
logging.error(f"Invalid JSON response: {e}")
new_memories_with_actions = []
returned_memories = []
try:
memory_tasks = []
for resp in new_memories_with_actions.get("memory", []):
logging.info(resp)
try:
if not resp.get("text"):
logging.info("Skipping memory entry because of empty `text` field.")
continue
elif resp.get("event") == "ADD":
task = asyncio.create_task(
self._create_memory(
data=resp.get("text"), existing_embeddings=new_message_embeddings, metadata=metadata
)
)
memory_tasks.append((task, resp, "ADD", None))
elif resp.get("event") == "UPDATE":
task = asyncio.create_task(
self._update_memory(
memory_id=temp_uuid_mapping[resp["id"]],
data=resp.get("text"),
existing_embeddings=new_message_embeddings,
metadata=metadata,
)
)
memory_tasks.append((task, resp, "UPDATE", temp_uuid_mapping[resp["id"]]))
elif resp.get("event") == "DELETE":
task = asyncio.create_task(self._delete_memory(memory_id=temp_uuid_mapping[resp.get("id")]))
memory_tasks.append((task, resp, "DELETE", temp_uuid_mapping[resp["id"]]))
elif resp.get("event") == "NONE":
logging.info("NOOP for Memory.")
except Exception as e:
logging.error(f"Error in new_memories_with_actions: {e}")
# Wait for all memory operations to complete
for task, resp, event_type, mem_id in memory_tasks:
try:
result_id = await task
if event_type == "ADD":
returned_memories.append(
{
"id": result_id,
"memory": resp.get("text"),
"event": resp.get("event"),
}
)
elif event_type == "UPDATE":
returned_memories.append(
{
"id": mem_id,
"memory": resp.get("text"),
"event": resp.get("event"),
"previous_memory": resp.get("old_memory"),
}
)
elif event_type == "DELETE":
returned_memories.append(
{
"id": mem_id,
"memory": resp.get("text"),
"event": resp.get("event"),
}
)
except Exception as e:
logging.error(f"Error processing memory task: {e}")
except Exception as e:
logging.error(f"Error in new_memories_with_actions: {e}")
capture_event("async_mem0.add", self, {"version": self.api_version, "keys": list(filters.keys())})
return returned_memories
async def _add_to_graph(self, messages, filters):
added_entities = []
if self.enable_graph:
if filters.get("user_id") is None:
filters["user_id"] = "user"
data = "\n".join([msg["content"] for msg in messages if "content" in msg and msg["role"] != "system"])
added_entities = await asyncio.to_thread(self.graph.add, data, filters)
return added_entities
async def get(self, memory_id):
"""
Retrieve a memory by ID asynchronously.
Args:
memory_id (str): ID of the memory to retrieve.
Returns:
dict: Retrieved memory.
"""
capture_event("async_mem0.get", self, {"memory_id": memory_id})
memory = await asyncio.to_thread(self.vector_store.get, vector_id=memory_id)
if not memory:
return None
filters = {key: memory.payload[key] for key in ["user_id", "agent_id", "run_id"] if memory.payload.get(key)}
# Prepare base memory item
memory_item = MemoryItem(
id=memory.id,
memory=memory.payload["data"],
hash=memory.payload.get("hash"),
created_at=memory.payload.get("created_at"),
updated_at=memory.payload.get("updated_at"),
).model_dump(exclude={"score"})
# Add metadata if there are additional keys
excluded_keys = {"user_id", "agent_id", "run_id", "hash", "data", "created_at", "updated_at", "id"}
additional_metadata = {k: v for k, v in memory.payload.items() if k not in excluded_keys}
if additional_metadata:
memory_item["metadata"] = additional_metadata
result = {**memory_item, **filters}
return result
async def get_all(self, user_id=None, agent_id=None, run_id=None, limit=100):
"""
List all memories asynchronously.
Returns:
list: List of all memories.
"""
filters = {}
if user_id:
filters["user_id"] = user_id
if agent_id:
filters["agent_id"] = agent_id
if run_id:
filters["run_id"] = run_id
capture_event("async_mem0.get_all", self, {"limit": limit, "keys": list(filters.keys())})
# Run vector store and graph operations concurrently
vector_store_task = asyncio.create_task(self._get_all_from_vector_store(filters, limit))
if self.enable_graph:
graph_task = asyncio.create_task(asyncio.to_thread(self.graph.get_all, filters, limit))
all_memories, graph_entities = await asyncio.gather(vector_store_task, graph_task)
else:
all_memories = await vector_store_task
graph_entities = None
if self.enable_graph:
return {"results": all_memories, "relations": graph_entities}
if self.api_version == "v1.0":
warnings.warn(
"The current get_all API output format is deprecated. "
"To use the latest format, set `api_version='v1.1'`. "
"The current format will be removed in mem0ai 1.1.0 and later versions.",
category=DeprecationWarning,
stacklevel=2,
)
return all_memories
else:
return {"results": all_memories}
async def _get_all_from_vector_store(self, filters, limit):
memories = await asyncio.to_thread(self.vector_store.list, filters=filters, limit=limit)
excluded_keys = {
"user_id",
"agent_id",
"run_id",
"hash",
"data",
"created_at",
"updated_at",
"id",
}
all_memories = [
{
**MemoryItem(
id=mem.id,
memory=mem.payload["data"],
hash=mem.payload.get("hash"),
created_at=mem.payload.get("created_at"),
updated_at=mem.payload.get("updated_at"),
).model_dump(exclude={"score"}),
**{key: mem.payload[key] for key in ["user_id", "agent_id", "run_id"] if key in mem.payload},
**(
{"metadata": {k: v for k, v in mem.payload.items() if k not in excluded_keys}}
if any(k for k in mem.payload if k not in excluded_keys)
else {}
),
}
for mem in memories[0]
]
return all_memories
async def search(self, query, user_id=None, agent_id=None, run_id=None, limit=100, filters=None):
"""
Search for memories asynchronously.
Args:
query (str): Query to search for.
user_id (str, optional): ID of the user to search for. Defaults to None.
agent_id (str, optional): ID of the agent to search for. Defaults to None.
run_id (str, optional): ID of the run to search for. Defaults to None.
limit (int, optional): Limit the number of results. Defaults to 100.
filters (dict, optional): Filters to apply to the search. Defaults to None.
Returns:
list: List of search results.
"""
filters = filters or {}
if user_id:
filters["user_id"] = user_id
if agent_id:
filters["agent_id"] = agent_id
if run_id:
filters["run_id"] = run_id
if not any(key in filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError("One of the filters: user_id, agent_id or run_id is required!")
capture_event(
"async_mem0.search",
self,
{"limit": limit, "version": self.api_version, "keys": list(filters.keys())},
)
# Run vector store and graph operations concurrently
vector_store_task = asyncio.create_task(self._search_vector_store(query, filters, limit))
if self.enable_graph:
graph_task = asyncio.create_task(asyncio.to_thread(self.graph.search, query, filters, limit))
original_memories, graph_entities = await asyncio.gather(vector_store_task, graph_task)
else:
original_memories = await vector_store_task
graph_entities = None
if self.enable_graph:
return {"results": original_memories, "relations": graph_entities}
if self.api_version == "v1.0":
warnings.warn(
"The current get_all API output format is deprecated. "
"To use the latest format, set `api_version='v1.1'`. "
"The current format will be removed in mem0ai 1.1.0 and later versions.",
category=DeprecationWarning,
stacklevel=2,
)
return original_memories
else:
return {"results": original_memories}
async def _search_vector_store(self, query, filters, limit):
embeddings = await asyncio.to_thread(self.embedding_model.embed, query, "search")
memories = await asyncio.to_thread(
self.vector_store.search, query=query, vectors=embeddings, limit=limit, filters=filters
)
excluded_keys = {
"user_id",
"agent_id",
"run_id",
"hash",
"data",
"created_at",
"updated_at",
"id",
}
original_memories = [
{
**MemoryItem(
id=mem.id,
memory=mem.payload["data"],
hash=mem.payload.get("hash"),
created_at=mem.payload.get("created_at"),
updated_at=mem.payload.get("updated_at"),
score=mem.score,
).model_dump(),
**{key: mem.payload[key] for key in ["user_id", "agent_id", "run_id"] if key in mem.payload},
**(
{"metadata": {k: v for k, v in mem.payload.items() if k not in excluded_keys}}
if any(k for k in mem.payload if k not in excluded_keys)
else {}
),
}
for mem in memories
]
return original_memories
async def update(self, memory_id, data):
"""
Update a memory by ID asynchronously.
Args:
memory_id (str): ID of the memory to update.
data (dict): Data to update the memory with.
Returns:
dict: Updated memory.
"""
capture_event("async_mem0.update", self, {"memory_id": memory_id})
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
existing_embeddings = {data: embeddings}
await self._update_memory(memory_id, data, existing_embeddings)
return {"message": "Memory updated successfully!"}
async def delete(self, memory_id):
"""
Delete a memory by ID asynchronously.
Args:
memory_id (str): ID of the memory to delete.
"""
capture_event("async_mem0.delete", self, {"memory_id": memory_id})
await self._delete_memory(memory_id)
return {"message": "Memory deleted successfully!"}
async def delete_all(self, user_id=None, agent_id=None, run_id=None):
"""
Delete all memories asynchronously.
Args:
user_id (str, optional): ID of the user to delete memories for. Defaults to None.
agent_id (str, optional): ID of the agent to delete memories for. Defaults to None.
run_id (str, optional): ID of the run to delete memories for. Defaults to None.
"""
filters = {}
if user_id:
filters["user_id"] = user_id
if agent_id:
filters["agent_id"] = agent_id
if run_id:
filters["run_id"] = run_id
if not filters:
raise ValueError(
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
)
capture_event("async_mem0.delete_all", self, {"keys": list(filters.keys())})
memories = await asyncio.to_thread(self.vector_store.list, filters=filters)
delete_tasks = []
for memory in memories[0]:
delete_tasks.append(self._delete_memory(memory.id))
await asyncio.gather(*delete_tasks)
logger.info(f"Deleted {len(memories[0])} memories")
if self.enable_graph:
await asyncio.to_thread(self.graph.delete_all, filters)
return {"message": "Memories deleted successfully!"}
async def history(self, memory_id):
"""
Get the history of changes for a memory by ID asynchronously.
Args:
memory_id (str): ID of the memory to get history for.
Returns:
list: List of changes for the memory.
"""
capture_event("async_mem0.history", self, {"memory_id": memory_id})
return await asyncio.to_thread(self.db.get_history, memory_id)
async def _create_memory(self, data, existing_embeddings, metadata=None):
logging.debug(f"Creating memory with {data=}")
if data in existing_embeddings:
embeddings = existing_embeddings[data]
else:
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, memory_action="add")
memory_id = str(uuid.uuid4())
metadata = metadata or {}
metadata["data"] = data
metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
metadata["created_at"] = datetime.now(pytz.timezone("US/Pacific")).isoformat()
await asyncio.to_thread(
self.vector_store.insert,
vectors=[embeddings],
ids=[memory_id],
payloads=[metadata],
)
await asyncio.to_thread(self.db.add_history, memory_id, None, data, "ADD", created_at=metadata["created_at"])
capture_event("async_mem0._create_memory", self, {"memory_id": memory_id})
return memory_id
async def _create_procedural_memory(self, messages, metadata=None, llm=None, prompt=None):
"""
Create a procedural memory asynchronously
Args:
messages (list): List of messages to create a procedural memory from.
metadata (dict): Metadata to create a procedural memory from.
llm (BaseChatModel, optional): LLM class to use for generating procedural memories. Defaults to None. Useful when user is using LangChain ChatModel.
prompt (str, optional): Prompt to use for the procedural memory creation. Defaults to None.
"""
try:
from langchain_core.messages.utils import (
convert_to_messages, # type: ignore
)
except Exception:
logger.error(
"Import error while loading langchain-core. Please install 'langchain-core' to use procedural memory."
)
raise
logger.info("Creating procedural memory")
parsed_messages = [
{"role": "system", "content": prompt or PROCEDURAL_MEMORY_SYSTEM_PROMPT},
*messages,
{"role": "user", "content": "Create procedural memory of the above conversation."},
]
try:
if llm is not None:
parsed_messages = convert_to_messages(parsed_messages)
response = await asyncio.to_thread(llm.invoke, input=parsed_messages)
procedural_memory = response.content
else:
procedural_memory = await asyncio.to_thread(self.llm.generate_response, messages=parsed_messages)
except Exception as e:
logger.error(f"Error generating procedural memory summary: {e}")
raise
if metadata is None:
raise ValueError("Metadata cannot be done for procedural memory.")
metadata["memory_type"] = MemoryType.PROCEDURAL.value
# Generate embeddings for the summary
embeddings = await asyncio.to_thread(self.embedding_model.embed, procedural_memory, memory_action="add")
# Create the memory
memory_id = await self._create_memory(procedural_memory, {procedural_memory: embeddings}, metadata=metadata)
capture_event("async_mem0._create_procedural_memory", self, {"memory_id": memory_id})
# Return results in the same format as add()
result = {"results": [{"id": memory_id, "memory": procedural_memory, "event": "ADD"}]}
return result
async def _update_memory(self, memory_id, data, existing_embeddings, metadata=None):
logger.info(f"Updating memory with {data=}")
try:
existing_memory = await asyncio.to_thread(self.vector_store.get, vector_id=memory_id)
except Exception:
raise ValueError(f"Error getting memory with ID {memory_id}. Please provide a valid 'memory_id'")
prev_value = existing_memory.payload.get("data")
new_metadata = metadata or {}
new_metadata["data"] = data
new_metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
new_metadata["created_at"] = existing_memory.payload.get("created_at")
new_metadata["updated_at"] = datetime.now(pytz.timezone("US/Pacific")).isoformat()
if "user_id" in existing_memory.payload:
new_metadata["user_id"] = existing_memory.payload["user_id"]
if "agent_id" in existing_memory.payload:
new_metadata["agent_id"] = existing_memory.payload["agent_id"]
if "run_id" in existing_memory.payload:
new_metadata["run_id"] = existing_memory.payload["run_id"]
if data in existing_embeddings:
embeddings = existing_embeddings[data]
else:
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
await asyncio.to_thread(
self.vector_store.update,
vector_id=memory_id,
vector=embeddings,
payload=new_metadata,
)
logger.info(f"Updating memory with ID {memory_id=} with {data=}")
await asyncio.to_thread(
self.db.add_history,
memory_id,
prev_value,
data,
"UPDATE",
created_at=new_metadata["created_at"],
updated_at=new_metadata["updated_at"],
)
capture_event("async_mem0._update_memory", self, {"memory_id": memory_id})
return memory_id
async def _delete_memory(self, memory_id):
logging.info(f"Deleting memory with {memory_id=}")
existing_memory = await asyncio.to_thread(self.vector_store.get, vector_id=memory_id)
prev_value = existing_memory.payload["data"]
await asyncio.to_thread(self.vector_store.delete, vector_id=memory_id)
await asyncio.to_thread(self.db.add_history, memory_id, prev_value, None, "DELETE", is_deleted=1)
capture_event("async_mem0._delete_memory", self, {"memory_id": memory_id})
return memory_id
async def reset(self):
"""
Reset the memory store asynchronously.
"""
logger.warning("Resetting all memories")
await asyncio.to_thread(self.vector_store.delete_col)
self.vector_store = VectorStoreFactory.create(
self.config.vector_store.provider, self.config.vector_store.config
)
await asyncio.to_thread(self.db.reset)
capture_event("async_mem0.reset", self)
async def chat(self, query):
raise NotImplementedError("Chat function not implemented yet.")
+27
View File
@@ -3,6 +3,7 @@ import os
import uuid
# Set up the directory path
VECTOR_ID = str(uuid.uuid4())
home_dir = os.path.expanduser("~")
mem0_dir = os.environ.get("MEM0_DIR") or os.path.join(home_dir, ".mem0")
os.makedirs(mem0_dir, exist_ok=True)
@@ -29,3 +30,29 @@ def get_user_id():
return user_id
except Exception:
return "anonymous_user"
def get_or_create_user_id(vector_store):
"""Store user_id in vector store and return it."""
user_id = get_user_id()
# Try to get existing user_id from vector store
try:
existing = vector_store.get(vector_id=VECTOR_ID)
if existing and hasattr(existing, "payload") and existing.payload and "user_id" in existing.payload:
return existing.payload["user_id"]
except:
pass
# If we get here, we need to insert the user_id
try:
dims = getattr(vector_store, "embedding_model_dims", 1)
vector_store.insert(
vectors=[[0.0] * dims],
payloads=[{"user_id": user_id, "type": "user_identity"}],
ids=[VECTOR_ID]
)
except:
pass
return user_id
+10 -12
View File
@@ -6,7 +6,7 @@ import sys
from posthog import Posthog
import mem0
from mem0.memory.setup import get_user_id, setup_config
from mem0.memory.setup import get_or_create_user_id
MEM0_TELEMETRY = os.environ.get("MEM0_TELEMETRY", "True")
@@ -21,11 +21,11 @@ logging.getLogger("urllib3").setLevel(logging.CRITICAL + 1)
class AnonymousTelemetry:
def __init__(self, project_api_key, host):
def __init__(self, project_api_key, host, vector_store=None):
self.posthog = Posthog(project_api_key=project_api_key, host=host)
# Call setup config to ensure that the user_id is generated
setup_config()
self.user_id = get_user_id()
self.user_id = get_or_create_user_id(vector_store)
if not MEM0_TELEMETRY:
self.posthog.disabled = True
@@ -50,14 +50,12 @@ class AnonymousTelemetry:
self.posthog.shutdown()
# Initialize AnonymousTelemetry
telemetry = AnonymousTelemetry(
project_api_key="phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX",
host="https://us.i.posthog.com",
)
def capture_event(event_name, memory_instance, additional_data=None):
global telemetry
# For OSS, we use the telemetry vector store to store the user_id
telemetry = AnonymousTelemetry(project_api_key="phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX", host="https://us.i.posthog.com", vector_store=memory_instance._telemetry_vector_store if hasattr(memory_instance, "_telemetry_vector_store") else None)
event_data = {
"collection": memory_instance.collection_name,
"vector_size": memory_instance.embedding_model.config.embedding_dims,
+1
View File
@@ -84,6 +84,7 @@ class VectorStoreFactory:
"supabase": "mem0.vector_stores.supabase.Supabase",
"weaviate": "mem0.vector_stores.weaviate.Weaviate",
"faiss": "mem0.vector_stores.faiss.FAISS",
"langchain": "mem0.vector_stores.langchain.Langchain",
}
@classmethod
+1
View File
@@ -25,6 +25,7 @@ class VectorStoreConfig(BaseModel):
"supabase": "SupabaseConfig",
"weaviate": "WeaviateConfig",
"faiss": "FAISSConfig",
"langchain": "LangchainConfig",
}
@model_validator(mode="after")
+161
View File
@@ -0,0 +1,161 @@
from typing import Dict, List, Optional
from pydantic import BaseModel
try:
from langchain_community.vectorstores import VectorStore
except ImportError:
raise ImportError("The 'langchain_community' library is required. Please install it using 'pip install langchain_community'.")
from mem0.vector_stores.base import VectorStoreBase
class OutputData(BaseModel):
id: Optional[str] # memory id
score: Optional[float] # distance
payload: Optional[Dict] # metadata
class Langchain(VectorStoreBase):
def __init__(self, client: VectorStore, collection_name: str = "mem0"):
self.client = client
self.collection_name = collection_name
def _parse_output(self, data: Dict) -> List[OutputData]:
"""
Parse the output data.
Args:
data (Dict): Output data or list of Document objects.
Returns:
List[OutputData]: Parsed output data.
"""
# Check if input is a list of Document objects
if isinstance(data, list) and all(hasattr(doc, 'metadata') for doc in data if hasattr(doc, '__dict__')):
result = []
for doc in data:
entry = OutputData(
id=getattr(doc, "id", None),
score=None, # Document objects typically don't include scores
payload=getattr(doc, "metadata", {})
)
result.append(entry)
return result
# Original format handling
keys = ["ids", "distances", "metadatas"]
values = []
for key in keys:
value = data.get(key, [])
if isinstance(value, list) and value and isinstance(value[0], list):
value = value[0]
values.append(value)
ids, distances, metadatas = values
max_length = max(len(v) for v in values if isinstance(v, list) and v is not None)
result = []
for i in range(max_length):
entry = OutputData(
id=ids[i] if isinstance(ids, list) and ids and i < len(ids) else None,
score=(distances[i] if isinstance(distances, list) and distances and i < len(distances) else None),
payload=(metadatas[i] if isinstance(metadatas, list) and metadatas and i < len(metadatas) else None),
)
result.append(entry)
return result
def create_col(self, name, vector_size=None, distance=None):
self.collection_name = name
return self.client
def insert(self, vectors: List[List[float]], payloads: Optional[List[Dict]] = None, ids: Optional[List[str]] = None):
"""
Insert vectors into the LangChain vectorstore.
"""
# Check if client has add_embeddings method
if hasattr(self.client, "add_embeddings"):
# Some LangChain vectorstores have a direct add_embeddings method
self.client.add_embeddings(
embeddings=vectors,
metadatas=payloads,
ids=ids
)
else:
# Fallback to add_texts method
texts = [payload.get("data", "") for payload in payloads] if payloads else [""] * len(vectors)
self.client.add_texts(
texts=texts,
metadatas=payloads,
ids=ids
)
def search(self, query: str, vectors: List[List[float]], limit: int = 5, filters: Optional[Dict] = None):
"""
Search for similar vectors in LangChain.
"""
# For each vector, perform a similarity search
if filters:
results = self.client.similarity_search_by_vector(
embedding=vectors,
k=limit,
filter=filters
)
else:
results = self.client.similarity_search_by_vector(
embedding=vectors,
k=limit
)
final_results = self._parse_output(results)
return final_results
def delete(self, vector_id):
"""
Delete a vector by ID.
"""
self.client.delete(ids=[vector_id])
def update(self, vector_id, vector=None, payload=None):
"""
Update a vector and its payload.
"""
self.delete(vector_id)
self.insert(vector, payload, [vector_id])
def get(self, vector_id):
"""
Retrieve a vector by ID.
"""
docs = self.client.get_by_ids([vector_id])
if docs and len(docs) > 0:
doc = docs[0]
return self._parse_output([doc])[0]
return None
def list_cols(self):
"""
List all collections.
"""
# LangChain doesn't have collections
return [self.collection_name]
def delete_col(self):
"""
Delete a collection.
"""
self.client.delete(ids=None)
def col_info(self):
"""
Get information about a collection.
"""
return {"name": self.collection_name}
def list(self, filters=None, limit=None):
"""
List all vectors in a collection.
"""
# This would require implementation-specific access to the underlying store
raise NotImplementedError("Listing all vectors not directly supported by LangChain vectorstores")
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mem0ai"
version = "0.1.88"
version = "0.1.89"
description = "Long-term memory for AI Agents"
authors = ["Mem0 <founders@mem0.ai>"]
exclude = [
+4 -4
View File
@@ -39,14 +39,14 @@ def test_create_col(faiss_instance, mock_faiss_index):
# Test creating a collection with euclidean distance
with patch('faiss.IndexFlatL2', return_value=mock_faiss_index) as mock_index_flat_l2:
with patch('faiss.write_index'):
faiss_instance.create_col(name="new_collection", vector_size=256)
mock_index_flat_l2.assert_called_once_with(256)
faiss_instance.create_col(name="new_collection")
mock_index_flat_l2.assert_called_once_with(faiss_instance.embedding_model_dims)
# Test creating a collection with inner product distance
with patch('faiss.IndexFlatIP', return_value=mock_faiss_index) as mock_index_flat_ip:
with patch('faiss.write_index'):
faiss_instance.create_col(name="new_collection", vector_size=256, distance="inner_product")
mock_index_flat_ip.assert_called_once_with(256)
faiss_instance.create_col(name="new_collection", distance="inner_product")
mock_index_flat_ip.assert_called_once_with(faiss_instance.embedding_model_dims)
def test_insert(faiss_instance, mock_faiss_index):
@@ -0,0 +1,101 @@
from unittest.mock import Mock, patch
import pytest
from langchain_community.vectorstores import VectorStore
from mem0.vector_stores.langchain import Langchain
@pytest.fixture
def mock_langchain_client():
with patch("langchain_community.vectorstores.VectorStore") as mock_client:
yield mock_client
@pytest.fixture
def langchain_instance(mock_langchain_client):
mock_client = Mock(spec=VectorStore)
return Langchain(client=mock_client, collection_name="test_collection")
def test_insert_vectors(langchain_instance):
# Test data
vectors = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
payloads = [{"data": "text1", "name": "vector1"}, {"data": "text2", "name": "vector2"}]
ids = ["id1", "id2"]
# Test with add_embeddings method
langchain_instance.client.add_embeddings = Mock()
langchain_instance.insert(vectors=vectors, payloads=payloads, ids=ids)
langchain_instance.client.add_embeddings.assert_called_once_with(
embeddings=vectors,
metadatas=payloads,
ids=ids
)
# Test with add_texts method
delattr(langchain_instance.client, "add_embeddings") # Remove attribute completely
langchain_instance.client.add_texts = Mock()
langchain_instance.insert(vectors=vectors, payloads=payloads, ids=ids)
langchain_instance.client.add_texts.assert_called_once_with(
texts=["text1", "text2"],
metadatas=payloads,
ids=ids
)
# Test with empty payloads
langchain_instance.client.add_texts.reset_mock()
langchain_instance.insert(vectors=vectors, payloads=None, ids=ids)
langchain_instance.client.add_texts.assert_called_once_with(
texts=["", ""],
metadatas=None,
ids=ids
)
def test_search_vectors(langchain_instance):
# Mock search results
mock_docs = [
Mock(metadata={"name": "vector1"}, id="id1"),
Mock(metadata={"name": "vector2"}, id="id2")
]
langchain_instance.client.similarity_search_by_vector.return_value = mock_docs
# Test search without filters
vectors = [[0.1, 0.2, 0.3]]
results = langchain_instance.search(query="", vectors=vectors, limit=2)
langchain_instance.client.similarity_search_by_vector.assert_called_once_with(
embedding=vectors,
k=2
)
assert len(results) == 2
assert results[0].id == "id1"
assert results[0].payload == {"name": "vector1"}
assert results[1].id == "id2"
assert results[1].payload == {"name": "vector2"}
# Test search with filters
filters = {"name": "vector1"}
langchain_instance.search(query="", vectors=vectors, limit=2, filters=filters)
langchain_instance.client.similarity_search_by_vector.assert_called_with(
embedding=vectors,
k=2,
filter=filters
)
def test_get_vector(langchain_instance):
# Mock get result
mock_doc = Mock(metadata={"name": "vector1"}, id="id1")
langchain_instance.client.get_by_ids.return_value = [mock_doc]
# Test get existing vector
result = langchain_instance.get("id1")
langchain_instance.client.get_by_ids.assert_called_once_with(["id1"])
assert result is not None
assert result.id == "id1"
assert result.payload == {"name": "vector1"}
# Test get non-existent vector
langchain_instance.client.get_by_ids.return_value = []
result = langchain_instance.get("non_existent_id")
assert result is None