From 2bb285752ef8d72c33fbfb145b6a1072c3f1f8f3 Mon Sep 17 00:00:00 2001 From: yjy Date: Mon, 22 Sep 2025 17:05:37 +0800 Subject: [PATCH 01/21] docker start --- docker/docker-compose.yml | 2 +- docker/requirements.txt | 297 +++++++++--------- src/memos/api/config.py | 16 +- src/memos/api/routers/start_router.py | 305 +++++++++++++++++++ src/memos/api/start_api.py | 419 ++------------------------ src/memos/api/start_config.py | 144 +++++++++ src/memos/api/start_models.py | 147 +++++++++ 7 files changed, 790 insertions(+), 540 deletions(-) create mode 100644 src/memos/api/routers/start_router.py create mode 100644 src/memos/api/start_config.py create mode 100644 src/memos/api/start_models.py diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d8998b6f7..40bf5e6c5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -44,7 +44,7 @@ services: - memos_network qdrant: - image: qdrant/qdrant:v1.15.0 + image: qdrant/qdrant:v1.15.3 container_name: qdrant-docker ports: - "6333:6333" # REST API diff --git a/docker/requirements.txt b/docker/requirements.txt index 211ec3cae..d20c0b36e 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -1,137 +1,160 @@ -annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0" -anyio==4.9.0 ; python_version >= "3.10" and python_version < "4.0" -attrs==25.3.0 ; python_version >= "3.10" and python_version < "4.0" -authlib==1.6.0 ; python_version >= "3.10" and python_version < "4.0" -beautifulsoup4==4.13.4 ; python_version >= "3.10" and python_version < "4.0" -certifi==2025.7.14 ; python_version >= "3.10" and python_version < "4.0" -cffi==1.17.1 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy" -cfgv==3.4.0 ; python_version >= "3.10" and python_version < "4.0" -charset-normalizer==3.4.2 ; python_version >= "3.10" and python_version < "4.0" -chonkie==1.1.1 ; python_version >= "3.10" and python_version < "4.0" -click==8.2.1 ; python_version >= "3.10" and python_version < "4.0" -cobble==0.1.4 ; python_version >= "3.10" and python_version < "4.0" -colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (platform_system == "Windows" or sys_platform == "win32") -coloredlogs==15.0.1 ; python_version >= "3.10" and python_version < "4.0" -cryptography==45.0.5 ; python_version >= "3.10" and python_version < "4.0" -cyclopts==3.22.2 ; python_version >= "3.10" and python_version < "4.0" -defusedxml==0.7.1 ; python_version >= "3.10" and python_version < "4.0" -distlib==0.4.0 ; python_version >= "3.10" and python_version < "4.0" -distro==1.9.0 ; python_version >= "3.10" and python_version < "4.0" -dnspython==2.7.0 ; python_version >= "3.10" and python_version < "4.0" -docstring-parser==0.16 ; python_version >= "3.10" and python_version < "4.0" -docutils==0.21.2 ; python_version >= "3.10" and python_version < "4.0" -email-validator==2.2.0 ; python_version >= "3.10" and python_version < "4.0" -et-xmlfile==2.0.0 ; python_version >= "3.10" and python_version < "4.0" -exceptiongroup==1.3.0 ; python_version >= "3.10" and python_version < "4.0" -fastapi-cli==0.0.8 ; python_version >= "3.10" and python_version < "4.0" -fastapi-cloud-cli==0.1.4 ; python_version >= "3.10" and python_version < "4.0" -fastapi==0.115.14 ; python_version >= "3.10" and python_version < "4.0" -fastmcp==2.10.5 ; python_version >= "3.10" and python_version < "4.0" -filelock==3.18.0 ; python_version >= "3.10" and python_version < "4.0" -flatbuffers==25.2.10 ; python_version >= "3.10" and python_version < "4.0" -fsspec==2025.7.0 ; python_version >= "3.10" and python_version < "4.0" -greenlet==3.2.3 ; python_version >= "3.10" and python_version < "3.14" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") -h11==0.16.0 ; python_version >= "3.10" and python_version < "4.0" -hf-xet==1.1.5 ; python_version >= "3.10" and python_version < "4.0" and (platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "arm64" or platform_machine == "aarch64") -httpcore==1.0.9 ; python_version >= "3.10" and python_version < "4.0" -httptools==0.6.4 ; python_version >= "3.10" and python_version < "4.0" -httpx-sse==0.4.1 ; python_version >= "3.10" and python_version < "4.0" -httpx==0.28.1 ; python_version >= "3.10" and python_version < "4.0" -huggingface-hub==0.33.4 ; python_version >= "3.10" and python_version < "4.0" -humanfriendly==10.0 ; python_version >= "3.10" and python_version < "4.0" -identify==2.6.12 ; python_version >= "3.10" and python_version < "4.0" -idna==3.10 ; python_version >= "3.10" and python_version < "4.0" -iniconfig==2.1.0 ; python_version >= "3.10" and python_version < "4.0" -itsdangerous==2.2.0 ; python_version >= "3.10" and python_version < "4.0" -jinja2==3.1.6 ; python_version >= "3.10" and python_version < "4.0" -jiter==0.10.0 ; python_version >= "3.10" and python_version < "4.0" -joblib==1.5.1 ; python_version >= "3.10" and python_version < "4.0" -jsonschema-specifications==2025.4.1 ; python_version >= "3.10" and python_version < "4.0" -jsonschema==4.24.1 ; python_version >= "3.10" and python_version < "4.0" -lxml==6.0.0 ; python_version >= "3.10" and python_version < "4.0" -magika==0.6.2 ; python_version >= "3.10" and python_version < "4.0" -mammoth==1.9.1 ; python_version >= "3.10" and python_version < "4.0" -markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "4.0" -markdownify==1.1.0 ; python_version >= "3.10" and python_version < "4.0" -markitdown==0.1.2 ; python_version >= "3.10" and python_version < "4.0" -markupsafe==3.0.2 ; python_version >= "3.10" and python_version < "4.0" -mcp==1.12.0 ; python_version >= "3.10" and python_version < "4.0" -mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0" -mpmath==1.3.0 ; python_version >= "3.10" and python_version < "4.0" -neo4j==5.28.1 ; python_version >= "3.10" and python_version < "4.0" -nodeenv==1.9.1 ; python_version >= "3.10" and python_version < "4.0" -numpy==2.2.6 ; python_version == "3.10" -numpy==2.3.1 ; python_version >= "3.11" and python_version < "4.0" -ollama==0.4.9 ; python_version >= "3.10" and python_version < "4.0" -onnxruntime==1.22.1 ; python_version >= "3.10" and python_version < "4.0" -openai==1.97.0 ; python_version >= "3.10" and python_version < "4.0" -openapi-pydantic==0.5.1 ; python_version >= "3.10" and python_version < "4.0" -openpyxl==3.1.5 ; python_version >= "3.10" and python_version < "4.0" -orjson==3.11.0 ; python_version >= "3.10" and python_version < "4.0" -packaging==25.0 ; python_version >= "3.10" and python_version < "4.0" -pandas==2.3.1 ; python_version >= "3.10" and python_version < "4.0" -pdfminer-six==20250506 ; python_version >= "3.10" and python_version < "4.0" -pillow==11.3.0 ; python_version >= "3.10" and python_version < "4.0" -platformdirs==4.3.8 ; python_version >= "3.10" and python_version < "4.0" -pluggy==1.6.0 ; python_version >= "3.10" and python_version < "4.0" -pre-commit==4.2.0 ; python_version >= "3.10" and python_version < "4.0" -protobuf==6.31.1 ; python_version >= "3.10" and python_version < "4.0" -pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy" -pydantic-core==2.33.2 ; python_version >= "3.10" and python_version < "4.0" -pydantic-extra-types==2.10.5 ; python_version >= "3.10" and python_version < "4.0" -pydantic-settings==2.10.1 ; python_version >= "3.10" and python_version < "4.0" -pydantic==2.11.7 ; python_version >= "3.10" and python_version < "4.0" -pygments==2.19.2 ; python_version >= "3.10" and python_version < "4.0" -pyperclip==1.9.0 ; python_version >= "3.10" and python_version < "4.0" -pyreadline3==3.5.4 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" -pytest-asyncio==0.23.8 ; python_version >= "3.10" and python_version < "4.0" -pytest==8.4.1 ; python_version >= "3.10" and python_version < "4.0" -python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0" -python-dotenv==1.1.1 ; python_version >= "3.10" and python_version < "4.0" -python-multipart==0.0.20 ; python_version >= "3.10" and python_version < "4.0" -python-pptx==1.0.2 ; python_version >= "3.10" and python_version < "4.0" -pytz==2025.2 ; python_version >= "3.10" and python_version < "4.0" -pywin32==311 ; python_version >= "3.10" and python_version < "4.0" and (platform_system == "Windows" or sys_platform == "win32") -pyyaml==6.0.2 ; python_version >= "3.10" and python_version < "4.0" -referencing==0.36.2 ; python_version >= "3.10" and python_version < "4.0" -regex==2024.11.6 ; python_version >= "3.10" and python_version < "4.0" -requests==2.32.4 ; python_version >= "3.10" and python_version < "4.0" -rich-rst==1.3.1 ; python_version >= "3.10" and python_version < "4.0" -rich-toolkit==0.14.8 ; python_version >= "3.10" and python_version < "4.0" -rich==14.0.0 ; python_version >= "3.10" and python_version < "4.0" -rignore==0.6.2 ; python_version >= "3.10" and python_version < "4.0" -rpds-py==0.26.0 ; python_version >= "3.10" and python_version < "4.0" -ruff==0.11.13 ; python_version >= "3.10" and python_version < "4.0" -safetensors==0.5.3 ; python_version >= "3.10" and python_version < "4.0" -schedule==1.2.2 ; python_version >= "3.10" and python_version < "4.0" -scikit-learn==1.7.0 ; python_version >= "3.10" and python_version < "4.0" -scipy==1.15.3 ; python_version == "3.10" -scipy==1.16.0 ; python_version >= "3.11" and python_version < "4.0" -sentry-sdk==2.33.0 ; python_version >= "3.10" and python_version < "4.0" -shellingham==1.5.4 ; python_version >= "3.10" and python_version < "4.0" -six==1.17.0 ; python_version >= "3.10" and python_version < "4.0" -sniffio==1.3.1 ; python_version >= "3.10" and python_version < "4.0" -soupsieve==2.7 ; python_version >= "3.10" and python_version < "4.0" -sqlalchemy==2.0.41 ; python_version >= "3.10" and python_version < "4.0" -sse-starlette==2.4.1 ; python_version >= "3.10" and python_version < "4.0" -starlette==0.46.2 ; python_version >= "3.10" and python_version < "4.0" -sympy==1.14.0 ; python_version >= "3.10" and python_version < "4.0" -tenacity==9.1.2 ; python_version >= "3.10" and python_version < "4.0" -threadpoolctl==3.6.0 ; python_version >= "3.10" and python_version < "4.0" -tokenizers==0.21.2 ; python_version >= "3.10" and python_version < "4.0" -tomli==2.2.1 ; python_version == "3.10" -tqdm==4.67.1 ; python_version >= "3.10" and python_version < "4.0" -transformers==4.53.2 ; python_version >= "3.10" and python_version < "4.0" -typer==0.16.0 ; python_version >= "3.10" and python_version < "4.0" -typing-extensions==4.14.1 ; python_version >= "3.10" and python_version < "4.0" -typing-inspection==0.4.1 ; python_version >= "3.10" and python_version < "4.0" -tzdata==2025.2 ; python_version >= "3.10" and python_version < "4.0" -ujson==5.10.0 ; python_version >= "3.10" and python_version < "4.0" -urllib3==2.5.0 ; python_version >= "3.10" and python_version < "4.0" -uvicorn==0.35.0 ; python_version >= "3.10" and python_version < "4.0" -uvloop==0.21.0 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy" and sys_platform != "win32" and sys_platform != "cygwin" -virtualenv==20.31.2 ; python_version >= "3.10" and python_version < "4.0" -watchfiles==1.1.0 ; python_version >= "3.10" and python_version < "4.0" -websockets==15.0.1 ; python_version >= "3.10" and python_version < "4.0" -xlrd==2.0.2 ; python_version >= "3.10" and python_version < "4.0" -xlsxwriter==3.2.5 ; python_version >= "3.10" and python_version < "4.0" +# Docker optimized requirements - Core dependencies only +# Excludes Windows-specific and heavy GPU packages for faster builds + +annotated-types==0.7.0 +anyio==4.9.0 +async-timeout==5.0.1 +attrs==25.3.0 +authlib==1.6.0 +beautifulsoup4==4.13.4 +certifi==2025.7.14 +cffi==1.17.1 +charset-normalizer==3.4.2 +chonkie==1.1.1 +click==8.2.1 +cobble==0.1.4 +colorama==0.4.6 +coloredlogs==15.0.1 +cryptography==45.0.5 +cyclopts==3.22.2 +defusedxml==0.7.1 +distro==1.9.0 +dnspython==2.7.0 +docstring-parser==0.16 +docutils==0.21.2 +email-validator==2.2.0 +et-xmlfile==2.0.0 +exceptiongroup==1.3.0 +fastapi-cli==0.0.8 +fastapi-cloud-cli==0.1.4 +fastapi==0.115.14 +fastmcp==2.10.5 +filelock==3.18.0 +flatbuffers==25.2.10 +fsspec==2025.7.0 +greenlet==3.2.3 +grpcio==1.73.1 +h11==0.16.0 +h2==4.2.0 +hf-xet==1.1.5 +hpack==4.1.0 +httpcore==1.0.9 +httptools==0.6.4 +httpx-sse==0.4.1 +httpx==0.28.1 +huggingface-hub==0.33.4 +humanfriendly==10.0 +hyperframe==6.1.0 +idna==3.10 +itsdangerous==2.2.0 +jinja2==3.1.6 +jiter==0.10.0 +joblib==1.5.1 +jsonschema-specifications==2025.4.1 +jsonschema==4.24.1 +lxml==6.0.0 +magika==0.6.2 +mammoth==1.9.1 +markdown-it-py==3.0.0 +markdownify==1.1.0 +markitdown==0.1.2 +markupsafe==3.0.2 +mcp==1.12.0 +mdurl==0.1.2 +mpmath==1.3.0 +neo4j==5.28.1 +networkx==3.5 +numpy==2.3.1 +# NVIDIA CUDA packages excluded for lighter Docker images +# If GPU support is needed, uncomment relevant packages below: +# nvidia-cublas-cu12==12.6.4.1 +# nvidia-cuda-cupti-cu12==12.6.80 +# nvidia-cuda-nvrtc-cu12==12.6.77 +# nvidia-cuda-runtime-cu12==12.6.77 +# nvidia-cudnn-cu12==9.5.1.17 +# nvidia-cufft-cu12==11.3.0.4 +# nvidia-cufile-cu12==1.11.1.6 +# nvidia-curand-cu12==10.3.7.77 +# nvidia-cusolver-cu12==11.7.1.2 +# nvidia-cusparse-cu12==12.5.4.2 +# nvidia-cusparselt-cu12==0.6.3 +# nvidia-nccl-cu12==2.26.2 +# nvidia-nvjitlink-cu12==12.6.85 +# nvidia-nvtx-cu12==12.6.77 +ollama==0.4.9 +onnxruntime==1.22.1 +openai==1.97.0 +openapi-pydantic==0.5.1 +openpyxl==3.1.5 +orjson==3.11.0 +packaging==25.0 +pandas==2.3.1 +pdfminer-six==20250506 +pika==1.3.2 +pillow==11.3.0 +portalocker==2.10.1 +protobuf==6.31.1 +pycparser==2.22 +pydantic-core==2.33.2 +pydantic-extra-types==2.10.5 +pydantic-settings==2.10.1 +pydantic==2.11.7 +pygments==2.19.2 +pymysql==1.1.1 +pyperclip==1.9.0 +# Windows-specific packages excluded: +# pyreadline3==3.5.4 # Windows only +# pywin32==311 # Windows only +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-multipart==0.0.20 +python-pptx==1.0.2 +pytz==2025.2 +pyyaml==6.0.2 +qdrant-client==1.14.3 +redis==6.2.0 +referencing==0.36.2 +regex==2024.11.6 +requests==2.32.4 +rich-rst==1.3.1 +rich-toolkit==0.14.8 +rich==14.0.0 +rignore==0.6.2 +rpds-py==0.26.0 +safetensors==0.5.3 +schedule==1.2.2 +scikit-learn==1.7.0 +scipy==1.16.0 +sentence-transformers==4.1.0 +sentry-sdk==2.33.0 +setuptools==80.9.0 +shellingham==1.5.4 +six==1.17.0 +sniffio==1.3.1 +soupsieve==2.7 +sqlalchemy==2.0.41 +sse-starlette==2.4.1 +starlette==0.46.2 +sympy==1.14.0 +tenacity==9.1.2 +threadpoolctl==3.6.0 +tokenizers==0.21.2 +# Torch excluded for lighter Docker images (very large package ~2GB) +# If needed for ML/AI features, uncomment: +# torch==2.7.1 +# triton==3.3.1 +tqdm==4.67.1 +transformers==4.53.2 +typer==0.16.0 +typing-extensions==4.14.1 +typing-inspection==0.4.1 +tzdata==2025.2 +ujson==5.10.0 +urllib3==2.5.0 +uvicorn==0.35.0 +uvloop==0.21.0 +volcengine-python-sdk==4.0.6 +watchfiles==1.1.0 +websockets==15.0.1 +xlrd==2.0.2 +xlsxwriter==3.2.5 \ No newline at end of file diff --git a/src/memos/api/config.py b/src/memos/api/config.py index e7cc5d65f..f9272b1ca 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -21,7 +21,7 @@ class APIConfig: def get_openai_config() -> dict[str, Any]: """Get OpenAI configuration.""" return { - "model_name_or_path": os.getenv("MOS_CHAT_MODEL", "gpt-4o-mini"), + "model_name_or_path": os.getenv("MOS_OPENAI_MODEL", "gpt-4o-mini"), "temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.8")), "max_tokens": int(os.getenv("MOS_MAX_TOKENS", "1024")), "top_p": float(os.getenv("MOS_TOP_P", "0.9")), @@ -186,25 +186,25 @@ def get_neo4j_community_config(user_id: str | None = None) -> dict[str, Any]: return { "uri": os.getenv("NEO4J_URI", "bolt://localhost:7687"), "user": os.getenv("NEO4J_USER", "neo4j"), - "db_name": os.getenv("NEO4J_DB_NAME", "shared-tree-textual-memory"), + "db_name": os.getenv("NEO4J_DB_NAME", "neo4j"), "password": os.getenv("NEO4J_PASSWORD", "12345678"), "user_name": f"memos{user_id.replace('-', '')}", - "auto_create": True, + "auto_create": False, "use_multi_db": False, - "embedding_dimension": int(os.getenv("EMBEDDING_DIMENSION", 3072)), + "embedding_dimension": int(os.getenv("EMBEDDING_DIMENSION", 1024)), "vec_config": { # Pass nested config to initialize external vector DB # If you use qdrant, please use Server instead of local mode. "backend": "qdrant", "config": { "collection_name": "neo4j_vec_db", - "vector_dimension": int(os.getenv("EMBEDDING_DIMENSION", 3072)), + "vector_dimension": int(os.getenv("EMBEDDING_DIMENSION", 1024)), "distance_metric": "cosine", - "host": "localhost", - "port": 6333, + "host": os.getenv("QDRANT_HOST", "localhost"), + "port": int(os.getenv("QDRANT_PORT", "6333")), }, }, - } + } @staticmethod def get_neo4j_config(user_id: str | None = None) -> dict[str, Any]: diff --git a/src/memos/api/routers/start_router.py b/src/memos/api/routers/start_router.py new file mode 100644 index 000000000..5d136faf9 --- /dev/null +++ b/src/memos/api/routers/start_router.py @@ -0,0 +1,305 @@ +""" +Basic MemOS API router for core functionality. +This router provides basic CRUD operations for memories, users, and cubes. +""" + +import traceback + +from typing import Any + +from fastapi import APIRouter, HTTPException +from fastapi.responses import RedirectResponse + +from memos.api.start_config import get_mos_instance +from memos.api.start_models import ( + ChatRequest, + ChatResponse, + ConfigRequest, + ConfigResponse, + CubeShare, + MemCubeRegister, + MemoryCreate, + MemoryResponse, + SearchRequest, + SearchResponse, + SimpleResponse, + UserCreate, + UserListResponse, + UserResponse, +) +from memos.log import get_logger +from memos.mem_user.user_manager import UserRole + + +logger = get_logger(__name__) + +router = APIRouter(tags=["Basic MemOS API"]) + + +@router.post("/configure", summary="Configure MemOS", response_model=ConfigResponse) +async def set_config(config_request: ConfigRequest): + """Set MemOS configuration with basic settings.""" + try: + # Get current MOS instance + mos = get_mos_instance() + + # Update configuration based on request + if config_request.user_id is not None: + mos.user_id = config_request.user_id + if config_request.session_id is not None: + mos.session_id = config_request.session_id + if config_request.top_k is not None: + mos.config.top_k = config_request.top_k + if config_request.enable_textual_memory is not None: + mos.config.enable_textual_memory = config_request.enable_textual_memory + if config_request.enable_activation_memory is not None: + mos.config.enable_activation_memory = config_request.enable_activation_memory + + return ConfigResponse(message="Configuration updated successfully") + except Exception as err: + logger.error(f"Failed to set configuration: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.post("/users", summary="Create a new user", response_model=UserResponse) +async def create_user(user_create: UserCreate): + """Create a new user.""" + try: + mos_instance = get_mos_instance() + role = UserRole(user_create.role) + user_id = mos_instance.create_user( + user_id=user_create.user_id, role=role, user_name=user_create.user_name + ) + return UserResponse(message="User created successfully", data={"user_id": user_id}) + except Exception as err: + logger.error(f"Failed to create user: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.get("/users", summary="List all users", response_model=UserListResponse) +async def list_users(): + """List all active users.""" + try: + mos_instance = get_mos_instance() + users = mos_instance.list_users() + return UserListResponse(message="Users retrieved successfully", data=users) + except Exception as err: + logger.error(f"Failed to list users: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.get("/users/me", summary="Get current user info", response_model=UserResponse) +async def get_user_info(): + """Get current user information including accessible cubes.""" + try: + mos_instance = get_mos_instance() + user_info = mos_instance.get_user_info() + return UserResponse(message="User info retrieved successfully", data=user_info) + except Exception as err: + logger.error(f"Failed to get user info: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.post("/mem_cubes", summary="Register a MemCube", response_model=SimpleResponse) +async def register_mem_cube(mem_cube: MemCubeRegister): + """Register a new MemCube.""" + try: + mos_instance = get_mos_instance() + mos_instance.register_mem_cube( + mem_cube_name_or_path=mem_cube.mem_cube_name_or_path, + mem_cube_id=mem_cube.mem_cube_id, + user_id=mem_cube.user_id, + ) + return SimpleResponse(message="MemCube registered successfully") + except Exception as err: + logger.error(f"Failed to register MemCube: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.delete( + "/mem_cubes/{mem_cube_id}", summary="Unregister a MemCube", response_model=SimpleResponse +) +async def unregister_mem_cube(mem_cube_id: str, user_id: str | None = None): + """Unregister a MemCube.""" + try: + mos_instance = get_mos_instance() + mos_instance.unregister_mem_cube(mem_cube_id=mem_cube_id, user_id=user_id) + return SimpleResponse(message="MemCube unregistered successfully") + except Exception as err: + logger.error(f"Failed to unregister MemCube: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.post( + "/mem_cubes/{cube_id}/share", + summary="Share a cube with another user", + response_model=SimpleResponse, +) +async def share_cube(cube_id: str, share_request: CubeShare): + """Share a cube with another user.""" + try: + mos_instance = get_mos_instance() + success = mos_instance.share_cube_with_user(cube_id, share_request.target_user_id) + if success: + return SimpleResponse(message="Cube shared successfully") + else: + raise HTTPException(status_code=500, detail="Failed to share cube") + except Exception as err: + logger.error(f"Failed to share cube: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.post("/memories", summary="Create memories", response_model=SimpleResponse) +async def add_memory(memory_create: MemoryCreate): + """Store new memories in a MemCube.""" + try: + if not any([memory_create.messages, memory_create.memory_content, memory_create.doc_path]): + raise HTTPException( + status_code=400, + detail="Either messages, memory_content, or doc_path must be provided", + ) + + mos_instance = get_mos_instance() + + if memory_create.messages: + messages = [m.model_dump() for m in memory_create.messages] + mos_instance.add( + messages=messages, + mem_cube_id=memory_create.mem_cube_id, + user_id=memory_create.user_id, + ) + elif memory_create.memory_content: + mos_instance.add( + memory_content=memory_create.memory_content, + mem_cube_id=memory_create.mem_cube_id, + user_id=memory_create.user_id, + ) + elif memory_create.doc_path: + mos_instance.add( + doc_path=memory_create.doc_path, + mem_cube_id=memory_create.mem_cube_id, + user_id=memory_create.user_id, + ) + + return SimpleResponse(message="Memories added successfully") + except HTTPException: + raise + except Exception as err: + logger.error(f"Failed to add memory: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.get("/memories", summary="Get all memories", response_model=MemoryResponse) +async def get_all_memories( + mem_cube_id: str | None = None, + user_id: str | None = None, +): + """Retrieve all memories from a MemCube.""" + try: + mos_instance = get_mos_instance() + result = mos_instance.get_all(mem_cube_id=mem_cube_id, user_id=user_id) + return MemoryResponse(message="Memories retrieved successfully", data=result) + except Exception as err: + logger.error(f"Failed to get memories: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.get( + "/memories/{mem_cube_id}/{memory_id}", summary="Get a memory", response_model=MemoryResponse +) +async def get_memory(mem_cube_id: str, memory_id: str, user_id: str | None = None): + """Retrieve a specific memory by ID from a MemCube.""" + try: + mos_instance = get_mos_instance() + result = mos_instance.get(mem_cube_id=mem_cube_id, memory_id=memory_id, user_id=user_id) + return MemoryResponse(message="Memory retrieved successfully", data=result) + except Exception as err: + logger.error(f"Failed to get memory: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.post("/search", summary="Search memories", response_model=SearchResponse) +async def search_memories(search_req: SearchRequest): + """Search for memories across MemCubes.""" + try: + mos_instance = get_mos_instance() + result = mos_instance.search( + query=search_req.query, + user_id=search_req.user_id, + install_cube_ids=search_req.install_cube_ids, + ) + return SearchResponse(message="Search completed successfully", data=result) + except Exception as err: + logger.error(f"Failed to search memories: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.put( + "/memories/{mem_cube_id}/{memory_id}", summary="Update a memory", response_model=SimpleResponse +) +async def update_memory( + mem_cube_id: str, memory_id: str, updated_memory: dict[str, Any], user_id: str | None = None +): + """Update an existing memory in a MemCube.""" + try: + mos_instance = get_mos_instance() + mos_instance.update( + mem_cube_id=mem_cube_id, + memory_id=memory_id, + text_memory_item=updated_memory, + user_id=user_id, + ) + return SimpleResponse(message="Memory updated successfully") + except Exception as err: + logger.error(f"Failed to update memory: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.delete( + "/memories/{mem_cube_id}/{memory_id}", summary="Delete a memory", response_model=SimpleResponse +) +async def delete_memory(mem_cube_id: str, memory_id: str, user_id: str | None = None): + """Delete a specific memory from a MemCube.""" + try: + mos_instance = get_mos_instance() + mos_instance.delete(mem_cube_id=mem_cube_id, memory_id=memory_id, user_id=user_id) + return SimpleResponse(message="Memory deleted successfully") + except Exception as err: + logger.error(f"Failed to delete memory: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.delete( + "/memories/{mem_cube_id}", summary="Delete all memories", response_model=SimpleResponse +) +async def delete_all_memories(mem_cube_id: str, user_id: str | None = None): + """Delete all memories from a MemCube.""" + try: + mos_instance = get_mos_instance() + mos_instance.delete_all(mem_cube_id=mem_cube_id, user_id=user_id) + return SimpleResponse(message="All memories deleted successfully") + except Exception as err: + logger.error(f"Failed to delete all memories: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.post("/chat", summary="Chat with MemOS", response_model=ChatResponse) +async def chat(chat_req: ChatRequest): + """Chat with the MemOS system.""" + try: + mos_instance = get_mos_instance() + response = mos_instance.chat(query=chat_req.query, user_id=chat_req.user_id) + if response is None: + raise HTTPException(status_code=500, detail="No response generated") + return ChatResponse(message="Chat response generated", data=response) + except HTTPException: + raise + except Exception as err: + logger.error(f"Failed to chat: {traceback.format_exc()}") + raise HTTPException(status_code=500, detail=str(err)) from err + + +@router.get("/", summary="Redirect to the OpenAPI documentation", include_in_schema=False) +async def home(): + """Redirect to the OpenAPI documentation.""" + return RedirectResponse(url="/docs", status_code=307) diff --git a/src/memos/api/start_api.py b/src/memos/api/start_api.py index 9f464a4ad..d736971a2 100644 --- a/src/memos/api/start_api.py +++ b/src/memos/api/start_api.py @@ -1,401 +1,36 @@ import logging -import os - -from typing import Any, Generic, TypeVar from dotenv import load_dotenv from fastapi import FastAPI -from fastapi.requests import Request -from fastapi.responses import JSONResponse, RedirectResponse -from pydantic import BaseModel, Field +from fastapi.responses import RedirectResponse +from memos.api.exceptions import APIExceptionHandler from memos.api.middleware.request_context import RequestContextMiddleware -from memos.configs.mem_os import MOSConfig -from memos.mem_os.main import MOS -from memos.mem_user.user_manager import UserManager, UserRole - +from memos.api.routers.start_router import router as start_router -# Configure logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") -logger = logging.getLogger(__name__) # Load environment variables load_dotenv() -T = TypeVar("T") - -# Default configuration -DEFAULT_CONFIG = { - "user_id": os.getenv("MOS_USER_ID", "default_user"), - "session_id": os.getenv("MOS_SESSION_ID", "default_session"), - "enable_textual_memory": True, - "enable_activation_memory": False, - "top_k": int(os.getenv("MOS_TOP_K", "5")), - "chat_model": { - "backend": os.getenv("MOS_CHAT_MODEL_PROVIDER", "openai"), - "config": { - "model_name_or_path": os.getenv("MOS_CHAT_MODEL", "gpt-3.5-turbo"), - "api_key": os.getenv("OPENAI_API_KEY", "apikey"), - "temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.7")), - "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), - }, - }, -} - -# Initialize MOS instance with lazy initialization -MOS_INSTANCE = None - - -def get_mos_instance(): - """Get or create MOS instance with default user creation.""" - global MOS_INSTANCE - if MOS_INSTANCE is None: - # Create a temporary MOS instance to access user manager - temp_config = MOSConfig(**DEFAULT_CONFIG) - temp_mos = MOS.__new__(MOS) - temp_mos.config = temp_config - temp_mos.user_id = temp_config.user_id - temp_mos.session_id = temp_config.session_id - temp_mos.mem_cubes = {} - temp_mos.chat_llm = None # Will be initialized later - temp_mos.user_manager = UserManager() - - # Create default user if it doesn't exist - if not temp_mos.user_manager.validate_user(temp_config.user_id): - temp_mos.user_manager.create_user( - user_name=temp_config.user_id, role=UserRole.USER, user_id=temp_config.user_id - ) - logger.info(f"Created default user: {temp_config.user_id}") - - # Now create the actual MOS instance - MOS_INSTANCE = MOS(config=temp_config) - - return MOS_INSTANCE - +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) +# Create FastAPI application app = FastAPI( - title="MemOS REST APIs", - description="A REST API for managing and searching memories using MemOS.", + title="MemOS Basic REST APIs", + description="A REST API for managing and searching memories using MemOS core functionality.", version="1.0.0", ) app.add_middleware(RequestContextMiddleware) +# Include routers +app.include_router(start_router) -class BaseRequest(BaseModel): - """Base model for all requests.""" - - user_id: str | None = Field( - None, description="User ID for the request", json_schema_extra={"example": "user123"} - ) - - -class BaseResponse(BaseModel, Generic[T]): - """Base model for all responses.""" - - code: int = Field(200, description="Response status code", json_schema_extra={"example": 200}) - message: str = Field( - ..., description="Response message", json_schema_extra={"example": "Operation successful"} - ) - data: T | None = Field(None, description="Response data") - - -class Message(BaseModel): - role: str = Field( - ..., - description="Role of the message (user or assistant).", - json_schema_extra={"example": "user"}, - ) - content: str = Field( - ..., - description="Message content.", - json_schema_extra={"example": "Hello, how can I help you?"}, - ) - - -class MemoryCreate(BaseRequest): - messages: list[Message] | None = Field( - None, - description="List of messages to store.", - json_schema_extra={"example": [{"role": "user", "content": "Hello"}]}, - ) - mem_cube_id: str | None = Field( - None, description="ID of the memory cube", json_schema_extra={"example": "cube123"} - ) - memory_content: str | None = Field( - None, - description="Content to store as memory", - json_schema_extra={"example": "This is a memory content"}, - ) - doc_path: str | None = Field( - None, - description="Path to document to store", - json_schema_extra={"example": "/path/to/document.txt"}, - ) - - -class SearchRequest(BaseRequest): - query: str = Field( - ..., - description="Search query.", - json_schema_extra={"example": "How to implement a feature?"}, - ) - install_cube_ids: list[str] | None = Field( - None, - description="List of cube IDs to search in", - json_schema_extra={"example": ["cube123", "cube456"]}, - ) - - -class MemCubeRegister(BaseRequest): - mem_cube_name_or_path: str = Field( - ..., - description="Name or path of the MemCube to register.", - json_schema_extra={"example": "/path/to/cube"}, - ) - mem_cube_id: str | None = Field( - None, description="ID for the MemCube", json_schema_extra={"example": "cube123"} - ) - - -class ChatRequest(BaseRequest): - query: str = Field( - ..., - description="Chat query message.", - json_schema_extra={"example": "What is the latest update?"}, - ) - - -class UserCreate(BaseRequest): - user_name: str | None = Field( - None, description="Name of the user", json_schema_extra={"example": "john_doe"} - ) - role: str = Field("user", description="Role of the user", json_schema_extra={"example": "user"}) - user_id: str = Field(..., description="User ID", json_schema_extra={"example": "user123"}) - - -class CubeShare(BaseRequest): - target_user_id: str = Field( - ..., description="Target user ID to share with", json_schema_extra={"example": "user456"} - ) - - -class SimpleResponse(BaseResponse[None]): - """Simple response model for operations without data return.""" - - -class ConfigResponse(BaseResponse[None]): - """Response model for configuration endpoint.""" - - -class MemoryResponse(BaseResponse[dict]): - """Response model for memory operations.""" - - -class SearchResponse(BaseResponse[dict]): - """Response model for search operations.""" - - -class ChatResponse(BaseResponse[str]): - """Response model for chat operations.""" - - -class UserResponse(BaseResponse[dict]): - """Response model for user operations.""" - - -class UserListResponse(BaseResponse[list]): - """Response model for user list operations.""" - - -@app.post("/configure", summary="Configure MemOS", response_model=ConfigResponse) -async def set_config(config: MOSConfig): - """Set MemOS configuration.""" - global MOS_INSTANCE - - # Create a temporary user manager to check/create default user - temp_user_manager = UserManager() - - # Create default user if it doesn't exist - if not temp_user_manager.validate_user(config.user_id): - temp_user_manager.create_user( - user_name=config.user_id, role=UserRole.USER, user_id=config.user_id - ) - logger.info(f"Created default user: {config.user_id}") - - # Now create the MOS instance - MOS_INSTANCE = MOS(config=config) - return ConfigResponse(message="Configuration set successfully") - - -@app.post("/users", summary="Create a new user", response_model=UserResponse) -async def create_user(user_create: UserCreate): - """Create a new user.""" - mos_instance = get_mos_instance() - role = UserRole(user_create.role) - user_id = mos_instance.create_user( - user_id=user_create.user_id, role=role, user_name=user_create.user_name - ) - return UserResponse(message="User created successfully", data={"user_id": user_id}) - - -@app.get("/users", summary="List all users", response_model=UserListResponse) -async def list_users(): - """List all active users.""" - mos_instance = get_mos_instance() - users = mos_instance.list_users() - return UserListResponse(message="Users retrieved successfully", data=users) - - -@app.get("/users/me", summary="Get current user info", response_model=UserResponse) -async def get_user_info(): - """Get current user information including accessible cubes.""" - mos_instance = get_mos_instance() - user_info = mos_instance.get_user_info() - return UserResponse(message="User info retrieved successfully", data=user_info) - - -@app.post("/mem_cubes", summary="Register a MemCube", response_model=SimpleResponse) -async def register_mem_cube(mem_cube: MemCubeRegister): - """Register a new MemCube.""" - mos_instance = get_mos_instance() - mos_instance.register_mem_cube( - mem_cube_name_or_path=mem_cube.mem_cube_name_or_path, - mem_cube_id=mem_cube.mem_cube_id, - user_id=mem_cube.user_id, - ) - return SimpleResponse(message="MemCube registered successfully") - - -@app.delete( - "/mem_cubes/{mem_cube_id}", summary="Unregister a MemCube", response_model=SimpleResponse -) -async def unregister_mem_cube(mem_cube_id: str, user_id: str | None = None): - """Unregister a MemCube.""" - mos_instance = get_mos_instance() - mos_instance.unregister_mem_cube(mem_cube_id=mem_cube_id, user_id=user_id) - return SimpleResponse(message="MemCube unregistered successfully") - - -@app.post( - "/mem_cubes/{cube_id}/share", - summary="Share a cube with another user", - response_model=SimpleResponse, -) -async def share_cube(cube_id: str, share_request: CubeShare): - """Share a cube with another user.""" - mos_instance = get_mos_instance() - success = mos_instance.share_cube_with_user(cube_id, share_request.target_user_id) - if success: - return SimpleResponse(message="Cube shared successfully") - else: - raise ValueError("Failed to share cube") - - -@app.post("/memories", summary="Create memories", response_model=SimpleResponse) -async def add_memory(memory_create: MemoryCreate): - """Store new memories in a MemCube.""" - if not any([memory_create.messages, memory_create.memory_content, memory_create.doc_path]): - raise ValueError("Either messages, memory_content, or doc_path must be provided") - mos_instance = get_mos_instance() - if memory_create.messages: - messages = [m.model_dump() for m in memory_create.messages] - mos_instance.add( - messages=messages, - mem_cube_id=memory_create.mem_cube_id, - user_id=memory_create.user_id, - ) - elif memory_create.memory_content: - mos_instance.add( - memory_content=memory_create.memory_content, - mem_cube_id=memory_create.mem_cube_id, - user_id=memory_create.user_id, - ) - elif memory_create.doc_path: - mos_instance.add( - doc_path=memory_create.doc_path, - mem_cube_id=memory_create.mem_cube_id, - user_id=memory_create.user_id, - ) - return SimpleResponse(message="Memories added successfully") - - -@app.get("/memories", summary="Get all memories", response_model=MemoryResponse) -async def get_all_memories( - mem_cube_id: str | None = None, - user_id: str | None = None, -): - """Retrieve all memories from a MemCube.""" - mos_instance = get_mos_instance() - result = mos_instance.get_all(mem_cube_id=mem_cube_id, user_id=user_id) - return MemoryResponse(message="Memories retrieved successfully", data=result) - - -@app.get( - "/memories/{mem_cube_id}/{memory_id}", summary="Get a memory", response_model=MemoryResponse -) -async def get_memory(mem_cube_id: str, memory_id: str, user_id: str | None = None): - """Retrieve a specific memory by ID from a MemCube.""" - mos_instance = get_mos_instance() - result = mos_instance.get(mem_cube_id=mem_cube_id, memory_id=memory_id, user_id=user_id) - return MemoryResponse(message="Memory retrieved successfully", data=result) - - -@app.post("/search", summary="Search memories", response_model=SearchResponse) -async def search_memories(search_req: SearchRequest): - """Search for memories across MemCubes.""" - mos_instance = get_mos_instance() - result = mos_instance.search( - query=search_req.query, - user_id=search_req.user_id, - install_cube_ids=search_req.install_cube_ids, - ) - return SearchResponse(message="Search completed successfully", data=result) - - -@app.put( - "/memories/{mem_cube_id}/{memory_id}", summary="Update a memory", response_model=SimpleResponse -) -async def update_memory( - mem_cube_id: str, memory_id: str, updated_memory: dict[str, Any], user_id: str | None = None -): - """Update an existing memory in a MemCube.""" - mos_instance = get_mos_instance() - mos_instance.update( - mem_cube_id=mem_cube_id, - memory_id=memory_id, - text_memory_item=updated_memory, - user_id=user_id, - ) - return SimpleResponse(message="Memory updated successfully") - - -@app.delete( - "/memories/{mem_cube_id}/{memory_id}", summary="Delete a memory", response_model=SimpleResponse -) -async def delete_memory(mem_cube_id: str, memory_id: str, user_id: str | None = None): - """Delete a specific memory from a MemCube.""" - mos_instance = get_mos_instance() - mos_instance.delete(mem_cube_id=mem_cube_id, memory_id=memory_id, user_id=user_id) - return SimpleResponse(message="Memory deleted successfully") - - -@app.delete("/memories/{mem_cube_id}", summary="Delete all memories", response_model=SimpleResponse) -async def delete_all_memories(mem_cube_id: str, user_id: str | None = None): - """Delete all memories from a MemCube.""" - mos_instance = get_mos_instance() - mos_instance.delete_all(mem_cube_id=mem_cube_id, user_id=user_id) - return SimpleResponse(message="All memories deleted successfully") - - -@app.post("/chat", summary="Chat with MemOS", response_model=ChatResponse) -async def chat(chat_req: ChatRequest): - """Chat with the MemOS system.""" - mos_instance = get_mos_instance() - response = mos_instance.chat(query=chat_req.query, user_id=chat_req.user_id) - if response is None: - raise ValueError("No response generated") - return ChatResponse(message="Chat response generated", data=response) +# Exception handlers +app.exception_handler(ValueError)(APIExceptionHandler.value_error_handler) +app.exception_handler(Exception)(APIExceptionHandler.global_exception_handler) @app.get("/", summary="Redirect to the OpenAPI documentation", include_in_schema=False) @@ -404,20 +39,16 @@ async def home(): return RedirectResponse(url="/docs", status_code=307) -@app.exception_handler(ValueError) -async def value_error_handler(request: Request, exc: ValueError): - """Handle ValueError exceptions globally.""" - return JSONResponse( - status_code=400, - content={"code": 400, "message": str(exc), "data": None}, - ) +if __name__ == "__main__": + import argparse + + import uvicorn + parser = argparse.ArgumentParser() + parser.add_argument("--port", type=int, default=8000, help="Port to run the server on") + parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to run the server on") + parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development") + args = parser.parse_args() -@app.exception_handler(Exception) -async def global_exception_handler(request: Request, exc: Exception): - """Handle all unhandled exceptions globally.""" - logger.exception("Unhandled error:") - return JSONResponse( - status_code=500, - content={"code": 500, "message": str(exc), "data": None}, - ) + logger.info(f"Starting MemOS Basic API server on {args.host}:{args.port}") + uvicorn.run("memos.api.start_api:app", host=args.host, port=args.port, reload=args.reload) diff --git a/src/memos/api/start_config.py b/src/memos/api/start_config.py new file mode 100644 index 000000000..bb069240d --- /dev/null +++ b/src/memos/api/start_config.py @@ -0,0 +1,144 @@ +""" +Configuration management for the basic MemOS API. +""" + +import os + +from typing import Any + +from memos.configs.mem_os import MOSConfig +from memos.log import get_logger +from memos.mem_os.main import MOS +from memos.mem_user.user_manager import UserManager, UserRole + + +logger = get_logger(__name__) + +# Global MOS instance with lazy initialization +_MOS_INSTANCE = None + +# Default configuration from environment variables +DEFAULT_CONFIG = { + "user_id": os.getenv("MOS_USER_ID", "default_user"), + "session_id": os.getenv("MOS_SESSION_ID", "default_session"), + "enable_textual_memory": True, + "enable_activation_memory": False, + "top_k": int(os.getenv("MOS_TOP_K", "5")), + "chat_model": { + "backend": os.getenv("MOS_CHAT_MODEL_PROVIDER", "openai"), + "config": { + "model_name_or_path": os.getenv("MOS_CHAT_MODEL", "gpt-3.5-turbo"), + "api_key": os.getenv("OPENAI_API_KEY", "apikey"), + "temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.7")), + "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), + }, + }, + "mem_reader": { + "backend": "simple_struct", + "config": { + "llm": { + "backend": os.getenv("MOS_MEM_READER_LLM_PROVIDER", "openai"), + "config": { + "model_name_or_path": os.getenv("MOS_MEM_READER_MODEL", "gpt-3.5-turbo"), + "api_key": os.getenv("OPENAI_API_KEY", "apikey"), + "temperature": float(os.getenv("MOS_MEM_READER_TEMPERATURE", "0.7")), + "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), + }, + }, + "embedder": { + "backend": "universal_api", + "config": { + "provider": os.getenv("MOS_EMBEDDER_PROVIDER", "openai"), + "model_name_or_path": os.getenv("MOS_EMBEDDER_MODEL", "text-embedding-ada-002"), + "api_key": os.getenv("OPENAI_API_KEY", "apikey"), + "base_url": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), + }, + }, + "chunker": { + "backend": "sentence", + "config": { + "tokenizer_or_token_counter": "gpt2", + "chunk_size": int(os.getenv("MOS_CHUNK_SIZE", "512")), + "chunk_overlap": int(os.getenv("MOS_CHUNK_OVERLAP", "128")), + "min_sentences_per_chunk": 1, + }, + }, + }, + }, +} + + +def get_default_config() -> dict[str, Any]: + """Get default configuration from environment variables.""" + return DEFAULT_CONFIG.copy() + + +def create_user_if_not_exists(user_id: str, user_manager: UserManager) -> None: + """Create user if it doesn't exist.""" + if not user_manager.validate_user(user_id): + user_manager.create_user(user_name=user_id, role=UserRole.USER, user_id=user_id) + logger.info(f"Created default user: {user_id}") + + +def get_mos_instance() -> MOS: + """ + Get or create MOS instance with default user creation. + + Returns: + MOS: The MOS instance + """ + global _MOS_INSTANCE + + if _MOS_INSTANCE is None: + # Create configuration + temp_config = MOSConfig(**DEFAULT_CONFIG) + + # Create a temporary MOS instance to access user manager + # This is a workaround for the chicken-and-egg problem: + # MOS needs a valid user, but we need MOS to create users + temp_mos = MOS.__new__(MOS) + temp_mos.config = temp_config + temp_mos.user_id = temp_config.user_id + temp_mos.session_id = temp_config.session_id + temp_mos.mem_cubes = {} + temp_mos.chat_llm = None + temp_mos.user_manager = UserManager() + + # Create default user if it doesn't exist + create_user_if_not_exists(temp_config.user_id, temp_mos.user_manager) + + # Now create the actual MOS instance + _MOS_INSTANCE = MOS(config=temp_config) + logger.info(f"MOS instance created successfully for user: {temp_config.user_id}") + + return _MOS_INSTANCE + + +def set_mos_instance(config: MOSConfig) -> MOS: + """ + Set a new MOS instance with the provided configuration. + + Args: + config: The MOSConfig to use + + Returns: + MOS: The new MOS instance + """ + global _MOS_INSTANCE + + # Create a temporary user manager to check/create default user + temp_user_manager = UserManager() + create_user_if_not_exists(config.user_id, temp_user_manager) + + # Create the MOS instance + _MOS_INSTANCE = MOS(config=config) + logger.info(f"MOS instance updated with new configuration for user: {config.user_id}") + + return _MOS_INSTANCE + + +def reset_mos_instance() -> None: + """Reset the MOS instance (useful for testing).""" + global _MOS_INSTANCE + _MOS_INSTANCE = None + logger.info("MOS instance reset") diff --git a/src/memos/api/start_models.py b/src/memos/api/start_models.py new file mode 100644 index 000000000..ee447053f --- /dev/null +++ b/src/memos/api/start_models.py @@ -0,0 +1,147 @@ +""" +Data models for the basic MemOS API endpoints. +""" + +from typing import Generic, TypeVar + +from pydantic import BaseModel, Field + + +T = TypeVar("T") + + +class BaseRequest(BaseModel): + """Base model for all requests.""" + + user_id: str | None = Field( + None, description="User ID for the request", json_schema_extra={"example": "user123"} + ) + + +class BaseResponse(BaseModel, Generic[T]): + """Base model for all responses.""" + + code: int = Field(200, description="Response status code", json_schema_extra={"example": 200}) + message: str = Field( + ..., description="Response message", json_schema_extra={"example": "Operation successful"} + ) + data: T | None = Field(None, description="Response data") + + +class Message(BaseModel): + role: str = Field( + ..., + description="Role of the message (user or assistant).", + json_schema_extra={"example": "user"}, + ) + content: str = Field( + ..., + description="Message content.", + json_schema_extra={"example": "Hello, how can I help you?"}, + ) + + +class MemoryCreate(BaseRequest): + messages: list[Message] | None = Field( + None, + description="List of messages to store.", + json_schema_extra={"example": [{"role": "user", "content": "Hello"}]}, + ) + mem_cube_id: str | None = Field( + None, description="ID of the memory cube", json_schema_extra={"example": "cube123"} + ) + memory_content: str | None = Field( + None, + description="Content to store as memory", + json_schema_extra={"example": "This is a memory content"}, + ) + doc_path: str | None = Field( + None, + description="Path to document to store", + json_schema_extra={"example": "/path/to/document.txt"}, + ) + + +class SearchRequest(BaseRequest): + query: str = Field( + ..., + description="Search query.", + json_schema_extra={"example": "How to implement a feature?"}, + ) + install_cube_ids: list[str] | None = Field( + None, + description="List of cube IDs to search in", + json_schema_extra={"example": ["cube123", "cube456"]}, + ) + + +class MemCubeRegister(BaseRequest): + mem_cube_name_or_path: str = Field( + ..., + description="Name or path of the MemCube to register.", + json_schema_extra={"example": "/path/to/cube"}, + ) + mem_cube_id: str | None = Field( + None, description="ID for the MemCube", json_schema_extra={"example": "cube123"} + ) + + +class ChatRequest(BaseRequest): + query: str = Field( + ..., + description="Chat query message.", + json_schema_extra={"example": "What is the latest update?"}, + ) + + +class UserCreate(BaseRequest): + user_name: str | None = Field( + None, description="Name of the user", json_schema_extra={"example": "john_doe"} + ) + role: str = Field("USER", description="Role of the user", json_schema_extra={"example": "USER"}) + user_id: str = Field(..., description="User ID", json_schema_extra={"example": "user123"}) + + +class CubeShare(BaseRequest): + target_user_id: str = Field( + ..., description="Target user ID to share with", json_schema_extra={"example": "user456"} + ) + + +# Response models +class SimpleResponse(BaseResponse[None]): + """Simple response model for operations without data return.""" + + +class ConfigRequest(BaseModel): + """Configuration request model for basic settings.""" + + user_id: str | None = Field(None, description="User ID to configure") + session_id: str | None = Field(None, description="Session ID to configure") + top_k: int | None = Field(None, description="Top K memories to retrieve") + enable_textual_memory: bool | None = Field(None, description="Enable textual memory") + enable_activation_memory: bool | None = Field(None, description="Enable activation memory") + + +class ConfigResponse(BaseResponse[None]): + """Response model for configuration endpoint.""" + + +class MemoryResponse(BaseResponse[dict]): + """Response model for memory operations.""" + + +class SearchResponse(BaseResponse[dict]): + """Response model for search operations.""" + + +class ChatResponse(BaseResponse[str]): + """Response model for chat operations.""" + + +class UserResponse(BaseResponse[dict]): + """Response model for user operations.""" + + +class UserListResponse(BaseResponse[list]): + """Response model for user list operations.""" From f4bbfb6b6b7792f09ab6a78bdb05080188d8dac9 Mon Sep 17 00:00:00 2001 From: yjy Date: Mon, 22 Sep 2025 17:12:53 +0800 Subject: [PATCH 02/21] docker start --- docker/docker-compose.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 40bf5e6c5..0f680505f 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -16,6 +16,9 @@ services: environment: - PYTHONPATH=/app/src - HF_ENDPOINT=https://hf-mirror.com + - QDRANT_HOST=qdrant-docker + - QDRANT_PORT=6333 + - NEO4J_URI=bolt://neo4j-docker:7687 volumes: - ../src:/app/src - .:/app/docker @@ -29,7 +32,7 @@ services: - "7474:7474" # HTTP - "7687:7687" # Bolt healthcheck: - test: wget http://localhost:7687 || exit 1 + test: wget http://localhost:7474 || exit 1 interval: 1s timeout: 10s retries: 20 From f0f079072c8c8492ad16f9d300d67b0820a85e6a Mon Sep 17 00:00:00 2001 From: yjy Date: Mon, 22 Sep 2025 17:18:43 +0800 Subject: [PATCH 03/21] update config --- src/memos/api/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/memos/api/config.py b/src/memos/api/config.py index f9272b1ca..a092c29f8 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -21,7 +21,7 @@ class APIConfig: def get_openai_config() -> dict[str, Any]: """Get OpenAI configuration.""" return { - "model_name_or_path": os.getenv("MOS_OPENAI_MODEL", "gpt-4o-mini"), + "model_name_or_path": os.getenv("MOS_CHAT_MODEL", "gpt-4o-mini"), "temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.8")), "max_tokens": int(os.getenv("MOS_MAX_TOKENS", "1024")), "top_p": float(os.getenv("MOS_TOP_P", "0.9")), From 45c57034db783a76210ca777335fd0072ab80a1d Mon Sep 17 00:00:00 2001 From: yjy Date: Mon, 22 Sep 2025 19:23:25 +0800 Subject: [PATCH 04/21] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/memos/api/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/memos/api/config.py b/src/memos/api/config.py index a092c29f8..c56061c60 100644 --- a/src/memos/api/config.py +++ b/src/memos/api/config.py @@ -204,7 +204,7 @@ def get_neo4j_community_config(user_id: str | None = None) -> dict[str, Any]: "port": int(os.getenv("QDRANT_PORT", "6333")), }, }, - } + } @staticmethod def get_neo4j_config(user_id: str | None = None) -> dict[str, Any]: From 4a13c1e2e12a683e8cbc8664fccdfc9f7f1e5149 Mon Sep 17 00:00:00 2001 From: yjy Date: Mon, 22 Sep 2025 20:45:40 +0800 Subject: [PATCH 05/21] test_start_api --- tests/api/test_start_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_start_api.py b/tests/api/test_start_api.py index c4f6eff64..8184a2321 100644 --- a/tests/api/test_start_api.py +++ b/tests/api/test_start_api.py @@ -57,7 +57,7 @@ @pytest.fixture def mock_mos(): """Mock MOS instance for testing.""" - with patch("memos.api.start_api.get_mos_instance") as mock_get_mos: + with patch("memos.api.start_config.get_mos_instance") as mock_get_mos: # Create a mock MOS instance mock_instance = Mock() From c337ef3124d2283297d5751a3870dbec9bb53c0e Mon Sep 17 00:00:00 2001 From: yjy Date: Mon, 22 Sep 2025 20:46:43 +0800 Subject: [PATCH 06/21] test_start_api --- src/memos/api/start_config.py | 20 ++++++++++---------- tests/api/test_start_api.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/memos/api/start_config.py b/src/memos/api/start_config.py index bb069240d..314fdb060 100644 --- a/src/memos/api/start_config.py +++ b/src/memos/api/start_config.py @@ -15,7 +15,7 @@ logger = get_logger(__name__) # Global MOS instance with lazy initialization -_MOS_INSTANCE = None +MOS_INSTANCE = None # Default configuration from environment variables DEFAULT_CONFIG = { @@ -87,9 +87,9 @@ def get_mos_instance() -> MOS: Returns: MOS: The MOS instance """ - global _MOS_INSTANCE + global MOS_INSTANCE - if _MOS_INSTANCE is None: + if MOS_INSTANCE is None: # Create configuration temp_config = MOSConfig(**DEFAULT_CONFIG) @@ -108,10 +108,10 @@ def get_mos_instance() -> MOS: create_user_if_not_exists(temp_config.user_id, temp_mos.user_manager) # Now create the actual MOS instance - _MOS_INSTANCE = MOS(config=temp_config) + MOS_INSTANCE = MOS(config=temp_config) logger.info(f"MOS instance created successfully for user: {temp_config.user_id}") - return _MOS_INSTANCE + return MOS_INSTANCE def set_mos_instance(config: MOSConfig) -> MOS: @@ -124,21 +124,21 @@ def set_mos_instance(config: MOSConfig) -> MOS: Returns: MOS: The new MOS instance """ - global _MOS_INSTANCE + global MOS_INSTANCE # Create a temporary user manager to check/create default user temp_user_manager = UserManager() create_user_if_not_exists(config.user_id, temp_user_manager) # Create the MOS instance - _MOS_INSTANCE = MOS(config=config) + MOS_INSTANCE = MOS(config=config) logger.info(f"MOS instance updated with new configuration for user: {config.user_id}") - return _MOS_INSTANCE + return MOS_INSTANCE def reset_mos_instance() -> None: """Reset the MOS instance (useful for testing).""" - global _MOS_INSTANCE - _MOS_INSTANCE = None + global MOS_INSTANCE + MOS_INSTANCE = None logger.info("MOS instance reset") diff --git a/tests/api/test_start_api.py b/tests/api/test_start_api.py index 8184a2321..fa5d432e5 100644 --- a/tests/api/test_start_api.py +++ b/tests/api/test_start_api.py @@ -84,7 +84,7 @@ def mock_mos(): def test_configure(mock_mos): """Test configuration endpoint.""" - with patch("memos.api.start_api.MOS_INSTANCE", None): + with patch("memos.api.start_config.MOS_INSTANCE", None): # Use a valid configuration valid_config = { "user_id": "test_user", From c0bd774df4bf02cd03211172cf01b241db1f01b2 Mon Sep 17 00:00:00 2001 From: yjy Date: Tue, 23 Sep 2025 10:00:09 +0800 Subject: [PATCH 07/21] test_start_api --- tests/api/test_start_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_start_api.py b/tests/api/test_start_api.py index fa5d432e5..bc37555ca 100644 --- a/tests/api/test_start_api.py +++ b/tests/api/test_start_api.py @@ -140,7 +140,7 @@ def test_configure(mock_mos): def test_configure_error(mock_mos): """Test configuration endpoint with error.""" - with patch("memos.api.start_api.MOS_INSTANCE", None): + with patch("memos.api.start_config.MOS_INSTANCE", None): response = client.post("/configure", json={}) assert response.status_code == 422 # FastAPI validation error From 05f485f4c381972c8b5598a7135740663000346e Mon Sep 17 00:00:00 2001 From: yjy Date: Tue, 23 Sep 2025 10:33:03 +0800 Subject: [PATCH 08/21] fix docker start --- docker/Dockerfile | 2 +- src/memos/api/routers/start_router.py | 305 ------------------- src/memos/api/start_api.py | 418 ++++++++++++++++++++++++-- src/memos/api/start_config.py | 144 --------- src/memos/api/start_models.py | 147 --------- tests/api/test_start_api.py | 6 +- 6 files changed, 405 insertions(+), 617 deletions(-) delete mode 100644 src/memos/api/routers/start_router.py delete mode 100644 src/memos/api/start_config.py delete mode 100644 src/memos/api/start_models.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 29636881c..57a4e8857 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,4 +32,4 @@ ENV PYTHONPATH=/app/src EXPOSE 8000 # Start the docker -CMD ["uvicorn", "memos.api.product_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +CMD ["uvicorn", "memos.api.start_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/src/memos/api/routers/start_router.py b/src/memos/api/routers/start_router.py deleted file mode 100644 index 5d136faf9..000000000 --- a/src/memos/api/routers/start_router.py +++ /dev/null @@ -1,305 +0,0 @@ -""" -Basic MemOS API router for core functionality. -This router provides basic CRUD operations for memories, users, and cubes. -""" - -import traceback - -from typing import Any - -from fastapi import APIRouter, HTTPException -from fastapi.responses import RedirectResponse - -from memos.api.start_config import get_mos_instance -from memos.api.start_models import ( - ChatRequest, - ChatResponse, - ConfigRequest, - ConfigResponse, - CubeShare, - MemCubeRegister, - MemoryCreate, - MemoryResponse, - SearchRequest, - SearchResponse, - SimpleResponse, - UserCreate, - UserListResponse, - UserResponse, -) -from memos.log import get_logger -from memos.mem_user.user_manager import UserRole - - -logger = get_logger(__name__) - -router = APIRouter(tags=["Basic MemOS API"]) - - -@router.post("/configure", summary="Configure MemOS", response_model=ConfigResponse) -async def set_config(config_request: ConfigRequest): - """Set MemOS configuration with basic settings.""" - try: - # Get current MOS instance - mos = get_mos_instance() - - # Update configuration based on request - if config_request.user_id is not None: - mos.user_id = config_request.user_id - if config_request.session_id is not None: - mos.session_id = config_request.session_id - if config_request.top_k is not None: - mos.config.top_k = config_request.top_k - if config_request.enable_textual_memory is not None: - mos.config.enable_textual_memory = config_request.enable_textual_memory - if config_request.enable_activation_memory is not None: - mos.config.enable_activation_memory = config_request.enable_activation_memory - - return ConfigResponse(message="Configuration updated successfully") - except Exception as err: - logger.error(f"Failed to set configuration: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.post("/users", summary="Create a new user", response_model=UserResponse) -async def create_user(user_create: UserCreate): - """Create a new user.""" - try: - mos_instance = get_mos_instance() - role = UserRole(user_create.role) - user_id = mos_instance.create_user( - user_id=user_create.user_id, role=role, user_name=user_create.user_name - ) - return UserResponse(message="User created successfully", data={"user_id": user_id}) - except Exception as err: - logger.error(f"Failed to create user: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.get("/users", summary="List all users", response_model=UserListResponse) -async def list_users(): - """List all active users.""" - try: - mos_instance = get_mos_instance() - users = mos_instance.list_users() - return UserListResponse(message="Users retrieved successfully", data=users) - except Exception as err: - logger.error(f"Failed to list users: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.get("/users/me", summary="Get current user info", response_model=UserResponse) -async def get_user_info(): - """Get current user information including accessible cubes.""" - try: - mos_instance = get_mos_instance() - user_info = mos_instance.get_user_info() - return UserResponse(message="User info retrieved successfully", data=user_info) - except Exception as err: - logger.error(f"Failed to get user info: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.post("/mem_cubes", summary="Register a MemCube", response_model=SimpleResponse) -async def register_mem_cube(mem_cube: MemCubeRegister): - """Register a new MemCube.""" - try: - mos_instance = get_mos_instance() - mos_instance.register_mem_cube( - mem_cube_name_or_path=mem_cube.mem_cube_name_or_path, - mem_cube_id=mem_cube.mem_cube_id, - user_id=mem_cube.user_id, - ) - return SimpleResponse(message="MemCube registered successfully") - except Exception as err: - logger.error(f"Failed to register MemCube: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.delete( - "/mem_cubes/{mem_cube_id}", summary="Unregister a MemCube", response_model=SimpleResponse -) -async def unregister_mem_cube(mem_cube_id: str, user_id: str | None = None): - """Unregister a MemCube.""" - try: - mos_instance = get_mos_instance() - mos_instance.unregister_mem_cube(mem_cube_id=mem_cube_id, user_id=user_id) - return SimpleResponse(message="MemCube unregistered successfully") - except Exception as err: - logger.error(f"Failed to unregister MemCube: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.post( - "/mem_cubes/{cube_id}/share", - summary="Share a cube with another user", - response_model=SimpleResponse, -) -async def share_cube(cube_id: str, share_request: CubeShare): - """Share a cube with another user.""" - try: - mos_instance = get_mos_instance() - success = mos_instance.share_cube_with_user(cube_id, share_request.target_user_id) - if success: - return SimpleResponse(message="Cube shared successfully") - else: - raise HTTPException(status_code=500, detail="Failed to share cube") - except Exception as err: - logger.error(f"Failed to share cube: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.post("/memories", summary="Create memories", response_model=SimpleResponse) -async def add_memory(memory_create: MemoryCreate): - """Store new memories in a MemCube.""" - try: - if not any([memory_create.messages, memory_create.memory_content, memory_create.doc_path]): - raise HTTPException( - status_code=400, - detail="Either messages, memory_content, or doc_path must be provided", - ) - - mos_instance = get_mos_instance() - - if memory_create.messages: - messages = [m.model_dump() for m in memory_create.messages] - mos_instance.add( - messages=messages, - mem_cube_id=memory_create.mem_cube_id, - user_id=memory_create.user_id, - ) - elif memory_create.memory_content: - mos_instance.add( - memory_content=memory_create.memory_content, - mem_cube_id=memory_create.mem_cube_id, - user_id=memory_create.user_id, - ) - elif memory_create.doc_path: - mos_instance.add( - doc_path=memory_create.doc_path, - mem_cube_id=memory_create.mem_cube_id, - user_id=memory_create.user_id, - ) - - return SimpleResponse(message="Memories added successfully") - except HTTPException: - raise - except Exception as err: - logger.error(f"Failed to add memory: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.get("/memories", summary="Get all memories", response_model=MemoryResponse) -async def get_all_memories( - mem_cube_id: str | None = None, - user_id: str | None = None, -): - """Retrieve all memories from a MemCube.""" - try: - mos_instance = get_mos_instance() - result = mos_instance.get_all(mem_cube_id=mem_cube_id, user_id=user_id) - return MemoryResponse(message="Memories retrieved successfully", data=result) - except Exception as err: - logger.error(f"Failed to get memories: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.get( - "/memories/{mem_cube_id}/{memory_id}", summary="Get a memory", response_model=MemoryResponse -) -async def get_memory(mem_cube_id: str, memory_id: str, user_id: str | None = None): - """Retrieve a specific memory by ID from a MemCube.""" - try: - mos_instance = get_mos_instance() - result = mos_instance.get(mem_cube_id=mem_cube_id, memory_id=memory_id, user_id=user_id) - return MemoryResponse(message="Memory retrieved successfully", data=result) - except Exception as err: - logger.error(f"Failed to get memory: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.post("/search", summary="Search memories", response_model=SearchResponse) -async def search_memories(search_req: SearchRequest): - """Search for memories across MemCubes.""" - try: - mos_instance = get_mos_instance() - result = mos_instance.search( - query=search_req.query, - user_id=search_req.user_id, - install_cube_ids=search_req.install_cube_ids, - ) - return SearchResponse(message="Search completed successfully", data=result) - except Exception as err: - logger.error(f"Failed to search memories: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.put( - "/memories/{mem_cube_id}/{memory_id}", summary="Update a memory", response_model=SimpleResponse -) -async def update_memory( - mem_cube_id: str, memory_id: str, updated_memory: dict[str, Any], user_id: str | None = None -): - """Update an existing memory in a MemCube.""" - try: - mos_instance = get_mos_instance() - mos_instance.update( - mem_cube_id=mem_cube_id, - memory_id=memory_id, - text_memory_item=updated_memory, - user_id=user_id, - ) - return SimpleResponse(message="Memory updated successfully") - except Exception as err: - logger.error(f"Failed to update memory: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.delete( - "/memories/{mem_cube_id}/{memory_id}", summary="Delete a memory", response_model=SimpleResponse -) -async def delete_memory(mem_cube_id: str, memory_id: str, user_id: str | None = None): - """Delete a specific memory from a MemCube.""" - try: - mos_instance = get_mos_instance() - mos_instance.delete(mem_cube_id=mem_cube_id, memory_id=memory_id, user_id=user_id) - return SimpleResponse(message="Memory deleted successfully") - except Exception as err: - logger.error(f"Failed to delete memory: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.delete( - "/memories/{mem_cube_id}", summary="Delete all memories", response_model=SimpleResponse -) -async def delete_all_memories(mem_cube_id: str, user_id: str | None = None): - """Delete all memories from a MemCube.""" - try: - mos_instance = get_mos_instance() - mos_instance.delete_all(mem_cube_id=mem_cube_id, user_id=user_id) - return SimpleResponse(message="All memories deleted successfully") - except Exception as err: - logger.error(f"Failed to delete all memories: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.post("/chat", summary="Chat with MemOS", response_model=ChatResponse) -async def chat(chat_req: ChatRequest): - """Chat with the MemOS system.""" - try: - mos_instance = get_mos_instance() - response = mos_instance.chat(query=chat_req.query, user_id=chat_req.user_id) - if response is None: - raise HTTPException(status_code=500, detail="No response generated") - return ChatResponse(message="Chat response generated", data=response) - except HTTPException: - raise - except Exception as err: - logger.error(f"Failed to chat: {traceback.format_exc()}") - raise HTTPException(status_code=500, detail=str(err)) from err - - -@router.get("/", summary="Redirect to the OpenAPI documentation", include_in_schema=False) -async def home(): - """Redirect to the OpenAPI documentation.""" - return RedirectResponse(url="/docs", status_code=307) diff --git a/src/memos/api/start_api.py b/src/memos/api/start_api.py index d736971a2..827af9e51 100644 --- a/src/memos/api/start_api.py +++ b/src/memos/api/start_api.py @@ -1,36 +1,404 @@ import logging +import os + +from typing import Any, Generic, TypeVar from dotenv import load_dotenv from fastapi import FastAPI -from fastapi.responses import RedirectResponse +from fastapi.requests import Request +from fastapi.responses import JSONResponse, RedirectResponse +from pydantic import BaseModel, Field -from memos.api.exceptions import APIExceptionHandler from memos.api.middleware.request_context import RequestContextMiddleware -from memos.api.routers.start_router import router as start_router +from memos.configs.mem_os import MOSConfig +from memos.mem_os.main import MOS +from memos.mem_user.user_manager import UserManager, UserRole +from fastapi.responses import RedirectResponse +from memos.api.exceptions import APIExceptionHandler -# Load environment variables -load_dotenv() # Configure logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) -# Create FastAPI application +# Load environment variables +load_dotenv() + +T = TypeVar("T") + +# Default configuration +DEFAULT_CONFIG = { + "user_id": os.getenv("MOS_USER_ID", "default_user"), + "session_id": os.getenv("MOS_SESSION_ID", "default_session"), + "enable_textual_memory": True, + "enable_activation_memory": False, + "top_k": int(os.getenv("MOS_TOP_K", "5")), + "chat_model": { + "backend": os.getenv("MOS_CHAT_MODEL_PROVIDER", "openai"), + "config": { + "model_name_or_path": os.getenv("MOS_CHAT_MODEL", "gpt-3.5-turbo"), + "api_key": os.getenv("OPENAI_API_KEY", "apikey"), + "temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.7")), + "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), + }, + }, +} + +# Initialize MOS instance with lazy initialization +MOS_INSTANCE = None + + +def get_mos_instance(): + """Get or create MOS instance with default user creation.""" + global MOS_INSTANCE + if MOS_INSTANCE is None: + # Create a temporary MOS instance to access user manager + temp_config = MOSConfig(**DEFAULT_CONFIG) + temp_mos = MOS.__new__(MOS) + temp_mos.config = temp_config + temp_mos.user_id = temp_config.user_id + temp_mos.session_id = temp_config.session_id + temp_mos.mem_cubes = {} + temp_mos.chat_llm = None # Will be initialized later + temp_mos.user_manager = UserManager() + + # Create default user if it doesn't exist + if not temp_mos.user_manager.validate_user(temp_config.user_id): + temp_mos.user_manager.create_user( + user_name=temp_config.user_id, role=UserRole.USER, user_id=temp_config.user_id + ) + logger.info(f"Created default user: {temp_config.user_id}") + + # Now create the actual MOS instance + MOS_INSTANCE = MOS(config=temp_config) + + return MOS_INSTANCE + + app = FastAPI( - title="MemOS Basic REST APIs", - description="A REST API for managing and searching memories using MemOS core functionality.", + title="MemOS REST APIs", + description="A REST API for managing and searching memories using MemOS.", version="1.0.0", ) app.add_middleware(RequestContextMiddleware) -# Include routers -app.include_router(start_router) -# Exception handlers -app.exception_handler(ValueError)(APIExceptionHandler.value_error_handler) -app.exception_handler(Exception)(APIExceptionHandler.global_exception_handler) +class BaseRequest(BaseModel): + """Base model for all requests.""" + + user_id: str | None = Field( + None, description="User ID for the request", json_schema_extra={"example": "user123"} + ) + + +class BaseResponse(BaseModel, Generic[T]): + """Base model for all responses.""" + + code: int = Field(200, description="Response status code", json_schema_extra={"example": 200}) + message: str = Field( + ..., description="Response message", json_schema_extra={"example": "Operation successful"} + ) + data: T | None = Field(None, description="Response data") + + +class Message(BaseModel): + role: str = Field( + ..., + description="Role of the message (user or assistant).", + json_schema_extra={"example": "user"}, + ) + content: str = Field( + ..., + description="Message content.", + json_schema_extra={"example": "Hello, how can I help you?"}, + ) + + +class MemoryCreate(BaseRequest): + messages: list[Message] | None = Field( + None, + description="List of messages to store.", + json_schema_extra={"example": [{"role": "user", "content": "Hello"}]}, + ) + mem_cube_id: str | None = Field( + None, description="ID of the memory cube", json_schema_extra={"example": "cube123"} + ) + memory_content: str | None = Field( + None, + description="Content to store as memory", + json_schema_extra={"example": "This is a memory content"}, + ) + doc_path: str | None = Field( + None, + description="Path to document to store", + json_schema_extra={"example": "/path/to/document.txt"}, + ) + + +class SearchRequest(BaseRequest): + query: str = Field( + ..., + description="Search query.", + json_schema_extra={"example": "How to implement a feature?"}, + ) + install_cube_ids: list[str] | None = Field( + None, + description="List of cube IDs to search in", + json_schema_extra={"example": ["cube123", "cube456"]}, + ) + + +class MemCubeRegister(BaseRequest): + mem_cube_name_or_path: str = Field( + ..., + description="Name or path of the MemCube to register.", + json_schema_extra={"example": "/path/to/cube"}, + ) + mem_cube_id: str | None = Field( + None, description="ID for the MemCube", json_schema_extra={"example": "cube123"} + ) + + +class ChatRequest(BaseRequest): + query: str = Field( + ..., + description="Chat query message.", + json_schema_extra={"example": "What is the latest update?"}, + ) + + +class UserCreate(BaseRequest): + user_name: str | None = Field( + None, description="Name of the user", json_schema_extra={"example": "john_doe"} + ) + role: str = Field("user", description="Role of the user", json_schema_extra={"example": "user"}) + user_id: str = Field(..., description="User ID", json_schema_extra={"example": "user123"}) + + +class CubeShare(BaseRequest): + target_user_id: str = Field( + ..., description="Target user ID to share with", json_schema_extra={"example": "user456"} + ) + + +class SimpleResponse(BaseResponse[None]): + """Simple response model for operations without data return.""" + + +class ConfigResponse(BaseResponse[None]): + """Response model for configuration endpoint.""" + + +class MemoryResponse(BaseResponse[dict]): + """Response model for memory operations.""" + + +class SearchResponse(BaseResponse[dict]): + """Response model for search operations.""" + + +class ChatResponse(BaseResponse[str]): + """Response model for chat operations.""" + + +class UserResponse(BaseResponse[dict]): + """Response model for user operations.""" + + +class UserListResponse(BaseResponse[list]): + """Response model for user list operations.""" + + +@app.post("/configure", summary="Configure MemOS", response_model=ConfigResponse) +async def set_config(config: MOSConfig): + """Set MemOS configuration.""" + global MOS_INSTANCE + + # Create a temporary user manager to check/create default user + temp_user_manager = UserManager() + + # Create default user if it doesn't exist + if not temp_user_manager.validate_user(config.user_id): + temp_user_manager.create_user( + user_name=config.user_id, role=UserRole.USER, user_id=config.user_id + ) + logger.info(f"Created default user: {config.user_id}") + + # Now create the MOS instance + MOS_INSTANCE = MOS(config=config) + return ConfigResponse(message="Configuration set successfully") + + +@app.post("/users", summary="Create a new user", response_model=UserResponse) +async def create_user(user_create: UserCreate): + """Create a new user.""" + mos_instance = get_mos_instance() + role = UserRole(user_create.role) + user_id = mos_instance.create_user( + user_id=user_create.user_id, role=role, user_name=user_create.user_name + ) + return UserResponse(message="User created successfully", data={"user_id": user_id}) + + +@app.get("/users", summary="List all users", response_model=UserListResponse) +async def list_users(): + """List all active users.""" + mos_instance = get_mos_instance() + users = mos_instance.list_users() + return UserListResponse(message="Users retrieved successfully", data=users) + + +@app.get("/users/me", summary="Get current user info", response_model=UserResponse) +async def get_user_info(): + """Get current user information including accessible cubes.""" + mos_instance = get_mos_instance() + user_info = mos_instance.get_user_info() + return UserResponse(message="User info retrieved successfully", data=user_info) + + +@app.post("/mem_cubes", summary="Register a MemCube", response_model=SimpleResponse) +async def register_mem_cube(mem_cube: MemCubeRegister): + """Register a new MemCube.""" + mos_instance = get_mos_instance() + mos_instance.register_mem_cube( + mem_cube_name_or_path=mem_cube.mem_cube_name_or_path, + mem_cube_id=mem_cube.mem_cube_id, + user_id=mem_cube.user_id, + ) + return SimpleResponse(message="MemCube registered successfully") + + +@app.delete( + "/mem_cubes/{mem_cube_id}", summary="Unregister a MemCube", response_model=SimpleResponse +) +async def unregister_mem_cube(mem_cube_id: str, user_id: str | None = None): + """Unregister a MemCube.""" + mos_instance = get_mos_instance() + mos_instance.unregister_mem_cube(mem_cube_id=mem_cube_id, user_id=user_id) + return SimpleResponse(message="MemCube unregistered successfully") + + +@app.post( + "/mem_cubes/{cube_id}/share", + summary="Share a cube with another user", + response_model=SimpleResponse, +) +async def share_cube(cube_id: str, share_request: CubeShare): + """Share a cube with another user.""" + mos_instance = get_mos_instance() + success = mos_instance.share_cube_with_user(cube_id, share_request.target_user_id) + if success: + return SimpleResponse(message="Cube shared successfully") + else: + raise ValueError("Failed to share cube") + + +@app.post("/memories", summary="Create memories", response_model=SimpleResponse) +async def add_memory(memory_create: MemoryCreate): + """Store new memories in a MemCube.""" + if not any([memory_create.messages, memory_create.memory_content, memory_create.doc_path]): + raise ValueError("Either messages, memory_content, or doc_path must be provided") + mos_instance = get_mos_instance() + if memory_create.messages: + messages = [m.model_dump() for m in memory_create.messages] + mos_instance.add( + messages=messages, + mem_cube_id=memory_create.mem_cube_id, + user_id=memory_create.user_id, + ) + elif memory_create.memory_content: + mos_instance.add( + memory_content=memory_create.memory_content, + mem_cube_id=memory_create.mem_cube_id, + user_id=memory_create.user_id, + ) + elif memory_create.doc_path: + mos_instance.add( + doc_path=memory_create.doc_path, + mem_cube_id=memory_create.mem_cube_id, + user_id=memory_create.user_id, + ) + return SimpleResponse(message="Memories added successfully") + + +@app.get("/memories", summary="Get all memories", response_model=MemoryResponse) +async def get_all_memories( + mem_cube_id: str | None = None, + user_id: str | None = None, +): + """Retrieve all memories from a MemCube.""" + mos_instance = get_mos_instance() + result = mos_instance.get_all(mem_cube_id=mem_cube_id, user_id=user_id) + return MemoryResponse(message="Memories retrieved successfully", data=result) + + +@app.get( + "/memories/{mem_cube_id}/{memory_id}", summary="Get a memory", response_model=MemoryResponse +) +async def get_memory(mem_cube_id: str, memory_id: str, user_id: str | None = None): + """Retrieve a specific memory by ID from a MemCube.""" + mos_instance = get_mos_instance() + result = mos_instance.get(mem_cube_id=mem_cube_id, memory_id=memory_id, user_id=user_id) + return MemoryResponse(message="Memory retrieved successfully", data=result) + + +@app.post("/search", summary="Search memories", response_model=SearchResponse) +async def search_memories(search_req: SearchRequest): + """Search for memories across MemCubes.""" + mos_instance = get_mos_instance() + result = mos_instance.search( + query=search_req.query, + user_id=search_req.user_id, + install_cube_ids=search_req.install_cube_ids, + ) + return SearchResponse(message="Search completed successfully", data=result) + + +@app.put( + "/memories/{mem_cube_id}/{memory_id}", summary="Update a memory", response_model=SimpleResponse +) +async def update_memory( + mem_cube_id: str, memory_id: str, updated_memory: dict[str, Any], user_id: str | None = None +): + """Update an existing memory in a MemCube.""" + mos_instance = get_mos_instance() + mos_instance.update( + mem_cube_id=mem_cube_id, + memory_id=memory_id, + text_memory_item=updated_memory, + user_id=user_id, + ) + return SimpleResponse(message="Memory updated successfully") + + +@app.delete( + "/memories/{mem_cube_id}/{memory_id}", summary="Delete a memory", response_model=SimpleResponse +) +async def delete_memory(mem_cube_id: str, memory_id: str, user_id: str | None = None): + """Delete a specific memory from a MemCube.""" + mos_instance = get_mos_instance() + mos_instance.delete(mem_cube_id=mem_cube_id, memory_id=memory_id, user_id=user_id) + return SimpleResponse(message="Memory deleted successfully") + + +@app.delete("/memories/{mem_cube_id}", summary="Delete all memories", response_model=SimpleResponse) +async def delete_all_memories(mem_cube_id: str, user_id: str | None = None): + """Delete all memories from a MemCube.""" + mos_instance = get_mos_instance() + mos_instance.delete_all(mem_cube_id=mem_cube_id, user_id=user_id) + return SimpleResponse(message="All memories deleted successfully") + + +@app.post("/chat", summary="Chat with MemOS", response_model=ChatResponse) +async def chat(chat_req: ChatRequest): + """Chat with the MemOS system.""" + mos_instance = get_mos_instance() + response = mos_instance.chat(query=chat_req.query, user_id=chat_req.user_id) + if response is None: + raise ValueError("No response generated") + return ChatResponse(message="Chat response generated", data=response) @app.get("/", summary="Redirect to the OpenAPI documentation", include_in_schema=False) @@ -39,6 +407,25 @@ async def home(): return RedirectResponse(url="/docs", status_code=307) + +@app.exception_handler(ValueError) +async def value_error_handler(request: Request, exc: ValueError): + """Handle ValueError exceptions globally.""" + return JSONResponse( + status_code=400, + content={"code": 400, "message": str(exc), "data": None}, + ) + + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + """Handle all unhandled exceptions globally.""" + logger.exception("Unhandled error:") + return JSONResponse( + status_code=500, + content={"code": 500, "message": str(exc), "data": None}, + ) + if __name__ == "__main__": import argparse @@ -48,7 +435,4 @@ async def home(): parser.add_argument("--port", type=int, default=8000, help="Port to run the server on") parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to run the server on") parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development") - args = parser.parse_args() - - logger.info(f"Starting MemOS Basic API server on {args.host}:{args.port}") - uvicorn.run("memos.api.start_api:app", host=args.host, port=args.port, reload=args.reload) + args = parser.parse_args() \ No newline at end of file diff --git a/src/memos/api/start_config.py b/src/memos/api/start_config.py deleted file mode 100644 index 314fdb060..000000000 --- a/src/memos/api/start_config.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Configuration management for the basic MemOS API. -""" - -import os - -from typing import Any - -from memos.configs.mem_os import MOSConfig -from memos.log import get_logger -from memos.mem_os.main import MOS -from memos.mem_user.user_manager import UserManager, UserRole - - -logger = get_logger(__name__) - -# Global MOS instance with lazy initialization -MOS_INSTANCE = None - -# Default configuration from environment variables -DEFAULT_CONFIG = { - "user_id": os.getenv("MOS_USER_ID", "default_user"), - "session_id": os.getenv("MOS_SESSION_ID", "default_session"), - "enable_textual_memory": True, - "enable_activation_memory": False, - "top_k": int(os.getenv("MOS_TOP_K", "5")), - "chat_model": { - "backend": os.getenv("MOS_CHAT_MODEL_PROVIDER", "openai"), - "config": { - "model_name_or_path": os.getenv("MOS_CHAT_MODEL", "gpt-3.5-turbo"), - "api_key": os.getenv("OPENAI_API_KEY", "apikey"), - "temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.7")), - "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), - }, - }, - "mem_reader": { - "backend": "simple_struct", - "config": { - "llm": { - "backend": os.getenv("MOS_MEM_READER_LLM_PROVIDER", "openai"), - "config": { - "model_name_or_path": os.getenv("MOS_MEM_READER_MODEL", "gpt-3.5-turbo"), - "api_key": os.getenv("OPENAI_API_KEY", "apikey"), - "temperature": float(os.getenv("MOS_MEM_READER_TEMPERATURE", "0.7")), - "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), - }, - }, - "embedder": { - "backend": "universal_api", - "config": { - "provider": os.getenv("MOS_EMBEDDER_PROVIDER", "openai"), - "model_name_or_path": os.getenv("MOS_EMBEDDER_MODEL", "text-embedding-ada-002"), - "api_key": os.getenv("OPENAI_API_KEY", "apikey"), - "base_url": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), - }, - }, - "chunker": { - "backend": "sentence", - "config": { - "tokenizer_or_token_counter": "gpt2", - "chunk_size": int(os.getenv("MOS_CHUNK_SIZE", "512")), - "chunk_overlap": int(os.getenv("MOS_CHUNK_OVERLAP", "128")), - "min_sentences_per_chunk": 1, - }, - }, - }, - }, -} - - -def get_default_config() -> dict[str, Any]: - """Get default configuration from environment variables.""" - return DEFAULT_CONFIG.copy() - - -def create_user_if_not_exists(user_id: str, user_manager: UserManager) -> None: - """Create user if it doesn't exist.""" - if not user_manager.validate_user(user_id): - user_manager.create_user(user_name=user_id, role=UserRole.USER, user_id=user_id) - logger.info(f"Created default user: {user_id}") - - -def get_mos_instance() -> MOS: - """ - Get or create MOS instance with default user creation. - - Returns: - MOS: The MOS instance - """ - global MOS_INSTANCE - - if MOS_INSTANCE is None: - # Create configuration - temp_config = MOSConfig(**DEFAULT_CONFIG) - - # Create a temporary MOS instance to access user manager - # This is a workaround for the chicken-and-egg problem: - # MOS needs a valid user, but we need MOS to create users - temp_mos = MOS.__new__(MOS) - temp_mos.config = temp_config - temp_mos.user_id = temp_config.user_id - temp_mos.session_id = temp_config.session_id - temp_mos.mem_cubes = {} - temp_mos.chat_llm = None - temp_mos.user_manager = UserManager() - - # Create default user if it doesn't exist - create_user_if_not_exists(temp_config.user_id, temp_mos.user_manager) - - # Now create the actual MOS instance - MOS_INSTANCE = MOS(config=temp_config) - logger.info(f"MOS instance created successfully for user: {temp_config.user_id}") - - return MOS_INSTANCE - - -def set_mos_instance(config: MOSConfig) -> MOS: - """ - Set a new MOS instance with the provided configuration. - - Args: - config: The MOSConfig to use - - Returns: - MOS: The new MOS instance - """ - global MOS_INSTANCE - - # Create a temporary user manager to check/create default user - temp_user_manager = UserManager() - create_user_if_not_exists(config.user_id, temp_user_manager) - - # Create the MOS instance - MOS_INSTANCE = MOS(config=config) - logger.info(f"MOS instance updated with new configuration for user: {config.user_id}") - - return MOS_INSTANCE - - -def reset_mos_instance() -> None: - """Reset the MOS instance (useful for testing).""" - global MOS_INSTANCE - MOS_INSTANCE = None - logger.info("MOS instance reset") diff --git a/src/memos/api/start_models.py b/src/memos/api/start_models.py deleted file mode 100644 index ee447053f..000000000 --- a/src/memos/api/start_models.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -Data models for the basic MemOS API endpoints. -""" - -from typing import Generic, TypeVar - -from pydantic import BaseModel, Field - - -T = TypeVar("T") - - -class BaseRequest(BaseModel): - """Base model for all requests.""" - - user_id: str | None = Field( - None, description="User ID for the request", json_schema_extra={"example": "user123"} - ) - - -class BaseResponse(BaseModel, Generic[T]): - """Base model for all responses.""" - - code: int = Field(200, description="Response status code", json_schema_extra={"example": 200}) - message: str = Field( - ..., description="Response message", json_schema_extra={"example": "Operation successful"} - ) - data: T | None = Field(None, description="Response data") - - -class Message(BaseModel): - role: str = Field( - ..., - description="Role of the message (user or assistant).", - json_schema_extra={"example": "user"}, - ) - content: str = Field( - ..., - description="Message content.", - json_schema_extra={"example": "Hello, how can I help you?"}, - ) - - -class MemoryCreate(BaseRequest): - messages: list[Message] | None = Field( - None, - description="List of messages to store.", - json_schema_extra={"example": [{"role": "user", "content": "Hello"}]}, - ) - mem_cube_id: str | None = Field( - None, description="ID of the memory cube", json_schema_extra={"example": "cube123"} - ) - memory_content: str | None = Field( - None, - description="Content to store as memory", - json_schema_extra={"example": "This is a memory content"}, - ) - doc_path: str | None = Field( - None, - description="Path to document to store", - json_schema_extra={"example": "/path/to/document.txt"}, - ) - - -class SearchRequest(BaseRequest): - query: str = Field( - ..., - description="Search query.", - json_schema_extra={"example": "How to implement a feature?"}, - ) - install_cube_ids: list[str] | None = Field( - None, - description="List of cube IDs to search in", - json_schema_extra={"example": ["cube123", "cube456"]}, - ) - - -class MemCubeRegister(BaseRequest): - mem_cube_name_or_path: str = Field( - ..., - description="Name or path of the MemCube to register.", - json_schema_extra={"example": "/path/to/cube"}, - ) - mem_cube_id: str | None = Field( - None, description="ID for the MemCube", json_schema_extra={"example": "cube123"} - ) - - -class ChatRequest(BaseRequest): - query: str = Field( - ..., - description="Chat query message.", - json_schema_extra={"example": "What is the latest update?"}, - ) - - -class UserCreate(BaseRequest): - user_name: str | None = Field( - None, description="Name of the user", json_schema_extra={"example": "john_doe"} - ) - role: str = Field("USER", description="Role of the user", json_schema_extra={"example": "USER"}) - user_id: str = Field(..., description="User ID", json_schema_extra={"example": "user123"}) - - -class CubeShare(BaseRequest): - target_user_id: str = Field( - ..., description="Target user ID to share with", json_schema_extra={"example": "user456"} - ) - - -# Response models -class SimpleResponse(BaseResponse[None]): - """Simple response model for operations without data return.""" - - -class ConfigRequest(BaseModel): - """Configuration request model for basic settings.""" - - user_id: str | None = Field(None, description="User ID to configure") - session_id: str | None = Field(None, description="Session ID to configure") - top_k: int | None = Field(None, description="Top K memories to retrieve") - enable_textual_memory: bool | None = Field(None, description="Enable textual memory") - enable_activation_memory: bool | None = Field(None, description="Enable activation memory") - - -class ConfigResponse(BaseResponse[None]): - """Response model for configuration endpoint.""" - - -class MemoryResponse(BaseResponse[dict]): - """Response model for memory operations.""" - - -class SearchResponse(BaseResponse[dict]): - """Response model for search operations.""" - - -class ChatResponse(BaseResponse[str]): - """Response model for chat operations.""" - - -class UserResponse(BaseResponse[dict]): - """Response model for user operations.""" - - -class UserListResponse(BaseResponse[list]): - """Response model for user list operations.""" diff --git a/tests/api/test_start_api.py b/tests/api/test_start_api.py index bc37555ca..c4f6eff64 100644 --- a/tests/api/test_start_api.py +++ b/tests/api/test_start_api.py @@ -57,7 +57,7 @@ @pytest.fixture def mock_mos(): """Mock MOS instance for testing.""" - with patch("memos.api.start_config.get_mos_instance") as mock_get_mos: + with patch("memos.api.start_api.get_mos_instance") as mock_get_mos: # Create a mock MOS instance mock_instance = Mock() @@ -84,7 +84,7 @@ def mock_mos(): def test_configure(mock_mos): """Test configuration endpoint.""" - with patch("memos.api.start_config.MOS_INSTANCE", None): + with patch("memos.api.start_api.MOS_INSTANCE", None): # Use a valid configuration valid_config = { "user_id": "test_user", @@ -140,7 +140,7 @@ def test_configure(mock_mos): def test_configure_error(mock_mos): """Test configuration endpoint with error.""" - with patch("memos.api.start_config.MOS_INSTANCE", None): + with patch("memos.api.start_api.MOS_INSTANCE", None): response = client.post("/configure", json={}) assert response.status_code == 422 # FastAPI validation error From 7c77883128897d38797a4b807664affef4869b8c Mon Sep 17 00:00:00 2001 From: yjy Date: Tue, 23 Sep 2025 10:34:25 +0800 Subject: [PATCH 09/21] update start_api --- src/memos/api/start_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/memos/api/start_api.py b/src/memos/api/start_api.py index 827af9e51..bb11a534a 100644 --- a/src/memos/api/start_api.py +++ b/src/memos/api/start_api.py @@ -14,8 +14,6 @@ from memos.mem_os.main import MOS from memos.mem_user.user_manager import UserManager, UserRole -from fastapi.responses import RedirectResponse -from memos.api.exceptions import APIExceptionHandler # Configure logging @@ -400,7 +398,6 @@ async def chat(chat_req: ChatRequest): raise ValueError("No response generated") return ChatResponse(message="Chat response generated", data=response) - @app.get("/", summary="Redirect to the OpenAPI documentation", include_in_schema=False) async def home(): """Redirect to the OpenAPI documentation.""" From 3dd8e2e38ef27e80d545d001f96a19abe7ede199 Mon Sep 17 00:00:00 2001 From: yjy Date: Tue, 23 Sep 2025 10:35:28 +0800 Subject: [PATCH 10/21] update start_api --- src/memos/api/start_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/memos/api/start_api.py b/src/memos/api/start_api.py index bb11a534a..1c94fda54 100644 --- a/src/memos/api/start_api.py +++ b/src/memos/api/start_api.py @@ -15,7 +15,6 @@ from memos.mem_user.user_manager import UserManager, UserRole - # Configure logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) @@ -398,13 +397,13 @@ async def chat(chat_req: ChatRequest): raise ValueError("No response generated") return ChatResponse(message="Chat response generated", data=response) + @app.get("/", summary="Redirect to the OpenAPI documentation", include_in_schema=False) async def home(): """Redirect to the OpenAPI documentation.""" return RedirectResponse(url="/docs", status_code=307) - @app.exception_handler(ValueError) async def value_error_handler(request: Request, exc: ValueError): """Handle ValueError exceptions globally.""" From f7d29f86844699cde6fac46d4bb3c77421338201 Mon Sep 17 00:00:00 2001 From: yjy Date: Tue, 23 Sep 2025 10:36:22 +0800 Subject: [PATCH 11/21] update start_api --- docker/Dockerfile | 2 +- src/memos/api/start_api.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 57a4e8857..29636881c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,4 +32,4 @@ ENV PYTHONPATH=/app/src EXPOSE 8000 # Start the docker -CMD ["uvicorn", "memos.api.start_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +CMD ["uvicorn", "memos.api.product_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/src/memos/api/start_api.py b/src/memos/api/start_api.py index 1c94fda54..5e56101bf 100644 --- a/src/memos/api/start_api.py +++ b/src/memos/api/start_api.py @@ -425,8 +425,6 @@ async def global_exception_handler(request: Request, exc: Exception): if __name__ == "__main__": import argparse - import uvicorn - parser = argparse.ArgumentParser() parser.add_argument("--port", type=int, default=8000, help="Port to run the server on") parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to run the server on") From c817aac5d64b134f4508d81e459f9dbd263c1782 Mon Sep 17 00:00:00 2001 From: yjy Date: Tue, 23 Sep 2025 11:49:31 +0800 Subject: [PATCH 12/21] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/memos/api/start_api.py | 3 ++- src/memos/memories/activation/kv.py | 3 ++- .../memories/textual/tree_text_memory/organize/handler.py | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/memos/api/start_api.py b/src/memos/api/start_api.py index 5e56101bf..cbcdf6ce2 100644 --- a/src/memos/api/start_api.py +++ b/src/memos/api/start_api.py @@ -422,6 +422,7 @@ async def global_exception_handler(request: Request, exc: Exception): content={"code": 500, "message": str(exc), "data": None}, ) + if __name__ == "__main__": import argparse @@ -429,4 +430,4 @@ async def global_exception_handler(request: Request, exc: Exception): parser.add_argument("--port", type=int, default=8000, help="Port to run the server on") parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to run the server on") parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development") - args = parser.parse_args() \ No newline at end of file + args = parser.parse_args() diff --git a/src/memos/memories/activation/kv.py b/src/memos/memories/activation/kv.py index 06cef794f..2fa08590f 100644 --- a/src/memos/memories/activation/kv.py +++ b/src/memos/memories/activation/kv.py @@ -1,9 +1,10 @@ import os import pickle + from datetime import datetime from importlib.metadata import version -from packaging.version import Version +from packaging.version import Version from transformers import DynamicCache from memos.configs.memory import KVCacheMemoryConfig diff --git a/src/memos/memories/textual/tree_text_memory/organize/handler.py b/src/memos/memories/textual/tree_text_memory/organize/handler.py index a1121fcd2..271902ca0 100644 --- a/src/memos/memories/textual/tree_text_memory/organize/handler.py +++ b/src/memos/memories/textual/tree_text_memory/organize/handler.py @@ -1,5 +1,6 @@ import json import re + from datetime import datetime from dateutil import parser @@ -14,6 +15,7 @@ MEMORY_RELATION_RESOLVER_PROMPT, ) + logger = get_logger(__name__) From 3206a15cf942ea263627a8811db5fa7e552379bb Mon Sep 17 00:00:00 2001 From: yjy Date: Sat, 4 Oct 2025 13:29:08 +0800 Subject: [PATCH 13/21] update start_api --- src/memos/api/start_api.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/memos/api/start_api.py b/src/memos/api/start_api.py index cbcdf6ce2..0a73c3085 100644 --- a/src/memos/api/start_api.py +++ b/src/memos/api/start_api.py @@ -40,6 +40,40 @@ "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), }, }, + # Ensure mem_reader has required fields to satisfy Pydantic validation + # This prevents `MemReaderConfigFactory` missing-field errors when MOSConfig is instantiated + "mem_reader": { + "backend": "simple_struct", + "config": { + "llm": { + "backend": "openai", + "config": { + "model_name_or_path": os.getenv("MOS_CHAT_MODEL", "gpt-3.5-turbo"), + "api_key": os.getenv("OPENAI_API_KEY", "apikey"), + "temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.7")), + "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), + }, + }, + "embedder": { + "backend": "universal_api", + "config": { + "provider": "openai", + "api_key": os.getenv("OPENAI_API_KEY", "apikey"), + "model_name_or_path": os.getenv("MOS_EMBED_MODEL", "text-embedding-3-large"), + "base_url": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), + }, + }, + "chunker": { + "backend": "sentence", + "config": { + "tokenizer_or_token_counter": "gpt2", + "chunk_size": int(os.getenv("MOS_CHUNK_SIZE", "512")), + "chunk_overlap": int(os.getenv("MOS_CHUNK_OVERLAP", "128")), + "min_sentences_per_chunk": 1, + }, + }, + }, + }, } # Initialize MOS instance with lazy initialization @@ -170,7 +204,7 @@ class UserCreate(BaseRequest): user_name: str | None = Field( None, description="Name of the user", json_schema_extra={"example": "john_doe"} ) - role: str = Field("user", description="Role of the user", json_schema_extra={"example": "user"}) + role: str = Field("USER", description="Role of the user", json_schema_extra={"example": "USER"}) user_id: str = Field(..., description="User ID", json_schema_extra={"example": "user123"}) From bf5b954b6d8df364e7e82d4a2e88731cee4de717 Mon Sep 17 00:00:00 2001 From: pursues <15180521816@163.com> Date: Tue, 23 Dec 2025 17:08:01 +0800 Subject: [PATCH 14/21] update .env.example --- docker/.env.example | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index ac921beb5..96a4e2d3e 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -20,7 +20,7 @@ MOS_TOP_K=50 ## Chat LLM (main dialogue) MOS_CHAT_MODEL=gpt-4o-mini MOS_CHAT_TEMPERATURE=0.8 -MOS_MAX_TOKENS=8000 +MOS_MAX_TOKENS=2048 MOS_TOP_P=0.9 MOS_CHAT_MODEL_PROVIDER=openai # openai | huggingface | vllm MOS_MODEL_SCHEMA=memos.configs.llm.VLLMLLMConfig # vllm only: config class path; keep default unless you extend it @@ -51,18 +51,27 @@ MOS_RERANKER_HEADERS_EXTRA= # extra headers, JSON string, e.g. {"A MOS_RERANKER_STRATEGY=single_turn MOS_RERANK_SOURCE= # optional rerank scope, e.g., history/stream/custom + +# External Services (for evaluation scripts) +ZEP_API_KEY=your_zep_api_key_here +MEM0_API_KEY=your_mem0_api_key_here +MODEL=gpt-4o-mini +EMBEDDING_MODEL=nomic-embed-text:latest + ## Internet search & preference memory ENABLE_INTERNET=false BOCHA_API_KEY= # required if ENABLE_INTERNET=true +XINYU_API_KEY= +XINYU_SEARCH_ENGINE_ID= SEARCH_MODE=fast # fast | fine | mixture FAST_GRAPH=false BM25_CALL=false VEC_COT_CALL=false FINE_STRATEGY=rewrite # rewrite | recreate | deep_search -ENABLE_ACTIVATION_MEMORY=false -ENABLE_PREFERENCE_MEMORY=true +ENABLE_ACTIVATION_MEMORY=false # Enable activation memory +ENABLE_PREFERENCE_MEMORY=true # Enable preference memory PREFERENCE_ADDER_MODE=fast # fast | safe -DEDUP_PREF_EXP_BY_TEXTUAL=false +DEDUP_PREF_EXP_BY_TEXTUAL=false # Deduplicate preference explanation by text ## Reader chunking MEM_READER_BACKEND=simple_struct # simple_struct | strategy_struct @@ -72,7 +81,7 @@ MEM_READER_CHAT_CHUNK_SESS_SIZE=10 # sessions per chunk (default mode) MEM_READER_CHAT_CHUNK_OVERLAP=2 # overlap between chunks ## Scheduler (MemScheduler / API) -MOS_ENABLE_SCHEDULER=false +MOS_ENABLE_SCHEDULER=false # Enable task scheduler MOS_SCHEDULER_TOP_K=10 MOS_SCHEDULER_ACT_MEM_UPDATE_INTERVAL=300 MOS_SCHEDULER_CONTEXT_WINDOW_SIZE=5 @@ -90,7 +99,7 @@ NEO4J_URI=bolt://localhost:7687 # required when backend=neo4j* NEO4J_USER=neo4j # required when backend=neo4j* NEO4J_PASSWORD=12345678 # required when backend=neo4j* NEO4J_DB_NAME=neo4j # required for shared-db mode -MOS_NEO4J_SHARED_DB=false +MOS_NEO4J_SHARED_DB=false # Enable Neo4j shared database mode QDRANT_HOST=localhost QDRANT_PORT=6333 # For Qdrant Cloud / remote endpoint (takes priority if set): @@ -99,7 +108,7 @@ QDRANT_API_KEY=your_qdrant_key MILVUS_URI=http://localhost:19530 # required when ENABLE_PREFERENCE_MEMORY=true MILVUS_USER_NAME=root # same as above MILVUS_PASSWORD=12345678 # same as above -NEBULAR_HOSTS=["localhost"] +NEBULAR_HOSTS=["localhost"] # Nebular graph database host NEBULAR_USER=root NEBULAR_PASSWORD=xxxxxx NEBULAR_SPACE=shared-tree-textual-memory @@ -121,6 +130,8 @@ POLAR_DB_USER=root POLAR_DB_PASSWORD=123456 POLAR_DB_DB_NAME=shared_memos_db POLAR_DB_USE_MULTI_DB=false +# PolarDB pool max connections +POLARDB_POOL_MAX_CONN=100 ## Redis (scheduler queue) — fill only if you want scheduler queues in Redis; otherwise in-memory queue is used REDIS_HOST=localhost # global Redis endpoint (preferred over MEMSCHEDULER_*) @@ -175,3 +186,12 @@ CUSTOM_LOGGER_WORKERS=2 ## SDK / external client MEMOS_API_KEY= MEMOS_BASE_URL=https://memos.memtensor.cn/api/openmem/v1 + + +CHAT_MODEL_LIST='[{ + "backend": "deepseek", + "api_base": "http://localhost:1234", + "api_key": "your-api-key", + "model_name_or_path": "deepseek-r1", + "support_models": ["deepseek-r1"] +}]' From 0ed79f990466f5a97c783869a5a08272377c5da6 Mon Sep 17 00:00:00 2001 From: pursues <15180521816@163.com> Date: Tue, 23 Dec 2025 17:14:54 +0800 Subject: [PATCH 15/21] back start_api --- src/memos/api/start_api.py | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/src/memos/api/start_api.py b/src/memos/api/start_api.py index 0a73c3085..cbcdf6ce2 100644 --- a/src/memos/api/start_api.py +++ b/src/memos/api/start_api.py @@ -40,40 +40,6 @@ "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), }, }, - # Ensure mem_reader has required fields to satisfy Pydantic validation - # This prevents `MemReaderConfigFactory` missing-field errors when MOSConfig is instantiated - "mem_reader": { - "backend": "simple_struct", - "config": { - "llm": { - "backend": "openai", - "config": { - "model_name_or_path": os.getenv("MOS_CHAT_MODEL", "gpt-3.5-turbo"), - "api_key": os.getenv("OPENAI_API_KEY", "apikey"), - "temperature": float(os.getenv("MOS_CHAT_TEMPERATURE", "0.7")), - "api_base": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), - }, - }, - "embedder": { - "backend": "universal_api", - "config": { - "provider": "openai", - "api_key": os.getenv("OPENAI_API_KEY", "apikey"), - "model_name_or_path": os.getenv("MOS_EMBED_MODEL", "text-embedding-3-large"), - "base_url": os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"), - }, - }, - "chunker": { - "backend": "sentence", - "config": { - "tokenizer_or_token_counter": "gpt2", - "chunk_size": int(os.getenv("MOS_CHUNK_SIZE", "512")), - "chunk_overlap": int(os.getenv("MOS_CHUNK_OVERLAP", "128")), - "min_sentences_per_chunk": 1, - }, - }, - }, - }, } # Initialize MOS instance with lazy initialization @@ -204,7 +170,7 @@ class UserCreate(BaseRequest): user_name: str | None = Field( None, description="Name of the user", json_schema_extra={"example": "john_doe"} ) - role: str = Field("USER", description="Role of the user", json_schema_extra={"example": "USER"}) + role: str = Field("user", description="Role of the user", json_schema_extra={"example": "user"}) user_id: str = Field(..., description="User ID", json_schema_extra={"example": "user123"}) From 51334f718782bde3d1fb41f6bec3be18ddeca7ea Mon Sep 17 00:00:00 2001 From: pursues <15180521816@163.com> Date: Tue, 23 Dec 2025 17:18:59 +0800 Subject: [PATCH 16/21] update --- docker/.env.example | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 96a4e2d3e..5fefac269 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -68,10 +68,10 @@ FAST_GRAPH=false BM25_CALL=false VEC_COT_CALL=false FINE_STRATEGY=rewrite # rewrite | recreate | deep_search -ENABLE_ACTIVATION_MEMORY=false # Enable activation memory -ENABLE_PREFERENCE_MEMORY=true # Enable preference memory +ENABLE_ACTIVATION_MEMORY=false +ENABLE_PREFERENCE_MEMORY=true PREFERENCE_ADDER_MODE=fast # fast | safe -DEDUP_PREF_EXP_BY_TEXTUAL=false # Deduplicate preference explanation by text +DEDUP_PREF_EXP_BY_TEXTUAL=false ## Reader chunking MEM_READER_BACKEND=simple_struct # simple_struct | strategy_struct @@ -81,7 +81,7 @@ MEM_READER_CHAT_CHUNK_SESS_SIZE=10 # sessions per chunk (default mode) MEM_READER_CHAT_CHUNK_OVERLAP=2 # overlap between chunks ## Scheduler (MemScheduler / API) -MOS_ENABLE_SCHEDULER=false # Enable task scheduler +MOS_ENABLE_SCHEDULER=false MOS_SCHEDULER_TOP_K=10 MOS_SCHEDULER_ACT_MEM_UPDATE_INTERVAL=300 MOS_SCHEDULER_CONTEXT_WINDOW_SIZE=5 @@ -99,7 +99,7 @@ NEO4J_URI=bolt://localhost:7687 # required when backend=neo4j* NEO4J_USER=neo4j # required when backend=neo4j* NEO4J_PASSWORD=12345678 # required when backend=neo4j* NEO4J_DB_NAME=neo4j # required for shared-db mode -MOS_NEO4J_SHARED_DB=false # Enable Neo4j shared database mode +MOS_NEO4J_SHARED_DB=false QDRANT_HOST=localhost QDRANT_PORT=6333 # For Qdrant Cloud / remote endpoint (takes priority if set): @@ -108,7 +108,7 @@ QDRANT_API_KEY=your_qdrant_key MILVUS_URI=http://localhost:19530 # required when ENABLE_PREFERENCE_MEMORY=true MILVUS_USER_NAME=root # same as above MILVUS_PASSWORD=12345678 # same as above -NEBULAR_HOSTS=["localhost"] # Nebular graph database host +NEBULAR_HOSTS=["localhost"] NEBULAR_USER=root NEBULAR_PASSWORD=xxxxxx NEBULAR_SPACE=shared-tree-textual-memory @@ -130,7 +130,6 @@ POLAR_DB_USER=root POLAR_DB_PASSWORD=123456 POLAR_DB_DB_NAME=shared_memos_db POLAR_DB_USE_MULTI_DB=false -# PolarDB pool max connections POLARDB_POOL_MAX_CONN=100 ## Redis (scheduler queue) — fill only if you want scheduler queues in Redis; otherwise in-memory queue is used @@ -187,7 +186,6 @@ CUSTOM_LOGGER_WORKERS=2 MEMOS_API_KEY= MEMOS_BASE_URL=https://memos.memtensor.cn/api/openmem/v1 - CHAT_MODEL_LIST='[{ "backend": "deepseek", "api_base": "http://localhost:1234", From 4a575b961b56b99954762356e984d83bf47bc5aa Mon Sep 17 00:00:00 2001 From: pursues <15180521816@163.com> Date: Wed, 24 Dec 2025 17:57:38 +0800 Subject: [PATCH 17/21] update Dockerfile --- docker/.env.example | 4 ---- docker/Dockerfile | 30 ++++++------------------------ 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 5fefac269..20396464e 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -177,10 +177,6 @@ OSS_ACCESS_KEY_ID= OSS_ACCESS_KEY_SECRET= OSS_PUBLIC_BASE_URL= -## Logging / external sink -CUSTOM_LOGGER_URL= -CUSTOM_LOGGER_TOKEN= -CUSTOM_LOGGER_WORKERS=2 ## SDK / external client MEMOS_API_KEY= diff --git a/docker/Dockerfile b/docker/Dockerfile index 29636881c..68596c383 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,35 +1,17 @@ -# Base image -FROM python:3.11-slim +# Simplified package url +FROM registry.cn-shanghai.aliyuncs.com/memtensor/memos:base-v1.0 -# Install dependencies -RUN apt-get update && apt-get install -y \ - gcc \ - g++ \ - build-essential \ - libffi-dev \ - python3-dev \ - curl \ - && rm -rf /var/lib/apt/lists/* +# Full package url +# FROM registry.cn-shanghai.aliyuncs.com/memtensor/memos-full-base:v1.0.0 -# Set working directory WORKDIR /app -# Set Hugging Face mirror ENV HF_ENDPOINT=https://hf-mirror.com -# Install Python packages -COPY docker/requirements.txt . -RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt +ENV PYTHONPATH=/app/src -# Copy application code -COPY docker/ ./docker/ COPY src/ ./src/ -# Set Python import path -ENV PYTHONPATH=/app/src - -# Expose port EXPOSE 8000 -# Start the docker -CMD ["uvicorn", "memos.api.product_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] +CMD ["uvicorn", "memos.api.server_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file From 063e2a4bb611a0cf3d6cc1de28229331cf0a9534 Mon Sep 17 00:00:00 2001 From: pursues <15180521816@163.com> Date: Wed, 24 Dec 2025 20:14:24 +0800 Subject: [PATCH 18/21] update requirements --- docker/requirements-full.txt | 186 ++++++++++++++++++++++++++++ docker/requirements.txt | 234 +++++++++++++++-------------------- 2 files changed, 283 insertions(+), 137 deletions(-) create mode 100644 docker/requirements-full.txt diff --git a/docker/requirements-full.txt b/docker/requirements-full.txt new file mode 100644 index 000000000..538f5e578 --- /dev/null +++ b/docker/requirements-full.txt @@ -0,0 +1,186 @@ +# Generated from poetry.lock - Main dependencies +# This file contains all transitive dependencies for the production build. + +annotated-types==0.7.0 +anyio==4.9.0 +async-timeout==5.0.1 +attrs==25.3.0 +authlib==1.6.0 +beautifulsoup4==4.13.4 +cachetools==6.2.1 +certifi==2025.7.14 +cffi==1.17.1 +charset-normalizer==3.4.2 +chonkie==1.1.1 +click==8.2.1 +cobble==0.1.4 +colorama==0.4.6 +coloredlogs==15.0.1 +concurrent-log-handler==0.9.28 +cryptography==45.0.5 +cyclopts==3.22.2 +datasketch==1.6.5 +defusedxml==0.7.1 +distro==1.9.0 +dnspython==2.7.0 +docstring-parser==0.16 +docutils==0.21.2 +email-validator==2.2.0 +et-xmlfile==2.0.0 +exceptiongroup==1.3.0 +fastapi==0.115.14 +fastapi-cli==0.0.8 +fastapi-cloud-cli==0.1.4 +fastmcp==2.10.5 +filelock==3.18.0 +flatbuffers==25.2.10 +fsspec==2025.7.0 +greenlet==3.2.3 +grpcio==1.73.1 +h11==0.16.0 +h2==4.2.0 +hf-xet==1.1.5 +hpack==4.1.0 +httpcore==1.0.9 +httptools==0.6.4 +httpx==0.28.1 +httpx-sse==0.4.1 +huggingface-hub==0.33.4 +humanfriendly==10.0 +hyperframe==6.1.0 +idna==3.10 +itsdangerous==2.2.0 +jieba==0.42 +jinja2==3.1.6 +jiter==0.10.0 +joblib==1.5.1 +jsonpatch==1.33 +jsonpointer==3.0.0 +jsonschema==4.24.1 +jsonschema-specifications==2025.4.1 +langchain-core==1.1.0 +langchain-text-splitters==1.0.0 +langsmith==0.4.7 +lxml==6.0.0 +magika==0.6.2 +mammoth==1.9.1 +markdown-it-py==3.0.0 +markdownify==1.1.0 +markitdown==0.1.2 +markupsafe==3.0.2 +mcp==1.12.0 +mdurl==0.1.2 +mpmath==1.3.0 +neo4j==5.28.1 +networkx==3.5 +nltk==3.9.1 +numpy==2.3.1 +nvidia-cublas-cu12==12.6.4.1 +nvidia-cuda-cupti-cu12==12.6.80 +nvidia-cuda-nvrtc-cu12==12.6.77 +nvidia-cuda-runtime-cu12==12.6.77 +nvidia-cudnn-cu12==9.5.1.17 +nvidia-cufft-cu12==11.3.0.4 +nvidia-cufile-cu12==1.11.1.6 +nvidia-curand-cu12==10.3.7.77 +nvidia-cusolver-cu12==11.7.1.2 +nvidia-cusparse-cu12==12.5.4.2 +nvidia-cusparselt-cu12==0.6.3 +nvidia-nccl-cu12==2.26.2 +nvidia-nvjitlink-cu12==12.6.85 +nvidia-nvtx-cu12==12.6.77 +ollama==0.4.9 +onnxruntime==1.22.1 +openai==1.97.0 +openapi-pydantic==0.5.1 +openpyxl==3.1.5 +orjson==3.11.0 +packaging==25.0 +pandas==2.3.1 +pdfminer-six==20250506 +pika==1.3.2 +pillow==11.3.0 +portalocker==2.10.1 +prometheus-client==0.23.1 +protobuf==6.31.1 +pycparser==2.22 +pydantic==2.11.7 +pydantic-core==2.33.2 +pydantic-extra-types==2.10.5 +pydantic-settings==2.10.1 +pygments==2.19.2 +pymilvus==2.6.2 +pymysql==1.1.2 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-multipart==0.0.20 +python-pptx==1.0.2 +pytz==2025.2 +pyyaml==6.0.2 +qdrant-client==1.14.3 +rake-nltk==1.0.6 +rank-bm25==0.2.2 +redis==6.2.0 +referencing==0.36.2 +regex==2024.11.6 +requests==2.32.4 +requests-toolbelt==1.0.0 +rich==14.0.0 +rich-rst==1.3.1 +rich-toolkit==0.14.8 +rignore==0.6.2 +rpds-py==0.26.0 +safetensors==0.5.3 +schedule==1.2.2 +scikit-learn==1.7.0 +scipy==1.16.0 +sentence-transformers==4.1.0 +sentry-sdk==2.33.0 +setuptools==80.9.0 +shellingham==1.5.4 +six==1.17.0 +sniffio==1.3.1 +soupsieve==2.7 +sqlalchemy==2.0.41 +sse-starlette==2.4.1 +starlette==0.46.2 +sympy==1.14.0 +tenacity==9.1.2 +threadpoolctl==3.6.0 +tokenizers==0.21.2 +torch +tqdm==4.67.1 +transformers==4.53.2 +triton==3.5.0 +typer==0.16.0 +typing-extensions +typing-inspection==0.4.1 +tzdata==2025.2 +ujson==5.10.0 +urllib3==2.5.0 +uvicorn==0.35.0 +uvloop==0.21.0 +volcengine-python-sdk==4.0.6 +watchfiles==1.1.0 +websockets==15.0.1 +xlrd==2.0.2 +xlsxwriter==3.2.5 +zstandard==0.23.0 +prometheus_client==0.23.1 +beartype==0.22.5 +diskcache==5.6.3 +iniconfig==2.3.0 +jaraco.classes==3.4.0 +jaraco.context==6.0.1 +jaraco.functools==4.3.0 +keyring==25.6.0 +more-itertools==10.8.0 +pathable==0.4.4 +pathvalidate==3.3.1 +platformdirs==4.5.0 +pluggy==1.6.0 +psycopg2-binary==2.9.9 +py-key-value-aio==0.2.8 +py-key-value-shared==0.2.8 +PyJWT==2.10.1 +pytest==9.0.2 \ No newline at end of file diff --git a/docker/requirements.txt b/docker/requirements.txt index f522dd3b6..d851c48da 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -1,164 +1,124 @@ -# Docker optimized requirements - Core dependencies only -# Excludes Windows-specific and heavy GPU packages for faster builds - annotated-types==0.7.0 -anyio==4.9.0 -async-timeout==5.0.1 -attrs==25.3.0 -authlib==1.6.0 -beautifulsoup4==4.13.4 -certifi==2025.7.14 -cffi==1.17.1 -charset-normalizer==3.4.2 -chonkie==1.1.1 -click==8.2.1 -cobble==0.1.4 -colorama==0.4.6 -coloredlogs==15.0.1 -cryptography==45.0.5 -cyclopts==3.22.2 -defusedxml==0.7.1 +anyio==4.11.0 +attrs==25.4.0 +Authlib==1.6.5 +beartype==0.22.5 +cachetools==6.2.2 +certifi==2025.11.12 +cffi==2.0.0 +charset-normalizer==3.4.4 +chonkie==1.1.0 +click==8.3.0 +concurrent-log-handler==0.9.28 +cryptography==46.0.3 +cyclopts==4.2.3 +diskcache==5.6.3 distro==1.9.0 -dnspython==2.7.0 -docstring-parser==0.16 -docutils==0.21.2 -email-validator==2.2.0 -et-xmlfile==2.0.0 +dnspython==2.8.0 +docstring_parser==0.17.0 +docutils==0.22.3 +email-validator==2.3.0 exceptiongroup==1.3.0 -fastapi-cli==0.0.8 -fastapi-cloud-cli==0.1.4 fastapi==0.115.14 -fastmcp==2.10.5 -filelock==3.18.0 -flatbuffers==25.2.10 -fsspec==2025.7.0 -greenlet==3.2.3 -grpcio==1.73.1 +fastapi-cli==0.0.16 +fastapi-cloud-cli==0.3.1 +fastmcp==2.13.0.2 +filelock==3.20.0 +fsspec==2025.10.0 +grpcio==1.76.0 h11==0.16.0 -h2==4.2.0 -hf-xet==1.1.5 -hpack==4.1.0 +hf-xet==1.2.0 httpcore==1.0.9 -httptools==0.6.4 -httpx-sse==0.4.1 +httptools==0.7.1 httpx==0.28.1 -huggingface-hub==0.33.4 -humanfriendly==10.0 -hyperframe==6.1.0 -idna==3.10 +httpx-sse==0.4.3 +huggingface-hub==0.36.0 +idna==3.11 +iniconfig==2.3.0 itsdangerous==2.2.0 -jinja2==3.1.6 -jiter==0.10.0 -joblib==1.5.1 -jsonschema-specifications==2025.4.1 -jsonschema==4.24.1 -lxml==6.0.0 -magika==0.6.2 -mammoth==1.9.1 -markdown-it-py==3.0.0 -markdownify==1.1.0 -markitdown==0.1.2 -markupsafe==3.0.2 -mcp==1.12.0 +jaraco.classes==3.4.0 +jaraco.context==6.0.1 +jaraco.functools==4.3.0 +jieba==0.42 +Jinja2==3.1.6 +jiter==0.12.0 +joblib==1.5.2 +jsonschema==4.25.1 +jsonschema-path==0.3.4 +jsonschema-specifications==2025.9.1 +keyring==25.6.0 +markdown-it-py==4.0.0 +MarkupSafe==3.0.3 +mcp==1.21.1 mdurl==0.1.2 -mpmath==1.3.0 -neo4j==5.28.1 -networkx==3.5 -numpy==2.3.1 -# NVIDIA CUDA packages excluded for lighter Docker images -# If GPU support is needed, uncomment relevant packages below: -# nvidia-cublas-cu12==12.6.4.1 -# nvidia-cuda-cupti-cu12==12.6.80 -# nvidia-cuda-nvrtc-cu12==12.6.77 -# nvidia-cuda-runtime-cu12==12.6.77 -# nvidia-cudnn-cu12==9.5.1.17 -# nvidia-cufft-cu12==11.3.0.4 -# nvidia-cufile-cu12==1.11.1.6 -# nvidia-curand-cu12==10.3.7.77 -# nvidia-cusolver-cu12==11.7.1.2 -# nvidia-cusparse-cu12==12.5.4.2 -# nvidia-cusparselt-cu12==0.6.3 -# nvidia-nccl-cu12==2.26.2 -# nvidia-nvjitlink-cu12==12.6.85 -# nvidia-nvtx-cu12==12.6.77 +more-itertools==10.8.0 +numpy==2.3.4 ollama==0.4.9 -onnxruntime==1.22.1 -openai==1.97.0 +openai==1.109.1 openapi-pydantic==0.5.1 -openpyxl==3.1.5 -orjson==3.11.0 +orjson==3.11.4 packaging==25.0 -pandas==2.3.1 -pdfminer-six==20250506 +pandas==2.3.3 +pathable==0.4.4 +pathvalidate==3.3.1 pika==1.3.2 -pillow==11.3.0 -portalocker==2.10.1 -protobuf==6.31.1 -pycparser==2.22 -pydantic-core==2.33.2 -pydantic-extra-types==2.10.5 -pydantic-settings==2.10.1 -pydantic==2.11.7 -pygments==2.19.2 -pymysql==1.1.1 -pyperclip==1.9.0 -# Windows-specific packages excluded: -# pyreadline3==3.5.4 # Windows only -# pywin32==311 # Windows only +platformdirs==4.5.0 +pluggy==1.6.0 +portalocker==3.2.0 +prometheus_client==0.23.1 +protobuf==6.33.1 +psycopg2-binary==2.9.9 +py-key-value-aio==0.2.8 +py-key-value-shared==0.2.8 +pycparser==2.23 +pydantic==2.12.4 +pydantic-extra-types==2.10.6 +pydantic-settings==2.12.0 +pydantic_core==2.41.5 +Pygments==2.19.2 +PyJWT==2.10.1 +pymilvus==2.6.5 +PyMySQL==1.1.2 +pyperclip==1.11.0 +pytest==9.0.2 python-dateutil==2.9.0.post0 -python-dotenv==1.1.1 +python-dotenv==1.2.1 python-multipart==0.0.20 -python-pptx==1.0.2 pytz==2025.2 -pyyaml==6.0.2 -qdrant-client==1.14.3 -redis==6.2.0 +PyYAML==6.0.3 +qdrant-client +redis==6.4.0 referencing==0.36.2 -regex==2024.11.6 -requests==2.32.4 -rich-rst==1.3.1 -rich-toolkit==0.14.8 -rich==14.0.0 -rignore==0.6.2 -rpds-py==0.26.0 -safetensors==0.5.3 -schedule==1.2.2 -scikit-learn==1.7.0 -scipy==1.16.0 -sentence-transformers==4.1.0 -sentry-sdk==2.33.0 +regex==2025.11.3 +requests==2.32.5 +rich==14.2.0 +rich-rst==1.3.2 +rich-toolkit==0.15.1 +rignore==0.7.6 +rpds-py==0.28.0 +safetensors==0.6.2 +scikit-learn==1.7.2 +scipy==1.16.3 +sentry-sdk==2.44.0 setuptools==80.9.0 shellingham==1.5.4 six==1.17.0 sniffio==1.3.1 -soupsieve==2.7 -sqlalchemy==2.0.41 -sse-starlette==2.4.1 +SQLAlchemy==2.0.44 +sse-starlette==3.0.3 starlette==0.46.2 -sympy==1.14.0 tenacity==9.1.2 threadpoolctl==3.6.0 -tokenizers==0.21.2 -# Torch excluded for lighter Docker images (very large package ~2GB) -# If needed for ML/AI features, uncomment: -# torch==2.7.1 -# triton==3.3.1 +tokenizers==0.22.1 tqdm==4.67.1 -transformers==4.53.2 -typer==0.16.0 -typing-extensions==4.14.1 -typing-inspection==0.4.1 +transformers==4.57.1 +typer==0.20.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 tzdata==2025.2 -ujson==5.10.0 +ujson==5.11.0 urllib3==2.5.0 -uvicorn==0.35.0 -uvloop==0.21.0 -volcengine-python-sdk==4.0.6 -watchfiles==1.1.0 -websockets==15.0.1 -xlrd==2.0.2 -xlsxwriter==3.2.5 -prometheus-client==0.23.1 -pymilvus==2.5.12 -nltk==3.9.1 -rake-nltk==1.0.6 +uvicorn==0.38.0 +uvloop==0.22.1 +watchfiles==1.1.1 +websockets==15.0.1 \ No newline at end of file From 1cfb82ad6bd8a721abc153472ab054c113345806 Mon Sep 17 00:00:00 2001 From: pursues <15180521816@163.com> Date: Wed, 24 Dec 2025 20:21:52 +0800 Subject: [PATCH 19/21] back Dockerfile --- docker/Dockerfile | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 68596c383..6b04dd095 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,17 +1,35 @@ -# Simplified package url -FROM registry.cn-shanghai.aliyuncs.com/memtensor/memos:base-v1.0 +# Base image +FROM python:3.11-slim -# Full package url -# FROM registry.cn-shanghai.aliyuncs.com/memtensor/memos-full-base:v1.0.0 +# Install dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + build-essential \ + libffi-dev \ + python3-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* +# Set working directory WORKDIR /app +# Set Hugging Face mirror ENV HF_ENDPOINT=https://hf-mirror.com -ENV PYTHONPATH=/app/src +# Install Python packages +COPY docker/requirements.txt . +RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt +# Copy application code +COPY docker/ ./docker/ COPY src/ ./src/ +# Set Python import path +ENV PYTHONPATH=/app/src + +# Expose port EXPOSE 8000 -CMD ["uvicorn", "memos.api.server_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +# Start the docker +CMD ["uvicorn", "memos.api.product_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file From c71c4d634abdb226910faa052d36fd7ec7eb2b9f Mon Sep 17 00:00:00 2001 From: pursues <15180521816@163.com> Date: Wed, 24 Dec 2025 20:33:38 +0800 Subject: [PATCH 20/21] upadte Dockerfile --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 6b04dd095..13fb477d9 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,4 +32,4 @@ ENV PYTHONPATH=/app/src EXPOSE 8000 # Start the docker -CMD ["uvicorn", "memos.api.product_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +CMD ["uvicorn", "memos.api.server_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file From 658fcce3b226c8c698ff1c6a5a48d80fbccb89a8 Mon Sep 17 00:00:00 2001 From: pursues <15180521816@163.com> Date: Wed, 24 Dec 2025 20:52:00 +0800 Subject: [PATCH 21/21] add --- docker/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/requirements.txt b/docker/requirements.txt index 12035e26f..738a53920 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -29,6 +29,7 @@ fastmcp==2.13.0.2 filelock==3.20.0 fsspec==2025.10.0 grpcio==1.76.0 +neo4j==5.28.1 h11==0.16.0 hf-xet==1.2.0 httpcore==1.0.9