Compare commits
1 commit
main
...
cloud-back
| Author | SHA1 | Date | |
|---|---|---|---|
| 2045a042eb |
7 changed files with 623 additions and 40 deletions
|
|
@ -11,11 +11,14 @@ RUN apt-get update && apt-get install -y \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy requirements and install Python dependencies
|
# Copy requirements and install Python dependencies
|
||||||
COPY requirements.txt .
|
COPY backend/requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy application code
|
# Copy backend code
|
||||||
COPY . .
|
COPY backend/ .
|
||||||
|
|
||||||
|
# Copy frontend directory
|
||||||
|
COPY frontend/ /app/frontend/
|
||||||
|
|
||||||
# Set Python path
|
# Set Python path
|
||||||
ENV PYTHONPATH=/app
|
ENV PYTHONPATH=/app
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""Simple API key authentication for Mem0 Interface."""
|
"""Simple API key authentication for Mem0 Interface."""
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import HTTPException, Security, status
|
from fastapi import HTTPException, Security, status, Header
|
||||||
from fastapi.security import APIKeyHeader
|
from fastapi.security import APIKeyHeader
|
||||||
import structlog
|
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)
|
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(
|
async def verify_user_access(
|
||||||
api_key: str = Security(api_key_header),
|
api_key: str = Security(api_key_header),
|
||||||
user_id: Optional[str] = None
|
user_id: Optional[str] = None
|
||||||
|
|
|
||||||
194
backend/main.py
194
backend/main.py
|
|
@ -8,18 +8,24 @@ from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, BackgroundTasks, Depends, Security
|
from fastapi import FastAPI, HTTPException, BackgroundTasks, Depends, Security
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
|
||||||
import structlog
|
import structlog
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
from models import (
|
from models import (
|
||||||
ChatRequest, MemoryAddRequest, MemoryAddResponse,
|
ChatRequest, MemoryAddRequest, MemoryAddResponse,
|
||||||
MemorySearchRequest, MemorySearchResponse, MemoryUpdateRequest,
|
MemorySearchRequest, MemorySearchResponse, MemoryUpdateRequest,
|
||||||
MemoryItem, GraphResponse, HealthResponse, ErrorResponse,
|
MemoryItem, GraphResponse, HealthResponse, ErrorResponse,
|
||||||
GlobalStatsResponse, UserStatsResponse
|
GlobalStatsResponse, UserStatsResponse,
|
||||||
|
OpenAIChatCompletionRequest, OpenAIChatCompletionResponse,
|
||||||
|
OpenAIChoice, OpenAIChoiceMessage, OpenAIUsage,
|
||||||
|
OpenAIStreamChoice, OpenAIStreamDelta
|
||||||
)
|
)
|
||||||
from mem0_manager import mem0_manager
|
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
|
# Configure structured logging
|
||||||
structlog.configure(
|
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
|
# Health check endpoint
|
||||||
@app.get("/health", response_model=HealthResponse)
|
@app.get("/health", response_model=HealthResponse)
|
||||||
async def health_check():
|
async def health_check():
|
||||||
|
|
@ -236,6 +252,178 @@ async def chat_with_memory(
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
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
|
# Memory management endpoints - pure Mem0 passthroughs
|
||||||
@app.post("/memories")
|
@app.post("/memories")
|
||||||
async def add_memories(
|
async def add_memories(
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,10 @@ class Mem0Manager:
|
||||||
"embedder": {
|
"embedder": {
|
||||||
"provider": "ollama",
|
"provider": "ollama",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "qwen3-embedding:4b-q8_0",
|
"model": "hf.co/Qwen/Qwen3-Embedding-0.6B-GGUF:Q8_0",
|
||||||
# "api_key": settings.embedder_api_key,
|
# "api_key": settings.embedder_api_key,
|
||||||
"ollama_base_url": "http://172.17.0.1:11434",
|
"ollama_base_url": "https://models.breezehq.dev",
|
||||||
"embedding_dims": 2560
|
"embedding_dims": 1024
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vector_store": {
|
"vector_store": {
|
||||||
|
|
@ -64,7 +64,7 @@ class Mem0Manager:
|
||||||
"collection_name": settings.qdrant_collection_name,
|
"collection_name": settings.qdrant_collection_name,
|
||||||
"host": settings.qdrant_host,
|
"host": settings.qdrant_host,
|
||||||
"port": settings.qdrant_port,
|
"port": settings.qdrant_port,
|
||||||
"embedding_model_dims": 2560,
|
"embedding_model_dims": 1024,
|
||||||
"on_disk": True
|
"on_disk": True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -138,3 +138,71 @@ class UserStatsResponse(BaseModel):
|
||||||
last_activity: Optional[str] = Field(None, description="Last activity timestamp")
|
last_activity: Optional[str] = Field(None, description="Last activity timestamp")
|
||||||
api_calls_today: int = Field(..., description="API calls made by this user today")
|
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")
|
||||||
|
|
@ -15,6 +15,8 @@ services:
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- mem0_network
|
||||||
|
|
||||||
# Neo4j with APOC for graph relationships
|
# Neo4j with APOC for graph relationships
|
||||||
neo4j:
|
neo4j:
|
||||||
|
|
@ -32,6 +34,8 @@ services:
|
||||||
expose:
|
expose:
|
||||||
- "7474" # HTTP - Internal only
|
- "7474" # HTTP - Internal only
|
||||||
- "7687" # Bolt - Internal only
|
- "7687" # Bolt - Internal only
|
||||||
|
networks:
|
||||||
|
- mem0_network
|
||||||
volumes:
|
volumes:
|
||||||
- neo4j_data:/data
|
- neo4j_data:/data
|
||||||
- neo4j_logs:/logs
|
- neo4j_logs:/logs
|
||||||
|
|
@ -47,8 +51,8 @@ services:
|
||||||
# Backend API service
|
# Backend API service
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: ./backend/Dockerfile
|
||||||
container_name: mem0-backend
|
container_name: mem0-backend
|
||||||
environment:
|
environment:
|
||||||
OPENAI_API_KEY: ${OPENAI_COMPAT_API_KEY}
|
OPENAI_API_KEY: ${OPENAI_COMPAT_API_KEY}
|
||||||
|
|
@ -65,8 +69,11 @@ services:
|
||||||
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000}
|
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000}
|
||||||
DEFAULT_MODEL: ${DEFAULT_MODEL:-claude-sonnet-4}
|
DEFAULT_MODEL: ${DEFAULT_MODEL:-claude-sonnet-4}
|
||||||
API_KEYS: ${API_KEYS:-{}}
|
API_KEYS: ${API_KEYS:-{}}
|
||||||
ports:
|
expose:
|
||||||
- "${BACKEND_PORT:-8000}:8000"
|
- 8000
|
||||||
|
networks:
|
||||||
|
- npm_network
|
||||||
|
- mem0_network
|
||||||
depends_on:
|
depends_on:
|
||||||
qdrant:
|
qdrant:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -75,7 +82,8 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./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:
|
volumes:
|
||||||
qdrant_data:
|
qdrant_data:
|
||||||
|
|
@ -85,5 +93,6 @@ volumes:
|
||||||
neo4j_plugins:
|
neo4j_plugins:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
mem0_network:
|
||||||
name: mem0-network
|
npm_network:
|
||||||
|
external: true
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,106 @@
|
||||||
display: flex;
|
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 {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Chat Section */
|
/* Chat Section */
|
||||||
.chat-section {
|
.chat-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -58,7 +152,12 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-chat-btn {
|
.header-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-chat-btn, .logout-btn {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
color: #666;
|
color: #666;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
|
|
@ -73,16 +172,27 @@
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-chat-btn:hover {
|
.clear-chat-btn:hover, .logout-btn:hover {
|
||||||
background: #e9ecef;
|
background: #e9ecef;
|
||||||
border-color: #ced4da;
|
border-color: #ced4da;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-chat-btn:active {
|
.clear-chat-btn:active, .logout-btn:active {
|
||||||
background: #dee2e6;
|
background: #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-color: #ffc107;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: #ffe69c;
|
||||||
|
border-color: #ffb300;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -281,17 +391,42 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 -->
|
<!-- Chat Section -->
|
||||||
<div class="chat-section">
|
<div class="chat-section">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<div class="chat-header-content">
|
<div class="chat-header-content">
|
||||||
<h1>What can I help you with?</h1>
|
<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>
|
||||||
|
<div class="header-buttons">
|
||||||
<button class="clear-chat-btn" id="clearChatBtn" title="Clear chat history">
|
<button class="clear-chat-btn" id="clearChatBtn" title="Clear chat history">
|
||||||
🗑️ Clear Chat
|
🗑️ Clear Chat
|
||||||
</button>
|
</button>
|
||||||
|
<button class="logout-btn" id="logoutBtn" title="Logout">
|
||||||
|
🚪 Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-messages" id="chatMessages">
|
<div class="chat-messages" id="chatMessages">
|
||||||
|
|
@ -319,10 +454,20 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Configuration
|
// Configuration
|
||||||
const API_BASE = 'http://localhost:8000';
|
const API_BASE = window.location.origin;
|
||||||
const USER_ID = 'pratik';
|
|
||||||
|
// State
|
||||||
|
let API_KEY = null;
|
||||||
|
let USER_ID = null;
|
||||||
|
|
||||||
// DOM Elements
|
// 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 chatMessages = document.getElementById('chatMessages');
|
||||||
const messageInput = document.getElementById('messageInput');
|
const messageInput = document.getElementById('messageInput');
|
||||||
const sendButton = document.getElementById('sendButton');
|
const sendButton = document.getElementById('sendButton');
|
||||||
|
|
@ -336,19 +481,143 @@
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadChatHistory();
|
// Check if already logged in
|
||||||
loadMemories();
|
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
|
// Event listeners
|
||||||
|
loginButton.addEventListener('click', handleLogin);
|
||||||
|
apiKeyInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') handleLogin();
|
||||||
|
});
|
||||||
|
logoutBtn.addEventListener('click', handleLogout);
|
||||||
sendButton.addEventListener('click', sendMessage);
|
sendButton.addEventListener('click', sendMessage);
|
||||||
messageInput.addEventListener('keydown', handleKeyDown);
|
messageInput.addEventListener('keydown', handleKeyDown);
|
||||||
messageInput.addEventListener('input', autoResizeTextarea);
|
messageInput.addEventListener('input', autoResizeTextarea);
|
||||||
refreshButton.addEventListener('click', loadMemories);
|
refreshButton.addEventListener('click', loadMemories);
|
||||||
clearChatBtn.addEventListener('click', clearChatWithConfirmation);
|
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
|
// Initialize textarea height
|
||||||
autoResizeTextarea();
|
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
|
// Load chat history from localStorage
|
||||||
function loadChatHistory() {
|
function loadChatHistory() {
|
||||||
|
|
@ -419,6 +688,7 @@
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': API_KEY
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: message,
|
message: message,
|
||||||
|
|
@ -460,7 +730,11 @@
|
||||||
// Load memories from backend
|
// Load memories from backend
|
||||||
async function loadMemories() {
|
async function loadMemories() {
|
||||||
try{
|
try{
|
||||||
const response = await fetch(`${API_BASE}/memories/${USER_ID}?limit=50`);
|
const response = await fetch(`${API_BASE}/memories/${USER_ID}?limit=50`, {
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': API_KEY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
|
@ -512,8 +786,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/memories/${memoryId}`, {
|
const response = await fetch(`${API_BASE}/memories/${memoryId}?user_id=${USER_ID}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': API_KEY
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue