Spaces:
Paused
Paused
| 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 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 | |
| from mem0.exceptions import ValidationError as Mem0ValidationError | |
| 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", "localhost") | |
| POSTGRES_PORT = os.environ.get("POSTGRES_PORT", "5432") | |
| POSTGRES_DB = os.environ.get("POSTGRES_DB", "postgres") | |
| POSTGRES_USER = os.environ.get("POSTGRES_USER", "mem0") | |
| POSTGRES_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "postgres") | |
| POSTGRES_COLLECTION_NAME = os.environ.get("POSTGRES_COLLECTION_NAME", "memories") | |
| # Placeholder keeps the OpenAI client constructible when no key is configured | |
| # yet (fresh deploy before /api/configure is called). Real calls will 401 until | |
| # a key is set via env or the config API. | |
| OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") or "sk-not-configured" | |
| HISTORY_DB_PATH = os.environ.get("HISTORY_DB_PATH", "/opt/data/history/history.db") | |
| # LLM config — set via env for persistent defaults without needing DB | |
| DEFAULT_LLM_PROVIDER = os.environ.get("MEM0_LLM_PROVIDER", "openai") | |
| DEFAULT_LLM_MODEL = os.environ.get("MEM0_LLM_MODEL", "gpt-4o-mini") | |
| DEFAULT_LLM_API_KEY = os.environ.get("MEM0_LLM_API_KEY") or OPENAI_API_KEY | |
| DEFAULT_LLM_BASE_URL = os.environ.get("MEM0_LLM_BASE_URL") # None = use provider default | |
| DEFAULT_LLM_MAX_TOKENS = int(os.environ.get("MEM0_LLM_MAX_TOKENS", "4000")) | |
| DEFAULT_LLM_TEMP = float(os.environ.get("MEM0_LLM_TEMPERATURE", "0.2")) | |
| # Embedder config | |
| DEFAULT_EMBED_PROVIDER = os.environ.get("MEM0_EMBEDDER_PROVIDER", "openai") | |
| DEFAULT_EMBED_MODEL = os.environ.get("MEM0_EMBEDDER_MODEL", "text-embedding-3-small") | |
| DEFAULT_EMBED_API_KEY = os.environ.get("MEM0_EMBEDDER_API_KEY") or OPENAI_API_KEY | |
| DEFAULT_EMBED_BASE_URL = os.environ.get("MEM0_EMBEDDER_BASE_URL") # None = use provider default | |
| DEFAULT_EMBED_DIMS = os.environ.get("MEM0_EMBEDDER_DIMS") # None = use model default | |
| def _build_llm_config() -> dict: | |
| cfg: dict = {"api_key": DEFAULT_LLM_API_KEY, "model": DEFAULT_LLM_MODEL, | |
| "temperature": DEFAULT_LLM_TEMP, "max_tokens": DEFAULT_LLM_MAX_TOKENS} | |
| if DEFAULT_LLM_BASE_URL: | |
| cfg["openai_base_url"] = DEFAULT_LLM_BASE_URL | |
| return cfg | |
| def _build_embedder_config() -> dict: | |
| cfg: dict = {"api_key": DEFAULT_EMBED_API_KEY, "model": DEFAULT_EMBED_MODEL} | |
| if DEFAULT_EMBED_BASE_URL: | |
| cfg["openai_base_url"] = DEFAULT_EMBED_BASE_URL | |
| if DEFAULT_EMBED_DIMS: | |
| cfg["embedding_dims"] = int(DEFAULT_EMBED_DIMS) | |
| return cfg | |
| 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": DEFAULT_LLM_PROVIDER, "config": _build_llm_config()}, | |
| "embedder": {"provider": DEFAULT_EMBED_PROVIDER, "config": _build_embedder_config()}, | |
| "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:7860") | |
| 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() | |
| 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"), | |
| ) | |
| def get_config(_auth=Depends(verify_auth)): | |
| return _redact_config(get_current_config()) | |
| def list_bundled_providers(_auth=Depends(verify_auth)): | |
| return {"llm": list(BUNDLED_LLM_PROVIDERS), "embedder": list(BUNDLED_EMBEDDER_PROVIDERS)} | |
| 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"} | |
| 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() | |
| 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]} | |
| 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() | |
| 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() | |
| 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() | |
| 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() | |
| 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() | |
| 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() | |
| 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() | |
| 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() | |
| def home(): | |
| """Redirect to the OpenAPI documentation.""" | |
| return RedirectResponse(url="/docs") | |
| def health(): | |
| """Health check endpoint.""" | |
| return {"status": "healthy"} |