555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
560 lines
22 KiB
Python
560 lines
22 KiB
Python
import asyncio
|
|
import logging
|
|
import os
|
|
import time
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import telemetry
|
|
from auth import ADMIN_API_KEY, AUTH_DISABLED, JWT_SECRET, require_admin, verify_auth
|
|
from db import SessionLocal
|
|
from dotenv import load_dotenv
|
|
from errors import (
|
|
UpstreamError,
|
|
install_request_id_logging,
|
|
new_request_id,
|
|
request_id_var,
|
|
upstream_error,
|
|
upstream_error_handler,
|
|
)
|
|
from fastapi import Depends, FastAPI, HTTPException, Query, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse, RedirectResponse
|
|
from mem0.exceptions import ValidationError as Mem0ValidationError
|
|
from models import RequestLog, User
|
|
from pydantic import BaseModel, Field
|
|
from rate_limit import limiter
|
|
from routers import api_keys as api_keys_router
|
|
from routers import auth as auth_router
|
|
from routers import entities as entities_router
|
|
from routers import requests as requests_router
|
|
from schemas import MessageResponse
|
|
from server_state import (
|
|
get_current_config,
|
|
get_memory_instance,
|
|
initialize_state,
|
|
set_session_factory,
|
|
update_config,
|
|
)
|
|
from slowapi import _rate_limit_exceeded_handler
|
|
from slowapi.errors import RateLimitExceeded
|
|
from sqlalchemy import func, select
|
|
|
|
load_dotenv()
|
|
|
|
install_request_id_logging()
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - [%(request_id)s] %(message)s")
|
|
|
|
MIN_KEY_LENGTH = 16
|
|
SENSITIVE_CONFIG_KEYS = {
|
|
"admin_api_key",
|
|
"api_key",
|
|
"authorization",
|
|
"jwt_secret",
|
|
"password",
|
|
"password_hash",
|
|
"secret",
|
|
"token",
|
|
}
|
|
SKIPPED_REQUEST_LOG_PATHS = {"/api/health", "/docs", "/redoc", "/openapi.json"}
|
|
SKIPPED_REQUEST_LOG_PREFIXES = ("/requests",)
|
|
|
|
BUNDLED_LLM_PROVIDERS = ("openai", "anthropic", "gemini")
|
|
BUNDLED_EMBEDDER_PROVIDERS = ("openai", "gemini")
|
|
|
|
|
|
def _warn_if_unconfigured() -> None:
|
|
"""Pre-auth deployments upgrading into this build will 401 everywhere until
|
|
an admin key or admin user exists. Surface the fix before the support tickets."""
|
|
try:
|
|
with SessionLocal() as session:
|
|
if session.scalar(select(func.count(User.id))) > 0:
|
|
return
|
|
except Exception:
|
|
return
|
|
|
|
logging.warning(
|
|
"\n%s\n"
|
|
" Auth is enabled by default and this server has no admin configured.\n"
|
|
" Protected endpoints will return 401 until you either:\n"
|
|
" 1. Set ADMIN_API_KEY=<long-random-value> (fastest, no client changes)\n"
|
|
" 2. Register an admin at http://<host>:3000/setup\n"
|
|
" 3. Set AUTH_DISABLED=true (local development only)\n"
|
|
" Docs: https://docs.mem0.ai/open-source/features/rest-api#authentication\n"
|
|
"%s",
|
|
"=" * 72,
|
|
"=" * 72,
|
|
)
|
|
|
|
|
|
if not AUTH_DISABLED and not JWT_SECRET:
|
|
raise RuntimeError(
|
|
"JWT_SECRET is required. Set it in .env (generate with `openssl rand -base64 48`) "
|
|
"or set AUTH_DISABLED=true for local development only."
|
|
)
|
|
|
|
if AUTH_DISABLED:
|
|
logging.warning("AUTH_DISABLED is enabled. Protected endpoints are open for local development only.")
|
|
elif ADMIN_API_KEY and len(ADMIN_API_KEY) < MIN_KEY_LENGTH:
|
|
logging.warning(
|
|
"ADMIN_API_KEY is shorter than %d characters - consider using a longer key for production.",
|
|
MIN_KEY_LENGTH,
|
|
)
|
|
elif not ADMIN_API_KEY:
|
|
_warn_if_unconfigured()
|
|
|
|
telemetry.log_status()
|
|
|
|
POSTGRES_HOST = os.environ.get("POSTGRES_HOST", "postgres")
|
|
POSTGRES_PORT = os.environ.get("POSTGRES_PORT", "5432")
|
|
POSTGRES_DB = os.environ.get("POSTGRES_DB", "postgres")
|
|
POSTGRES_USER = os.environ.get("POSTGRES_USER", "postgres")
|
|
POSTGRES_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres")
|
|
POSTGRES_COLLECTION_NAME = os.environ.get("POSTGRES_COLLECTION_NAME", "memories")
|
|
|
|
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
|
HISTORY_DB_PATH = os.environ.get("HISTORY_DB_PATH", "/app/history/history.db")
|
|
DEFAULT_LLM_MODEL = os.environ.get("MEM0_DEFAULT_LLM_MODEL", "gpt-4.1-nano-2025-04-14")
|
|
DEFAULT_EMBEDDER_MODEL = os.environ.get("MEM0_DEFAULT_EMBEDDER_MODEL", "text-embedding-3-small")
|
|
|
|
DEFAULT_CONFIG = {
|
|
"version": "v1.1",
|
|
"vector_store": {
|
|
"provider": "pgvector",
|
|
"config": {
|
|
"host": POSTGRES_HOST,
|
|
"port": int(POSTGRES_PORT),
|
|
"dbname": POSTGRES_DB,
|
|
"user": POSTGRES_USER,
|
|
"password": POSTGRES_PASSWORD,
|
|
"collection_name": POSTGRES_COLLECTION_NAME,
|
|
},
|
|
},
|
|
"llm": {
|
|
"provider": "openai",
|
|
"config": {"api_key": OPENAI_API_KEY, "temperature": 0.2, "model": DEFAULT_LLM_MODEL},
|
|
},
|
|
"embedder": {"provider": "openai", "config": {"api_key": OPENAI_API_KEY, "model": DEFAULT_EMBEDDER_MODEL}},
|
|
"history_db_path": HISTORY_DB_PATH,
|
|
}
|
|
|
|
|
|
set_session_factory(SessionLocal)
|
|
initialize_state(DEFAULT_CONFIG)
|
|
|
|
|
|
app = FastAPI(
|
|
title="Mem0 REST APIs",
|
|
description=(
|
|
"A REST API for managing and searching memories for your AI Agents and Apps.\n\n"
|
|
"## Authentication\n"
|
|
"Supports Bearer JWT tokens, per-user API keys via `X-API-Key` header, "
|
|
"or the legacy `ADMIN_API_KEY` environment variable. Set `AUTH_DISABLED=true` for local development only."
|
|
),
|
|
version="1.0.0",
|
|
redirect_slashes=False,
|
|
)
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
app.add_exception_handler(UpstreamError, upstream_error_handler)
|
|
DASHBOARD_URL = os.environ.get("DASHBOARD_URL", "http://localhost:3000")
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[DASHBOARD_URL],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(auth_router.router)
|
|
app.include_router(api_keys_router.router)
|
|
app.include_router(entities_router.router)
|
|
app.include_router(requests_router.router)
|
|
|
|
|
|
class Message(BaseModel):
|
|
role: str = Field(..., description="Role of the message (user or assistant).")
|
|
content: str = Field(..., description="Message content.")
|
|
|
|
|
|
class MemoryCreate(BaseModel):
|
|
messages: List[Message] = Field(..., description="List of messages to store.")
|
|
user_id: Optional[str] = None
|
|
agent_id: Optional[str] = None
|
|
run_id: Optional[str] = None
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format.")
|
|
infer: Optional[bool] = Field(None, description="Whether to extract facts from messages. Defaults to True.")
|
|
memory_type: Optional[str] = Field(None, description="Type of memory to store (e.g. 'core').")
|
|
prompt: Optional[str] = Field(None, description="Custom prompt to use for fact extraction.")
|
|
|
|
|
|
class MemoryUpdate(BaseModel):
|
|
text: Optional[str] = Field(None, description="New content to update the memory with.")
|
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Metadata to update.")
|
|
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format, or null to clear.")
|
|
|
|
|
|
class SearchRequest(BaseModel):
|
|
query: str = Field(..., description="Search query.")
|
|
user_id: Optional[str] = Field(None, description="Deprecated: pass inside `filters` instead.", deprecated=True)
|
|
run_id: Optional[str] = Field(None, description="Deprecated: pass inside `filters` instead.", deprecated=True)
|
|
agent_id: Optional[str] = Field(None, description="Deprecated: pass inside `filters` instead.", deprecated=True)
|
|
filters: Optional[Dict[str, Any]] = None
|
|
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
|
|
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
|
|
explain: Optional[bool] = Field(None, description="Include score details for each search result.")
|
|
show_expired: Optional[bool] = Field(None, description="Include expired memories.")
|
|
|
|
|
|
class GenerateInstructionsRequest(BaseModel):
|
|
use_case: str = Field(..., description="Description of what the user will use Mem0 for.")
|
|
|
|
|
|
def _client_error(exc: Exception) -> HTTPException:
|
|
"""Map core validation / not-found errors to 4xx so clients can tell a bad
|
|
request from an upstream outage. 'not found' is a 404, everything else a 400."""
|
|
detail = str(exc)
|
|
status_code = 404 if isinstance(exc, ValueError) and "not found" in detail.lower() else 400
|
|
return HTTPException(status_code=status_code, detail=detail)
|
|
|
|
|
|
def _redact_config(value: Any, key: str | None = None) -> Any:
|
|
if isinstance(value, dict):
|
|
return {item_key: _redact_config(item_value, item_key) for item_key, item_value in value.items()}
|
|
if isinstance(value, list):
|
|
return [_redact_config(item_value, key) for item_value in value]
|
|
if key is not None and key.lower() in SENSITIVE_CONFIG_KEYS:
|
|
return "[redacted]" if value else value
|
|
return value
|
|
|
|
|
|
def _validate_bundled_providers(config: Dict[str, Any]) -> None:
|
|
llm = config.get("llm")
|
|
if isinstance(llm, dict) and (provider := llm.get("provider")) and provider not in BUNDLED_LLM_PROVIDERS:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
f"LLM provider '{provider}' is not bundled in this image. "
|
|
f"Bundled providers: {', '.join(BUNDLED_LLM_PROVIDERS)}. "
|
|
"To use another provider, install its Python package, rebuild the container, "
|
|
"and extend BUNDLED_LLM_PROVIDERS in server/main.py."
|
|
),
|
|
)
|
|
|
|
embedder = config.get("embedder")
|
|
if (
|
|
isinstance(embedder, dict)
|
|
and (provider := embedder.get("provider"))
|
|
and provider not in BUNDLED_EMBEDDER_PROVIDERS
|
|
):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=(
|
|
f"Embedder provider '{provider}' is not bundled in this image. "
|
|
f"Bundled providers: {', '.join(BUNDLED_EMBEDDER_PROVIDERS)}. "
|
|
"To use another provider, install its Python package, rebuild the container, "
|
|
"and extend BUNDLED_EMBEDDER_PROVIDERS in server/main.py."
|
|
),
|
|
)
|
|
|
|
|
|
def _should_log_request(request: Request) -> bool:
|
|
if request.method == "OPTIONS":
|
|
return False
|
|
path = request.url.path
|
|
if path in SKIPPED_REQUEST_LOG_PATHS:
|
|
return False
|
|
return not path.startswith(SKIPPED_REQUEST_LOG_PREFIXES)
|
|
|
|
|
|
def _persist_request_log(method: str, path: str, status_code: int, latency_ms: float, auth_type: str) -> None:
|
|
session = SessionLocal()
|
|
|
|
try:
|
|
session.add(
|
|
RequestLog(
|
|
method=method,
|
|
path=path,
|
|
status_code=status_code,
|
|
latency_ms=latency_ms,
|
|
auth_type=auth_type,
|
|
)
|
|
)
|
|
session.commit()
|
|
except Exception:
|
|
session.rollback()
|
|
logging.exception("Failed to persist request log")
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@app.middleware("http")
|
|
async def log_requests(request: Request, call_next):
|
|
request.state.auth_type = getattr(request.state, "auth_type", "none")
|
|
rid = new_request_id()
|
|
token = request_id_var.set(rid)
|
|
start = time.perf_counter()
|
|
status_code = 500
|
|
|
|
try:
|
|
response = await call_next(request)
|
|
status_code = response.status_code
|
|
response.headers["X-Request-ID"] = rid
|
|
return response
|
|
except Exception:
|
|
status_code = 500
|
|
raise
|
|
finally:
|
|
request_id_var.reset(token)
|
|
if _should_log_request(request):
|
|
asyncio.get_running_loop().run_in_executor(
|
|
None,
|
|
_persist_request_log,
|
|
request.method,
|
|
request.url.path,
|
|
status_code,
|
|
round((time.perf_counter() - start) * 1000, 2),
|
|
getattr(request.state, "auth_type", "none"),
|
|
)
|
|
|
|
|
|
@app.get("/configure", summary="Get current Mem0 configuration")
|
|
def get_config(_auth=Depends(verify_auth)):
|
|
return _redact_config(get_current_config())
|
|
|
|
|
|
@app.get("/configure/providers", summary="List bundled LLM and embedder providers")
|
|
def list_bundled_providers(_auth=Depends(verify_auth)):
|
|
return {"llm": list(BUNDLED_LLM_PROVIDERS), "embedder": list(BUNDLED_EMBEDDER_PROVIDERS)}
|
|
|
|
|
|
@app.post("/configure", summary="Configure Mem0")
|
|
def set_config(config: Dict[str, Any], _auth=Depends(require_admin)):
|
|
"""Set memory configuration. Requires admin role."""
|
|
_validate_bundled_providers(config)
|
|
update_config(config)
|
|
return {"message": "Configuration set successfully"}
|
|
|
|
|
|
@app.post("/generate-instructions", summary="Generate custom instructions from a use case")
|
|
def generate_instructions(req: GenerateInstructionsRequest, _auth=Depends(verify_auth)):
|
|
"""Generate custom instructions and a contextual test message tailored to a use case."""
|
|
try:
|
|
llm = get_memory_instance().llm
|
|
prompt = (
|
|
"You are configuring a memory system. Given the use case below, produce two things:\n"
|
|
"1. INSTRUCTIONS: A short paragraph of custom instructions telling the memory extraction system "
|
|
"what kinds of facts, preferences, and context to prioritize. Be specific to the use case.\n"
|
|
"2. TEST_MESSAGE: A single realistic sentence a user in this use case would say, suitable for "
|
|
"testing that the memory system works.\n\n"
|
|
"Respond in exactly this format (no markdown, no extra text):\n"
|
|
"INSTRUCTIONS: <your instructions>\n"
|
|
f"TEST_MESSAGE: <your test message>\n\nUse case: {req.use_case}"
|
|
)
|
|
response = llm.generate_response([{"role": "user", "content": prompt}])
|
|
instructions = response
|
|
test_message = "I like to hike on weekends."
|
|
if "INSTRUCTIONS:" in response and "TEST_MESSAGE:" in response:
|
|
parts = response.split("TEST_MESSAGE:")
|
|
instructions = parts[0].replace("INSTRUCTIONS:", "").strip()
|
|
test_message = parts[1].strip()
|
|
return {"custom_instructions": instructions, "test_message": test_message}
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.post("/memories", summary="Create memories")
|
|
def add_memory(memory_create: MemoryCreate, _auth=Depends(verify_auth)):
|
|
"""Store new memories."""
|
|
if not any([memory_create.user_id, memory_create.agent_id, memory_create.run_id]):
|
|
raise HTTPException(status_code=400, detail="At least one identifier (user_id, agent_id, run_id) is required.")
|
|
|
|
params = {k: v for k, v in memory_create.model_dump().items() if v is not None and k != "messages"}
|
|
try:
|
|
response = get_memory_instance().add(messages=[m.model_dump() for m in memory_create.messages], **params)
|
|
if response.get("results"):
|
|
telemetry.log_dashboard_nudge_once(DASHBOARD_URL)
|
|
return JSONResponse(content=response)
|
|
except (ValueError, Mem0ValidationError) as e:
|
|
raise _client_error(e)
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
ALL_MEMORIES_LIMIT = 1000
|
|
_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at", "expiration_date"}
|
|
|
|
|
|
def _serialize_memory(row: Any) -> Dict[str, Any]:
|
|
payload = getattr(row, "payload", None) or {}
|
|
return {
|
|
"id": getattr(row, "id", None),
|
|
"memory": payload.get("data"),
|
|
"user_id": payload.get("user_id"),
|
|
"agent_id": payload.get("agent_id"),
|
|
"run_id": payload.get("run_id"),
|
|
"hash": payload.get("hash"),
|
|
"expiration_date": payload.get("expiration_date"),
|
|
"metadata": {k: v for k, v in payload.items() if k not in _RESERVED_PAYLOAD_KEYS},
|
|
"created_at": payload.get("created_at"),
|
|
"updated_at": payload.get("updated_at"),
|
|
}
|
|
|
|
|
|
def _list_all_memories(limit: int = ALL_MEMORIES_LIMIT) -> Dict[str, Any]:
|
|
results = get_memory_instance().vector_store.list(top_k=limit)
|
|
rows = results[0] if results and isinstance(results, list) and isinstance(results[0], list) else results or []
|
|
return {"results": [_serialize_memory(row) for row in rows]}
|
|
|
|
|
|
@app.get("/memories", summary="Get memories")
|
|
def get_all_memories(
|
|
request: Request,
|
|
user_id: Optional[str] = None,
|
|
run_id: Optional[str] = None,
|
|
agent_id: Optional[str] = None,
|
|
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
|
|
show_expired: bool = Query(False),
|
|
_auth=Depends(verify_auth),
|
|
):
|
|
"""Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
|
|
try:
|
|
if not any([user_id, run_id, agent_id]):
|
|
auth_type = getattr(request.state, "auth_type", "none")
|
|
if _auth is not None and _auth.role != "admin" and auth_type not in {"admin_api_key", "disabled"}:
|
|
raise HTTPException(status_code=403, detail="Admin role required to list all memories.")
|
|
# Admin all-memory listing is intentionally raw; scoped get_all below applies expiry visibility.
|
|
return _list_all_memories(limit=top_k if top_k is not None else ALL_MEMORIES_LIMIT)
|
|
filters = {
|
|
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v
|
|
}
|
|
params = {"filters": filters}
|
|
if top_k is not None:
|
|
params["top_k"] = top_k
|
|
params["show_expired"] = show_expired
|
|
return get_memory_instance().get_all(**params)
|
|
except HTTPException:
|
|
raise
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.get("/memories/{memory_id}", summary="Get a memory")
|
|
def get_memory(memory_id: str, _auth=Depends(verify_auth)):
|
|
"""Retrieve a specific memory by ID."""
|
|
try:
|
|
return get_memory_instance().get(memory_id)
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.post("/search", summary="Search memories")
|
|
def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
|
|
"""Search for memories based on a query."""
|
|
try:
|
|
filters = search_req.filters or {}
|
|
deprecated_keys = []
|
|
for entity_key in ("user_id", "agent_id", "run_id"):
|
|
entity_val = getattr(search_req, entity_key, None)
|
|
if entity_val:
|
|
filters[entity_key] = entity_val
|
|
deprecated_keys.append(entity_key)
|
|
if deprecated_keys:
|
|
logging.warning(
|
|
"Top-level %s in /search is deprecated. Use filters={%s} instead.",
|
|
", ".join(deprecated_keys),
|
|
", ".join(f'"{k}": "..."' for k in deprecated_keys),
|
|
)
|
|
params = {}
|
|
if search_req.top_k is not None:
|
|
params["top_k"] = search_req.top_k
|
|
if search_req.threshold is not None:
|
|
params["threshold"] = search_req.threshold
|
|
if search_req.explain is not None:
|
|
params["explain"] = search_req.explain
|
|
if search_req.show_expired is not None:
|
|
params["show_expired"] = search_req.show_expired
|
|
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except HTTPException:
|
|
raise
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.put("/memories/{memory_id}", summary="Update a memory")
|
|
def update_memory(memory_id: str, updated_memory: MemoryUpdate, _auth=Depends(verify_auth)):
|
|
"""Update an existing memory."""
|
|
try:
|
|
fields_set = getattr(updated_memory, "model_fields_set", getattr(updated_memory, "__fields_set__", set()))
|
|
params = {"memory_id": memory_id}
|
|
if "text" in fields_set:
|
|
params["data"] = updated_memory.text
|
|
if "metadata" in fields_set:
|
|
params["metadata"] = updated_memory.metadata
|
|
if "expiration_date" in fields_set:
|
|
params["expiration_date"] = updated_memory.expiration_date
|
|
return get_memory_instance().update(**params)
|
|
except (ValueError, Mem0ValidationError) as e:
|
|
raise _client_error(e)
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.get("/memories/{memory_id}/history", summary="Get memory history")
|
|
def memory_history(memory_id: str, _auth=Depends(verify_auth)):
|
|
"""Retrieve memory history."""
|
|
try:
|
|
return get_memory_instance().history(memory_id=memory_id)
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.delete("/memories/{memory_id}", summary="Delete a memory", response_model=MessageResponse)
|
|
def delete_memory(memory_id: str, _auth=Depends(verify_auth)):
|
|
"""Delete a specific memory by ID."""
|
|
try:
|
|
get_memory_instance().delete(memory_id=memory_id)
|
|
return MessageResponse(message="Memory deleted successfully")
|
|
except (ValueError, Mem0ValidationError) as e:
|
|
raise _client_error(e)
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.delete("/memories", summary="Delete all memories", response_model=MessageResponse)
|
|
def delete_all_memories(
|
|
user_id: Optional[str] = None,
|
|
run_id: Optional[str] = None,
|
|
agent_id: Optional[str] = None,
|
|
_auth=Depends(require_admin),
|
|
):
|
|
"""Delete all memories for a given identifier. Requires admin role."""
|
|
if not any([user_id, run_id, agent_id]):
|
|
raise HTTPException(status_code=400, detail="At least one identifier is required.")
|
|
try:
|
|
params = {
|
|
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v
|
|
}
|
|
get_memory_instance().delete_all(**params)
|
|
return MessageResponse(message="All relevant memories deleted")
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.post("/reset", summary="Reset all memories")
|
|
def reset_memory(_auth=Depends(require_admin)):
|
|
"""Completely reset stored memories. Requires admin role."""
|
|
try:
|
|
get_memory_instance().reset()
|
|
return {"message": "All memories reset"}
|
|
except Exception:
|
|
raise upstream_error()
|
|
|
|
|
|
@app.get("/", summary="Redirect to the OpenAPI documentation", include_in_schema=False)
|
|
def home():
|
|
"""Redirect to the OpenAPI documentation."""
|
|
return RedirectResponse(url="/docs")
|