Compare commits

...

1 commit

Author SHA1 Message Date
2045a042eb Working frontend and openai compatible endpoint 2025-10-27 15:29:55 +00:00
7 changed files with 623 additions and 40 deletions

View file

@ -11,11 +11,14 @@ RUN apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Copy backend code
COPY backend/ .
# Copy frontend directory
COPY frontend/ /app/frontend/
# Set Python path
ENV PYTHONPATH=/app

View file

@ -1,7 +1,7 @@
"""Simple API key authentication for Mem0 Interface."""
from typing import Optional
from fastapi import HTTPException, Security, status
from fastapi import HTTPException, Security, status, Header
from fastapi.security import APIKeyHeader
import structlog
@ -91,6 +91,44 @@ async def get_current_user(api_key: str = Security(api_key_header)) -> str:
return auth_service.verify_api_key(api_key)
async def get_current_user_openai(
authorization: Optional[str] = Header(None),
x_api_key: Optional[str] = Header(None, alias="X-API-Key")
) -> str:
"""
FastAPI dependency for OpenAI-compatible authentication.
Supports both Authorization: Bearer and X-API-Key headers.
Args:
authorization: Authorization header (Bearer token)
x_api_key: X-API-Key header
Returns:
str: Authenticated user_id
Raises:
HTTPException: If no valid API key is provided
"""
api_key = None
# Try Bearer token first (OpenAI standard)
if authorization and authorization.startswith("Bearer "):
api_key = authorization[7:] # Remove "Bearer " prefix
logger.debug(f"Extracted API key from Authorization Bearer token")
# Fall back to X-API-Key header
elif x_api_key:
api_key = x_api_key
logger.debug(f"Extracted API key from X-API-Key header")
else:
logger.warning("No API key provided in Authorization or X-API-Key headers")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing API key. Provide either 'Authorization: Bearer <key>' or 'X-API-Key: <key>' header"
)
return auth_service.verify_api_key(api_key)
async def verify_user_access(
api_key: str = Security(api_key_header),
user_id: Optional[str] = None

View file

@ -8,18 +8,24 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, BackgroundTasks, Depends, Security
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
import structlog
import json
import asyncio
from pathlib import Path
from config import settings
from models import (
ChatRequest, MemoryAddRequest, MemoryAddResponse,
MemorySearchRequest, MemorySearchResponse, MemoryUpdateRequest,
MemoryItem, GraphResponse, HealthResponse, ErrorResponse,
GlobalStatsResponse, UserStatsResponse
GlobalStatsResponse, UserStatsResponse,
OpenAIChatCompletionRequest, OpenAIChatCompletionResponse,
OpenAIChoice, OpenAIChoiceMessage, OpenAIUsage,
OpenAIStreamChoice, OpenAIStreamDelta
)
from mem0_manager import mem0_manager
from auth import get_current_user, auth_service
from auth import get_current_user, get_current_user_openai, auth_service
# Configure structured logging
structlog.configure(
@ -175,6 +181,16 @@ async def global_exception_handler(request, exc):
)
# Root endpoint - serve frontend
@app.get("/")
async def root():
"""Serve the frontend HTML interface."""
frontend_path = Path("/app/frontend/index.html")
if frontend_path.exists():
return FileResponse(frontend_path)
return {"message": "Mem0 API", "version": "1.0.0", "docs": "/docs"}
# Health check endpoint
@app.get("/health", response_model=HealthResponse)
async def health_check():
@ -236,6 +252,178 @@ async def chat_with_memory(
raise HTTPException(status_code=500, detail=str(e))
async def stream_openai_response(completion_id: str, model: str, content: str, created: int):
"""
Generate Server-Sent Events (SSE) stream for OpenAI-compatible streaming.
Simulates streaming by chunking the response content.
"""
import uuid
# First chunk with role
chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{
"index": 0,
"delta": {"role": "assistant", "content": ""},
"finish_reason": None
}]
}
yield f"data: {json.dumps(chunk)}\n\n"
# Stream content in chunks (simulate streaming by splitting into words)
# For true streaming from LLM, we'd stream as tokens arrive
words = content.split()
chunk_size = 3 # Send 3 words at a time for smooth streaming effect
for i in range(0, len(words), chunk_size):
word_chunk = " ".join(words[i:i + chunk_size])
if i + chunk_size < len(words):
word_chunk += " " # Add space between chunks except last
chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{
"index": 0,
"delta": {"content": word_chunk},
"finish_reason": None
}]
}
yield f"data: {json.dumps(chunk)}\n\n"
await asyncio.sleep(0.05) # Small delay for streaming effect
# Final chunk with finish_reason
chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{
"index": 0,
"delta": {},
"finish_reason": "stop"
}]
}
yield f"data: {json.dumps(chunk)}\n\n"
# OpenAI standard: end with [DONE]
yield "data: [DONE]\n\n"
# OpenAI-compatible chat completions endpoint
@app.post("/v1/chat/completions")
@app.post("/chat/completions")
async def openai_chat_completions(
request: OpenAIChatCompletionRequest,
authenticated_user: str = Depends(get_current_user_openai)
):
"""
OpenAI-compatible chat completions endpoint with automatic mem0 memory integration.
Available at both:
- /v1/chat/completions (OpenAI standard)
- /chat/completions (direct access)
- API key maps to user_id automatically
- Memories are searched and added transparently
- Compatible with OpenAI Python SDK and other OpenAI-compatible clients
"""
try:
import uuid
# Extract user_id from authenticated API key
user_id = authenticated_user
logger.info(f"Processing OpenAI chat completion for user: {user_id} (streaming={request.stream})")
# Extract last user message and conversation context
user_messages = [m for m in request.messages if m.get("role") == "user"]
if not user_messages:
raise HTTPException(
status_code=400,
detail="No user messages provided. Include at least one message with role='user'."
)
last_message = user_messages[-1].get("content", "")
context = request.messages[:-1] # All messages except the last one
logger.info(f"Last user message: {last_message[:100]}...")
logger.info(f"Context messages: {len(context)}")
# Call existing chat_with_memory (handles mem0 search + LLM + mem0 add)
result = await mem0_manager.chat_with_memory(
message=last_message,
user_id=user_id,
context=context if context else None
)
# Generate IDs and timestamps
completion_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
created_time = int(time.time())
assistant_content = result.get("response", "")
# Return streaming or non-streaming response
if request.stream:
logger.info(f"Returning streaming response for {completion_id}")
return StreamingResponse(
stream_openai_response(
completion_id=completion_id,
model=settings.default_model,
content=assistant_content,
created=created_time
),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
else:
# Non-streaming response (original format)
response = OpenAIChatCompletionResponse(
id=completion_id,
object="chat.completion",
created=created_time,
model=settings.default_model,
choices=[
OpenAIChoice(
index=0,
message=OpenAIChoiceMessage(
role="assistant",
content=assistant_content
),
finish_reason="stop"
)
],
usage=OpenAIUsage(
prompt_tokens=0, # We don't track tokens yet
completion_tokens=0,
total_tokens=0
)
)
logger.info(
f"OpenAI completion successful",
completion_id=completion_id,
user_id=user_id,
memories_used=result.get("memories_used", 0)
)
return response
except HTTPException as e:
logger.error(f"HTTP Exception in OpenAI endpoint: {e.status_code} - {e.detail}")
raise
except Exception as e:
logger.error(f"Error in OpenAI chat completions endpoint: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
# Memory management endpoints - pure Mem0 passthroughs
@app.post("/memories")
async def add_memories(

View file

@ -52,10 +52,10 @@ class Mem0Manager:
"embedder": {
"provider": "ollama",
"config": {
"model": "qwen3-embedding:4b-q8_0",
"model": "hf.co/Qwen/Qwen3-Embedding-0.6B-GGUF:Q8_0",
# "api_key": settings.embedder_api_key,
"ollama_base_url": "http://172.17.0.1:11434",
"embedding_dims": 2560
"ollama_base_url": "https://models.breezehq.dev",
"embedding_dims": 1024
}
},
"vector_store": {
@ -64,7 +64,7 @@ class Mem0Manager:
"collection_name": settings.qdrant_collection_name,
"host": settings.qdrant_host,
"port": settings.qdrant_port,
"embedding_model_dims": 2560,
"embedding_model_dims": 1024,
"on_disk": True
}
},

View file

@ -137,4 +137,72 @@ class UserStatsResponse(BaseModel):
relationship_count: int = Field(..., description="Number of graph relationships for this user")
last_activity: Optional[str] = Field(None, description="Last activity timestamp")
api_calls_today: int = Field(..., description="API calls made by this user today")
avg_response_time_ms: float = Field(..., description="Average response time for this user's requests")
avg_response_time_ms: float = Field(..., description="Average response time for this user's requests")
# OpenAI-Compatible API Models
class OpenAIMessage(BaseModel):
"""OpenAI message format."""
role: str = Field(..., description="Message role (system, user, assistant)")
content: str = Field(..., description="Message content")
class OpenAIChatCompletionRequest(BaseModel):
"""OpenAI chat completion request format."""
model: str = Field(..., description="Model to use (will use configured default)")
messages: List[Dict[str, str]] = Field(..., description="List of messages")
temperature: Optional[float] = Field(0.7, description="Sampling temperature")
max_tokens: Optional[int] = Field(None, description="Maximum tokens to generate")
stream: Optional[bool] = Field(False, description="Whether to stream responses")
top_p: Optional[float] = Field(1.0, description="Nucleus sampling parameter")
n: Optional[int] = Field(1, description="Number of completions to generate")
stop: Optional[List[str]] = Field(None, description="Stop sequences")
presence_penalty: Optional[float] = Field(0, description="Presence penalty")
frequency_penalty: Optional[float] = Field(0, description="Frequency penalty")
user: Optional[str] = Field(None, description="User identifier (ignored, uses API key)")
class OpenAIUsage(BaseModel):
"""Token usage information."""
prompt_tokens: int = Field(..., description="Tokens in the prompt")
completion_tokens: int = Field(..., description="Tokens in the completion")
total_tokens: int = Field(..., description="Total tokens used")
class OpenAIChoiceMessage(BaseModel):
"""Message in a choice."""
role: str = Field(..., description="Role of the message")
content: str = Field(..., description="Content of the message")
class OpenAIChoice(BaseModel):
"""Individual completion choice."""
index: int = Field(..., description="Choice index")
message: OpenAIChoiceMessage = Field(..., description="Message content")
finish_reason: str = Field(..., description="Reason for completion finish")
class OpenAIChatCompletionResponse(BaseModel):
"""OpenAI chat completion response format."""
id: str = Field(..., description="Unique completion ID")
object: str = Field(default="chat.completion", description="Object type")
created: int = Field(..., description="Unix timestamp of creation")
model: str = Field(..., description="Model used for completion")
choices: List[OpenAIChoice] = Field(..., description="List of completion choices")
usage: Optional[OpenAIUsage] = Field(None, description="Token usage information")
# Streaming-specific models
class OpenAIStreamDelta(BaseModel):
"""Delta content in a streaming chunk."""
role: Optional[str] = Field(None, description="Role (only in first chunk)")
content: Optional[str] = Field(None, description="Incremental content")
class OpenAIStreamChoice(BaseModel):
"""Individual streaming choice."""
index: int = Field(..., description="Choice index")
delta: OpenAIStreamDelta = Field(..., description="Delta content")
finish_reason: Optional[str] = Field(None, description="Reason for completion finish")

View file

@ -15,6 +15,8 @@ services:
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- mem0_network
# Neo4j with APOC for graph relationships
neo4j:
@ -32,6 +34,8 @@ services:
expose:
- "7474" # HTTP - Internal only
- "7687" # Bolt - Internal only
networks:
- mem0_network
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
@ -46,9 +50,9 @@ services:
# Backend API service
backend:
build:
context: ./backend
dockerfile: Dockerfile
build:
context: .
dockerfile: ./backend/Dockerfile
container_name: mem0-backend
environment:
OPENAI_API_KEY: ${OPENAI_COMPAT_API_KEY}
@ -65,8 +69,11 @@ services:
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000}
DEFAULT_MODEL: ${DEFAULT_MODEL:-claude-sonnet-4}
API_KEYS: ${API_KEYS:-{}}
ports:
- "${BACKEND_PORT:-8000}:8000"
expose:
- 8000
networks:
- npm_network
- mem0_network
depends_on:
qdrant:
condition: service_healthy
@ -75,7 +82,8 @@ services:
restart: unless-stopped
volumes:
- ./backend:/app
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
- ./frontend:/app/frontend
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
volumes:
qdrant_data:
@ -85,5 +93,6 @@ volumes:
neo4j_plugins:
networks:
default:
name: mem0-network
mem0_network:
npm_network:
external: true

View file

@ -18,12 +18,106 @@
display: flex;
}
/* Login Screen */
.login-screen {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-screen.hidden {
display: none;
}
.login-box {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
}
.login-box h1 {
margin-bottom: 10px;
color: #333;
font-size: 28px;
text-align: center;
}
.login-box p {
color: #666;
font-size: 14px;
text-align: center;
margin-bottom: 30px;
}
.login-box input {
width: 100%;
padding: 14px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
margin-bottom: 20px;
outline: none;
transition: border-color 0.3s;
}
.login-box input:focus {
border-color: #667eea;
}
.login-box button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, opacity 0.3s;
}
.login-box button:hover {
transform: translateY(-2px);
}
.login-box button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.login-error {
background: #ffe6e6;
border: 1px solid #ffcccc;
color: #cc0000;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
display: none;
}
.login-error.show {
display: block;
}
.container {
display: flex;
width: 100%;
height: 100vh;
}
.container.hidden {
display: none;
}
/* Chat Section */
.chat-section {
flex: 1;
@ -58,7 +152,12 @@
font-size: 14px;
}
.clear-chat-btn {
.header-buttons {
display: flex;
gap: 10px;
}
.clear-chat-btn, .logout-btn {
background: #f8f9fa;
color: #666;
border: 1px solid #e0e0e0;
@ -73,16 +172,27 @@
transition: all 0.2s ease;
}
.clear-chat-btn:hover {
.clear-chat-btn:hover, .logout-btn:hover {
background: #e9ecef;
border-color: #ced4da;
color: #495057;
}
.clear-chat-btn:active {
.clear-chat-btn:active, .logout-btn:active {
background: #dee2e6;
}
.logout-btn {
background: #fff3cd;
border-color: #ffc107;
color: #856404;
}
.logout-btn:hover {
background: #ffe69c;
border-color: #ffb300;
}
.chat-messages {
flex: 1;
overflow-y: auto;
@ -281,17 +391,42 @@
</style>
</head>
<body>
<div class="container">
<!-- Login Screen -->
<div class="login-screen" id="loginScreen">
<div class="login-box">
<h1>🧠 Mem0 Chat</h1>
<p>Enter your API key to access your memory-powered assistant</p>
<div class="login-error" id="loginError"></div>
<input
type="password"
id="apiKeyInput"
placeholder="Enter your API key (e.g., sk-xxxxx)"
autocomplete="off"
/>
<button id="loginButton">Connect</button>
</div>
</div>
<!-- Main Chat Interface (hidden initially) -->
<div class="container hidden" id="mainContainer">
<!-- Chat Section -->
<div class="chat-section">
<div class="chat-header">
<div class="chat-header-content">
<h1>What can I help you with?</h1>
<p>Chat with your memories - User: pratik</p>
<p>Chat with your memories - User: <span id="currentUser">...</span></p>
</div>
<div class="header-buttons">
<button class="clear-chat-btn" id="clearChatBtn" title="Clear chat history">
🗑️ Clear Chat
</button>
<button class="logout-btn" id="logoutBtn" title="Logout">
🚪 Logout
</button>
</div>
<button class="clear-chat-btn" id="clearChatBtn" title="Clear chat history">
🗑️ Clear Chat
</button>
</div>
<div class="chat-messages" id="chatMessages">
@ -319,10 +454,20 @@
<script>
// Configuration
const API_BASE = 'http://localhost:8000';
const USER_ID = 'pratik';
const API_BASE = window.location.origin;
// State
let API_KEY = null;
let USER_ID = null;
// DOM Elements
const loginScreen = document.getElementById('loginScreen');
const mainContainer = document.getElementById('mainContainer');
const apiKeyInput = document.getElementById('apiKeyInput');
const loginButton = document.getElementById('loginButton');
const loginError = document.getElementById('loginError');
const logoutBtn = document.getElementById('logoutBtn');
const currentUser = document.getElementById('currentUser');
const chatMessages = document.getElementById('chatMessages');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
@ -336,19 +481,143 @@
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadChatHistory();
loadMemories();
// Check if already logged in
const savedApiKey = localStorage.getItem('apiKey');
const savedUserId = localStorage.getItem('userId');
if (savedApiKey && savedUserId) {
// Auto-login with saved credentials
API_KEY = savedApiKey;
USER_ID = savedUserId;
showMainInterface();
}
// Event listeners
loginButton.addEventListener('click', handleLogin);
apiKeyInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleLogin();
});
logoutBtn.addEventListener('click', handleLogout);
sendButton.addEventListener('click', sendMessage);
messageInput.addEventListener('keydown', handleKeyDown);
messageInput.addEventListener('input', autoResizeTextarea);
refreshButton.addEventListener('click', loadMemories);
clearChatBtn.addEventListener('click', clearChatWithConfirmation);
});
// Handle login
async function handleLogin() {
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
showLoginError('Please enter an API key');
return;
}
loginButton.disabled = true;
loginButton.textContent = 'Verifying...';
hideLoginError();
try {
// Verify API key by calling /health with auth
const response = await fetch(`${API_BASE}/health`, {
headers: {
'X-API-Key': apiKey
}
});
if (!response.ok) {
throw new Error('Invalid API key');
}
// Get user_id by trying to call a test endpoint
// We'll use /models since it doesn't require auth parameters
const userResponse = await fetch(`${API_BASE}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: 'test' }],
stream: false
})
});
if (!userResponse.ok) {
throw new Error('Failed to verify user');
}
// API key is valid, extract user_id from auth_service mapping
// We'll store both and use a simple username extraction
API_KEY = apiKey;
// Try to extract username from API key (e.g., sk-alice -> alice)
if (apiKey.startsWith('sk-')) {
const parts = apiKey.substring(3).split('-');
USER_ID = parts[0]; // Get first part after sk-
} else {
USER_ID = 'user';
}
// Save to localStorage
localStorage.setItem('apiKey', API_KEY);
localStorage.setItem('userId', USER_ID);
// Show main interface
showMainInterface();
} catch (error) {
console.error('Login error:', error);
showLoginError('Invalid API key. Please check and try again.');
loginButton.disabled = false;
loginButton.textContent = 'Connect';
}
}
// Show main interface
function showMainInterface() {
loginScreen.classList.add('hidden');
mainContainer.classList.remove('hidden');
currentUser.textContent = USER_ID;
// Load data
loadChatHistory();
loadMemories();
// Initialize textarea height
autoResizeTextarea();
});
messageInput.focus();
}
// Handle logout
function handleLogout() {
if (confirm('Are you sure you want to logout?')) {
// Clear credentials
localStorage.removeItem('apiKey');
localStorage.removeItem('userId');
API_KEY = null;
USER_ID = null;
// Show login screen
mainContainer.classList.add('hidden');
loginScreen.classList.remove('hidden');
apiKeyInput.value = '';
hideLoginError();
}
}
// Show login error
function showLoginError(message) {
loginError.textContent = message;
loginError.classList.add('show');
}
// Hide login error
function hideLoginError() {
loginError.classList.remove('show');
}
// Load chat history from localStorage
function loadChatHistory() {
@ -419,6 +688,7 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
},
body: JSON.stringify({
message: message,
@ -459,8 +729,12 @@
// Load memories from backend
async function loadMemories() {
try {
const response = await fetch(`${API_BASE}/memories/${USER_ID}?limit=50`);
try{
const response = await fetch(`${API_BASE}/memories/${USER_ID}?limit=50`, {
headers: {
'X-API-Key': API_KEY
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@ -512,8 +786,11 @@
}
try {
const response = await fetch(`${API_BASE}/memories/${memoryId}`, {
method: 'DELETE'
const response = await fetch(`${API_BASE}/memories/${memoryId}?user_id=${USER_ID}`, {
method: 'DELETE',
headers: {
'X-API-Key': API_KEY
}
});
if (!response.ok) {