Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5327d6e50d | |||
| 15a3e20371 | |||
| 19d7beef43 | |||
| 8b789adb15 | |||
| fd065fe9cc | |||
| b5127f7c62 | |||
| 37d9fed690 | |||
| 31861e9acb | |||
| 07462adc9a | |||
| 616313b8b5 | |||
| 44f2490667 |
@@ -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).
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
@@ -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">
|
||||
|
||||
@@ -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.
|
||||
@@ -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())
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.env*
|
||||
dist
|
||||
package-lock.json
|
||||
@@ -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/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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">
|
||||
×
|
||||
</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 };
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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(/&#39;/g, "'")
|
||||
.replace(/&quot;/g, '"')
|
||||
.replace(/&lt;/g, "<")
|
||||
.replace(/&gt;/g, ">")
|
||||
.replace(/&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>';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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},
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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.")
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,6 +25,7 @@ class VectorStoreConfig(BaseModel):
|
||||
"supabase": "SupabaseConfig",
|
||||
"weaviate": "WeaviateConfig",
|
||||
"faiss": "FAISSConfig",
|
||||
"langchain": "LangchainConfig",
|
||||
}
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
||||
@@ -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
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user