diff --git a/.gitignore b/.gitignore index 3243c0a5..26d77194 100644 --- a/.gitignore +++ b/.gitignore @@ -1016,3 +1016,4 @@ pgdata/ ## pytest mirrors memgpt/.pytest_cache/ memgpy/pytest.ini +**/**/pytest_cache diff --git a/Dockerfile b/Dockerfile index 7022a1d2..006e1b5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # The builder image, used to build the virtual environment -FROM python:3.12-bookworm as builder +FROM python:3.12.2-bookworm as builder ARG MEMGPT_ENVIRONMENT=PRODUCTION ENV MEMGPT_ENVIRONMENT=${MEMGPT_ENVIRONMENT} RUN pip install poetry==1.8.2 @@ -16,13 +16,13 @@ RUN poetry lock --no-update RUN if [ "$MEMGPT_ENVIRONMENT" = "DEVELOPMENT" ] ; then \ poetry install --no-root -E "postgres server dev autogen" ; \ else \ - poetry install --without dev --without local --no-root -E "postgres server" && \ + poetry install --no-root -E "postgres server" && \ rm -rf $POETRY_CACHE_DIR ; \ fi # The runtime image, used to just run the code provided its virtual environment -FROM python:3.12-slim-bookworm as runtime +FROM python:3.12.2-slim-bookworm as runtime ARG MEMGPT_ENVIRONMENT=PRODUCTION ENV MEMGPT_ENVIRONMENT=${MEMGPT_ENVIRONMENT} ENV VIRTUAL_ENV=/app/.venv \ diff --git a/dev-compose.yaml b/dev-compose.yaml index 32b66817..7718b262 100644 --- a/dev-compose.yaml +++ b/dev-compose.yaml @@ -22,6 +22,7 @@ services: build: context: . dockerfile: Dockerfile + target: runtime depends_on: - memgpt_db ports: diff --git a/development.compose.yml b/development.compose.yml index 4b576e38..69325379 100644 --- a/development.compose.yml +++ b/development.compose.yml @@ -14,6 +14,8 @@ services: - .env environment: - MEMGPT_SERVER_PASS=test_server_token + - WATCHFILES_FORCE_POLLING=true + volumes: - ./memgpt:/memgpt - ~/.memgpt/credentials:/root/.memgpt/credentials diff --git a/memgpt/cli/cli.py b/memgpt/cli/cli.py index 12ac3a70..6191049b 100644 --- a/memgpt/cli/cli.py +++ b/memgpt/cli/cli.py @@ -19,7 +19,7 @@ from memgpt.config import MemGPTConfig from memgpt.constants import CLI_WARNING_PREFIX, MEMGPT_DIR from memgpt.credentials import MemGPTCredentials from memgpt.data_types import EmbeddingConfig, LLMConfig, User -from memgpt.log import logger +from memgpt.log import get_logger from memgpt.metadata import MetadataStore from memgpt.migrate import migrate_all_agents, migrate_all_sources from memgpt.server.constants import WS_DEFAULT_PORT @@ -30,6 +30,8 @@ from memgpt.streaming_interface import ( ) from memgpt.utils import open_folder_in_explorer, printd +logger = get_logger(__name__) + def migrate( debug: Annotated[bool, typer.Option(help="Print extra tracebacks for failed migrations")] = False, diff --git a/memgpt/config.py b/memgpt/config.py index 4d0bb971..a6fc4f7e 100644 --- a/memgpt/config.py +++ b/memgpt/config.py @@ -9,7 +9,9 @@ import memgpt import memgpt.utils as utils from memgpt.constants import DEFAULT_HUMAN, DEFAULT_PERSONA, DEFAULT_PRESET, MEMGPT_DIR from memgpt.data_types import AgentState, EmbeddingConfig, LLMConfig -from memgpt.log import logger +from memgpt.log import get_logger + +logger = get_logger(__name__) # helper functions for writing to configs diff --git a/memgpt/constants.py b/memgpt/constants.py index 68ad7c5d..e6ece4ac 100644 --- a/memgpt/constants.py +++ b/memgpt/constants.py @@ -21,17 +21,6 @@ DEFAULT_PERSONA = "sam_pov" DEFAULT_HUMAN = "basic" DEFAULT_PRESET = "memgpt_chat" -# Used to isolate MemGPT logger instance from Dependant Libraries logging -LOGGER_NAME = "MemGPT" -LOGGER_DEFAULT_LEVEL = CRITICAL -# Where to store the logs -LOGGER_DIR = os.path.join(MEMGPT_DIR, "logs") -# filename of the log -LOGGER_FILENAME = "MemGPT.log" -# Number of log files to rotate -LOGGER_FILE_BACKUP_COUNT = 3 -# Max Log file size in bytes -LOGGER_MAX_FILE_SIZE = 10485760 # LOGGER_LOG_LEVEL is use to convert Text to Logging level value for logging mostly for Cli input to setting level LOGGER_LOG_LEVELS = {"CRITICAL": CRITICAL, "ERROR": ERROR, "WARN": WARN, "WARNING": WARNING, "INFO": INFO, "DEBUG": DEBUG, "NOTSET": NOTSET} diff --git a/memgpt/log.py b/memgpt/log.py index 6c2437c2..3b179cab 100644 --- a/memgpt/log.py +++ b/memgpt/log.py @@ -1,43 +1,77 @@ import logging -import os -import os.path +from logging.config import dictConfig from logging.handlers import RotatingFileHandler +from pathlib import Path +from sys import stdout +from typing import Optional -from memgpt.constants import ( - LOGGER_DEFAULT_LEVEL, - LOGGER_DIR, - LOGGER_FILE_BACKUP_COUNT, - LOGGER_FILENAME, - LOGGER_MAX_FILE_SIZE, - LOGGER_NAME, -) +from memgpt.settings import settings -# Checking if log directory exists -if not os.path.exists(LOGGER_DIR): - os.makedirs(LOGGER_DIR, exist_ok=True) +selected_log_level = logging.DEBUG if settings.debug else logging.INFO -# Create logger for MemGPT -logger = logging.getLogger(LOGGER_NAME) -logger.setLevel(LOGGER_DEFAULT_LEVEL) -# create console handler and set level to debug -console_handler = logging.StreamHandler() +def _setup_logfile() -> "Path": + """ensure the logger filepath is in place -# create rotatating file handler -file_handler = RotatingFileHandler( - os.path.join(LOGGER_DIR, LOGGER_FILENAME), maxBytes=LOGGER_MAX_FILE_SIZE, backupCount=LOGGER_FILE_BACKUP_COUNT -) + Returns: the logfile Path + """ + logfile = Path(settings.memgpt_dir / "logs" / "MemGPT.log") + logfile.parent.mkdir(parents=True, exist_ok=True) + logfile.touch(exist_ok=True) + return logfile -# create formatters -console_formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s") # not datetime -file_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") -# add formatter to console handler -console_handler.setFormatter(console_formatter) +# TODO: production logging should be much less invasive +DEVELOPMENT_LOGGING = { + "version": 1, + "disable_existing_loggers": True, + "formatters": { + "standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}, + "no_datetime": { + "format": "%(name)s - %(levelname)s - %(message)s", + }, + }, + "handlers": { + "console": { + "level": selected_log_level, + "class": "logging.StreamHandler", + "stream": stdout, + "formatter": "no_datetime", + }, + "file": { + "level": "DEBUG", + "class": "logging.handlers.RotatingFileHandler", + "filename": _setup_logfile(), + "maxBytes": 1024**2 * 10, + "backupCount": 3, + "formatter": "standard", + }, + }, + "loggers": { + "MemGPT": { + "level": logging.DEBUG if settings.debug else logging.INFO, + "handlers": [ + "console", + "file", + ], + "propagate": False, + }, + "uvicorn": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, + }, + }, +} -# add formatter for file handler -file_handler.setFormatter(file_formatter) -# add ch to logger -logger.addHandler(console_handler) -logger.addHandler(file_handler) +def get_logger(name: Optional[str] = None) -> "logging.Logger": + """returns the project logger, scoped to a child name if provided + Args: + name: will define a child logger + """ + dictConfig(DEVELOPMENT_LOGGING) + parent_logger = logging.getLogger("MemGPT") + if name: + return parent_logger.getChild(name) + return parent_logger diff --git a/memgpt/metadata.py b/memgpt/metadata.py index 673863af..ddf226a6 100644 --- a/memgpt/metadata.py +++ b/memgpt/metadata.py @@ -418,7 +418,7 @@ class MetadataStore: """Get the user associated with a given API key""" token = self.get_api_key(api_key=api_key) if token is None: - raise ValueError(f"Token {api_key} does not exist") + raise ValueError(f"Provided token does not exist") else: return self.get_user(user_id=token.user_id) diff --git a/memgpt/server/rest_api/auth/index.py b/memgpt/server/rest_api/auth/index.py index 065f4ee1..790cd311 100644 --- a/memgpt/server/rest_api/auth/index.py +++ b/memgpt/server/rest_api/auth/index.py @@ -3,9 +3,11 @@ from uuid import UUID from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field +from memgpt.log import get_logger from memgpt.server.rest_api.interface import QueuingInterface from memgpt.server.server import SyncServer +logger = get_logger(__name__) router = APIRouter() @@ -18,6 +20,7 @@ class AuthRequest(BaseModel): def setup_auth_router(server: SyncServer, interface: QueuingInterface, password: str) -> APIRouter: + @router.post("/auth", tags=["auth"], response_model=AuthResponse) def authenticate_user(request: AuthRequest) -> AuthResponse: """ @@ -26,16 +29,10 @@ def setup_auth_router(server: SyncServer, interface: QueuingInterface, password: Currently, this is a placeholder that simply returns a UUID placeholder """ interface.clear() - try: - if request.password != password: - # raise HTTPException(status_code=400, detail="Incorrect credentials") - response = server.api_key_to_user(api_key=request.password) - else: - response = server.authenticate_user() - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"{e}") + if request.password != password: + response = server.api_key_to_user(api_key=request.password) + else: + response = server.authenticate_user() return AuthResponse(uuid=response) return router diff --git a/memgpt/server/server.py b/memgpt/server/server.py index 2f3db3bb..40c0f39c 100644 --- a/memgpt/server/server.py +++ b/memgpt/server/server.py @@ -1,5 +1,4 @@ import json -import logging import uuid import warnings from abc import abstractmethod @@ -37,6 +36,7 @@ from memgpt.data_types import ( # TODO use custom interface from memgpt.interface import AgentInterface # abstract from memgpt.interface import CLIInterface # for printing to terminal +from memgpt.log import get_logger from memgpt.metadata import MetadataStore from memgpt.models.pydantic_models import ( DocumentModel, @@ -47,7 +47,7 @@ from memgpt.models.pydantic_models import ( ToolModel, ) -logger = logging.getLogger(__name__) +logger = get_logger(__name__) class Server(object): diff --git a/memgpt/server/startup.sh b/memgpt/server/startup.sh index 3d4822a6..f91c669a 100755 --- a/memgpt/server/startup.sh +++ b/memgpt/server/startup.sh @@ -1,6 +1,7 @@ #!/bin/sh echo "Starting MEMGPT server..." if [ "$MEMGPT_ENVIRONMENT" = "DEVELOPMENT" ] ; then + echo "Starting in development mode!" uvicorn memgpt.server.rest_api.server:app --reload --reload-dir /memgpt --host 0.0.0.0 --port 8083 else uvicorn memgpt.server.rest_api.server:app --host 0.0.0.0 --port 8083 diff --git a/memgpt/settings.py b/memgpt/settings.py index 1c982433..bd7ce616 100644 --- a/memgpt/settings.py +++ b/memgpt/settings.py @@ -1,11 +1,15 @@ +from pathlib import Path from typing import Optional +from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="memgpt_") + memgpt_dir: Optional[Path] = Field(Path.home() / ".memgpt", env="MEMGPT_DIR") + debug: Optional[bool] = False server_pass: Optional[str] = None pg_db: Optional[str] = None pg_user: Optional[str] = None diff --git a/tests/test_client.py b/tests/test_client.py index a3e653cf..68b4c3b3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,7 +29,6 @@ test_server_token = "test_server_token" def _reset_config(): - # Use os.getenv with a fallback to os.environ.get db_url = settings.memgpt_pg_uri @@ -51,14 +50,12 @@ def _reset_config(): config.archival_storage_type = "postgres" config.recall_storage_type = "postgres" config.metadata_storage_type = "postgres" - config.save() credentials.save() print("_reset_config :: ", config.config_path) def run_server(): - load_dotenv() _reset_config() diff --git a/tests/test_log.py b/tests/test_log.py deleted file mode 100644 index 6f9e2158..00000000 --- a/tests/test_log.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging - -from memgpt.constants import LOGGER_LOG_LEVELS -from memgpt.log import logger - - -def test_log_debug(): - # test setting logging level - assert logging.DEBUG == LOGGER_LOG_LEVELS["DEBUG"] - logger.setLevel(LOGGER_LOG_LEVELS["DEBUG"]) - assert logger.isEnabledFor(logging.DEBUG) - - # Assert that the message was logged - assert logger.hasHandlers() - logger.debug("This is a Debug message") - assert 1 == 1