diff --git a/README.md b/README.md index 8a97bc5..eb1baf1 100644 --- a/README.md +++ b/README.md @@ -279,3 +279,35 @@ python -m pytest 3. Commit your changes 4. Push to the branch 5. Create a Pull Request + +## Running the Background Task Worker + +The Redis Memory Server uses Docket for background task management. There are two ways to run the worker: + +### 1. Using the Docket CLI + +After installing the package, you can run the worker using the Docket CLI command: + +```bash +docket worker --tasks agent_memory_server.docket_tasks:task_collection +``` + +You can customize the concurrency and redelivery timeout: + +```bash +docket worker --tasks agent_memory_server.docket_tasks:task_collection --concurrency 5 --redelivery-timeout 60 +``` + +### 2. Using Python Code + +Alternatively, you can run the worker directly in Python: + +```bash +python -m agent_memory_server.worker +``` + +With customization options: + +```bash +python -m agent_memory_server.worker --concurrency 5 --redelivery-timeout 60 +``` diff --git a/agent_memory_server/api.py b/agent_memory_server/api.py index e8bbae9..e074a01 100644 --- a/agent_memory_server/api.py +++ b/agent_memory_server/api.py @@ -1,9 +1,10 @@ from typing import Literal -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException from agent_memory_server import long_term_memory, messages from agent_memory_server.config import settings +from agent_memory_server.dependencies import get_background_tasks from agent_memory_server.llms import get_model_config from agent_memory_server.logging import get_logger from agent_memory_server.models import ( @@ -130,7 +131,7 @@ async def get_session_memory( async def put_session_memory( session_id: str, memory: SessionMemory, - background_tasks: BackgroundTasks, + background_tasks=Depends(get_background_tasks), ): """ Set session memory. Replaces existing session memory. @@ -138,6 +139,7 @@ async def put_session_memory( Args: session_id: The session ID memory: Messages and context to save + background_tasks: DocketBackgroundTasks instance (injected automatically) Returns: Acknowledgement response @@ -179,26 +181,25 @@ async def delete_session_memory( @router.post("/long-term-memory", response_model=AckResponse) async def create_long_term_memory( - payload: CreateLongTermMemoryPayload, background_tasks: BackgroundTasks + payload: CreateLongTermMemoryPayload, + background_tasks=Depends(get_background_tasks), ): """ Create a long-term memory Args: payload: Long-term memory payload + background_tasks: DocketBackgroundTasks instance (injected automatically) Returns: Acknowledgement response """ - redis = get_redis_conn() - if not settings.long_term_memory: raise HTTPException(status_code=400, detail="Long-term memory is disabled") - await long_term_memory.index_long_term_memories( - redis=redis, + await background_tasks.add_task( + long_term_memory.index_long_term_memories, memories=payload.memories, - background_tasks=background_tasks, ) return AckResponse(status="ok") diff --git a/agent_memory_server/config.py b/agent_memory_server/config.py index b0e0d12..3f53717 100644 --- a/agent_memory_server/config.py +++ b/agent_memory_server/config.py @@ -30,5 +30,9 @@ class Settings(BaseSettings): redisvl_index_name: str = "memory" redisvl_index_prefix: str = "memory" + # Docket settings + docket_name: str = "memory-server" + use_docket: bool = True + settings = Settings() diff --git a/agent_memory_server/dependencies.py b/agent_memory_server/dependencies.py new file mode 100644 index 0000000..d970e56 --- /dev/null +++ b/agent_memory_server/dependencies.py @@ -0,0 +1,35 @@ +from collections.abc import Callable +from typing import Any + +from fastapi import BackgroundTasks + +from agent_memory_server.config import settings + + +class DocketBackgroundTasks(BackgroundTasks): + """A BackgroundTasks implementation that uses Docket.""" + + async def add_task( + self, func: Callable[..., Any], *args: Any, **kwargs: Any + ) -> None: + """Run tasks either directly or through Docket""" + from docket import Docket + + if settings.use_docket: + async with Docket( + name=settings.docket_name, + url=settings.redis_url, + ) as docket: + # Schedule task through Docket + await docket.add(func)(*args, **kwargs) + else: + await func(*args, **kwargs) + + +def get_background_tasks() -> DocketBackgroundTasks: + """ + Dependency function that returns a DocketBackgroundTasks instance. + + This is used by API endpoints to inject a consistent background tasks object. + """ + return DocketBackgroundTasks() diff --git a/agent_memory_server/docket_tasks.py b/agent_memory_server/docket_tasks.py new file mode 100644 index 0000000..a83b91e --- /dev/null +++ b/agent_memory_server/docket_tasks.py @@ -0,0 +1,43 @@ +""" +Background task management using Docket. +""" + +import logging + +from docket import Docket + +from agent_memory_server.config import settings +from agent_memory_server.long_term_memory import ( + extract_memory_structure, + index_long_term_memories, +) +from agent_memory_server.summarization import summarize_session + + +logger = logging.getLogger(__name__) + + +# Register functions in the task collection for the CLI worker +task_collection = [ + extract_memory_structure, + summarize_session, + index_long_term_memories, +] + + +async def register_tasks() -> None: + """Register all task functions with Docket.""" + if not settings.use_docket: + logger.info("Docket is disabled, skipping task registration") + return + + # Initialize Docket client + async with Docket( + name=settings.docket_name, + url=settings.redis_url, + ) as docket: + # Register all tasks + for task in task_collection: + docket.register(task) + + logger.info(f"Registered {len(task_collection)} background tasks with Docket") diff --git a/agent_memory_server/long_term_memory.py b/agent_memory_server/long_term_memory.py index b79bdf6..70b907a 100644 --- a/agent_memory_server/long_term_memory.py +++ b/agent_memory_server/long_term_memory.py @@ -3,11 +3,11 @@ from functools import reduce import nanoid -from fastapi import BackgroundTasks from redis.asyncio import Redis from redisvl.query import VectorQuery, VectorRangeQuery from redisvl.utils.vectorize import OpenAITextVectorizer +from agent_memory_server.dependencies import get_background_tasks from agent_memory_server.extraction import handle_extraction from agent_memory_server.filters import ( CreatedAt, @@ -25,6 +25,7 @@ ) from agent_memory_server.utils import ( Keys, + get_redis_conn, get_search_index, safe_get, ) @@ -33,11 +34,10 @@ logger = logging.getLogger(__name__) -async def extract_memory_structure( - redis: Redis, _id: str, text: str, namespace: str | None -): +async def extract_memory_structure(_id: str, text: str, namespace: str | None): + redis = get_redis_conn() + # Process messages for topic/entity extraction - # TODO: Move into background task. topics, entities = await handle_extraction(text) # Convert lists to comma-separated strings for TAG fields @@ -65,14 +65,13 @@ async def compact_long_term_memories(redis: Redis) -> None: async def index_long_term_memories( - redis: Redis, memories: list[LongTermMemory], - background_tasks: BackgroundTasks, ) -> None: """ Index long-term memories in Redis for search """ - + redis = get_redis_conn() + background_tasks = get_background_tasks() vectorizer = OpenAITextVectorizer() embeddings = await vectorizer.aembed_many( [memory.text for memory in memories], @@ -100,8 +99,8 @@ async def index_long_term_memories( }, ) - background_tasks.add_task( - extract_memory_structure, redis, id_, memory.text, memory.namespace + await background_tasks.add_task( + extract_memory_structure, id_, memory.text, memory.namespace ) await pipe.execute() diff --git a/agent_memory_server/main.py b/agent_memory_server/main.py index 619b2d2..2cf7997 100644 --- a/agent_memory_server/main.py +++ b/agent_memory_server/main.py @@ -1,4 +1,5 @@ import os +import sys from contextlib import asynccontextmanager import uvicorn @@ -7,6 +8,7 @@ from agent_memory_server import utils from agent_memory_server.api import router as memory_router from agent_memory_server.config import settings +from agent_memory_server.docket_tasks import register_tasks from agent_memory_server.healthcheck import router as health_router from agent_memory_server.llms import MODEL_CONFIGS, ModelProvider from agent_memory_server.logging import configure_logging, get_logger @@ -87,6 +89,20 @@ async def lifespan(app: FastAPI): logger.error(f"Failed to ensure RediSearch index: {e}") raise + # Initialize Docket for background tasks if enabled + if settings.use_docket: + try: + await register_tasks() + logger.info("Initialized Docket for background tasks") + logger.info("To run the worker, use one of these methods:") + logger.info( + "1. CLI: docket worker --tasks agent_memory_server.docket_tasks:task_collection" + ) + logger.info("2. Python: python -m agent_memory_server.worker") + except Exception as e: + logger.error(f"Failed to initialize Docket: {e}") + raise + # Show available models openai_models = [ model @@ -138,6 +154,31 @@ def on_start_logger(port: int): # Run the application if __name__ == "__main__": - port = int(os.environ.get("PORT", "8000")) + # Parse command line arguments for port + port = settings.port + + # Check if --port argument is provided + if "--port" in sys.argv: + try: + port_index = sys.argv.index("--port") + 1 + if port_index < len(sys.argv): + port = int(sys.argv[port_index]) + print(f"Using port from command line: {port}") + except (ValueError, IndexError): + # If conversion fails or index out of bounds, use default + print(f"Invalid port argument, using default: {port}") + else: + print(f"No port argument provided, using default: {port}") + + # Explicitly unset the PORT environment variable if it exists + if "PORT" in os.environ: + port_val = os.environ.pop("PORT") + print(f"Removed environment variable PORT={port_val}") + on_start_logger(port) - uvicorn.run("agent_memory_server.main:app", host="0.0.0.0", port=port, reload=False) + uvicorn.run( + app, # Using the app instance directly + host="0.0.0.0", + port=port, + reload=False, + ) diff --git a/agent_memory_server/mcp.py b/agent_memory_server/mcp.py index 6aab9d6..69e8096 100644 --- a/agent_memory_server/mcp.py +++ b/agent_memory_server/mcp.py @@ -2,7 +2,7 @@ import logging import sys -from fastapi import BackgroundTasks, HTTPException +from fastapi import HTTPException from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.prompts import base from mcp.types import TextContent @@ -13,6 +13,7 @@ search_long_term_memory as core_search_long_term_memory, ) from agent_memory_server.config import settings +from agent_memory_server.dependencies import get_background_tasks from agent_memory_server.models import ( AckResponse, CreateLongTermMemoryPayload, @@ -75,7 +76,7 @@ async def create_long_term_memories( An acknowledgement response indicating success """ return await core_create_long_term_memory( - payload, background_tasks=BackgroundTasks() + payload, background_tasks=get_background_tasks() ) diff --git a/agent_memory_server/messages.py b/agent_memory_server/messages.py index f43661d..d216d1b 100644 --- a/agent_memory_server/messages.py +++ b/agent_memory_server/messages.py @@ -2,11 +2,11 @@ import logging import time -from fastapi import BackgroundTasks from redis import WatchError from redis.asyncio import Redis from agent_memory_server.config import settings +from agent_memory_server.dependencies import DocketBackgroundTasks from agent_memory_server.long_term_memory import index_long_term_memories from agent_memory_server.models import ( LongTermMemory, @@ -87,18 +87,16 @@ async def set_session_memory( redis: Redis, session_id: str, memory: SessionMemory, - background_tasks: BackgroundTasks, + background_tasks: DocketBackgroundTasks, ): """ Create or update a session's memory - TODO: This shouldn't need BackgroundTasks. - Args: redis: The Redis client session_id: The session ID memory: The session memory to set - background_tasks: The background tasks to add the summarization task to + background_tasks: Background tasks instance """ sessions_key = Keys.sessions_key(namespace=memory.namespace) messages_key = Keys.messages_key(session_id, namespace=memory.namespace) @@ -127,32 +125,28 @@ async def set_session_memory( # Check if window size is exceeded current_size = await redis.llen(messages_key) # type: ignore if current_size > settings.window_size: - # Handle summarization in background - background_tasks.add_task( + # Add summarization task + await background_tasks.add_task( summarize_session, - redis, session_id, settings.generation_model, settings.window_size, ) # If long-term memory is enabled, index messages - # TODO: Use a distributed task queue - # TODO: Allow strategies for long-term memory: indexing - # messages vs. extracting memories from messages, etc. if settings.long_term_memory: - background_tasks.add_task( + memories = [ + LongTermMemory( + session_id=session_id, + text=f"{msg.role}: {msg.content}", + namespace=memory.namespace, + ) + for msg in memory.messages + ] + + await background_tasks.add_task( index_long_term_memories, - redis, - [ - LongTermMemory( - session_id=session_id, - text=f"{msg.role}: {msg.content}", - namespace=memory.namespace, - ) - for msg in memory.messages - ], - background_tasks, + memories, ) diff --git a/agent_memory_server/summarization.py b/agent_memory_server/summarization.py index bc93be6..11e44ad 100644 --- a/agent_memory_server/summarization.py +++ b/agent_memory_server/summarization.py @@ -3,7 +3,6 @@ import tiktoken from redis import WatchError -from redis.asyncio import Redis from agent_memory_server.config import settings from agent_memory_server.llms import ( @@ -12,7 +11,7 @@ get_model_config, ) from agent_memory_server.models import MemoryMessage -from agent_memory_server.utils import Keys, get_model_client +from agent_memory_server.utils import Keys, get_model_client, get_redis_conn logger = logging.getLogger(__name__) @@ -110,7 +109,6 @@ async def _incremental_summary( async def summarize_session( - redis: Redis, session_id: str, model: str, window_size: int, @@ -132,6 +130,7 @@ async def summarize_session( client: The client wrapper (OpenAI or Anthropic) redis_conn: Redis connection """ + redis = get_redis_conn() client = await get_model_client(settings.generation_model) messages_key = Keys.messages_key(session_id) diff --git a/agent_memory_server/worker.py b/agent_memory_server/worker.py new file mode 100644 index 0000000..7126942 --- /dev/null +++ b/agent_memory_server/worker.py @@ -0,0 +1,111 @@ +""" +Run the Docket worker directly from Python. + +This module provides a way to run the background task worker in-process +instead of using the CLI command. + +Usage: + python -m agent_memory_server.worker +""" + +import asyncio +import signal +import sys +from datetime import timedelta + +from docket import Docket, Worker + +from agent_memory_server.config import settings +from agent_memory_server.docket_tasks import task_collection +from agent_memory_server.logging import configure_logging, get_logger + + +configure_logging() +logger = get_logger(__name__) + + +async def run_worker(concurrency: int = 10, redelivery_timeout: int = 30): + """ + Run the Docket worker in Python. + + Args: + concurrency: Number of tasks to process concurrently + redelivery_timeout: Seconds to wait before redelivering a task to another worker + """ + if not settings.use_docket: + logger.error("Docket is disabled in settings. Cannot run worker.") + return None + + logger.info(f"Starting Docket worker for {settings.docket_name}") + logger.info( + f"Concurrency: {concurrency}, Redelivery timeout: {redelivery_timeout}s" + ) + + # Create a signal handler to gracefully shut down + shutdown_event = asyncio.Event() + + def handle_signal(sig, frame): + logger.info(f"Received signal {sig}, shutting down...") + shutdown_event.set() + + # Register signal handlers + signal.signal(signal.SIGINT, handle_signal) + signal.signal(signal.SIGTERM, handle_signal) + + try: + # Initialize Docket client + async with Docket( + name=settings.docket_name, + url=settings.redis_url, + ) as docket: + # Register all tasks + for task in task_collection: + docket.register(task) + + logger.info(f"Registered {len(task_collection)} tasks") + + # Create and run the worker + async with Worker( + docket, + concurrency=concurrency, + redelivery_timeout=timedelta(seconds=redelivery_timeout), + ) as worker: + # Run until shutdown is requested + await worker.run_forever() + + except Exception as e: + logger.error(f"Error running worker: {e}") + return 1 + + logger.info("Worker shut down gracefully") + return 0 + + +def main(): + """Command line entry point""" + # Parse command line arguments + concurrency = 10 + redelivery_timeout = 30 + + args = sys.argv[1:] + if "--concurrency" in args: + try: + idx = args.index("--concurrency") + concurrency = int(args[idx + 1]) + except (ValueError, IndexError): + pass + + if "--redelivery-timeout" in args: + try: + idx = args.index("--redelivery-timeout") + redelivery_timeout = int(args[idx + 1]) + except (ValueError, IndexError): + pass + + return asyncio.run( + run_worker(concurrency=concurrency, redelivery_timeout=redelivery_timeout) + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index e057cc3..cb05515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,31 +5,31 @@ build-backend = "hatchling.build" [project] name = "agent-memory-server" version = "0.2.0" -description = "A Redis-powered memory server built for AI agents and applications." +description = "A Memory Server for LLM Agents and Applications" readme = "README.md" requires-python = ">=3.12,<3.13" license = { text = "MIT" } authors = [{ name = "Andrew Brookins", email = "andrew.brookins@redis.com" }] dependencies = [ + "accelerate>=1.6.0", + "anthropic>=0.15.0", + "bertopic<0.17.0,>=0.16.4", "fastapi>=0.115.11", - "uvicorn>=0.24.0", - "redis>=5.0.1", + "mcp>=1.6.0", + "nanoid>=2.0.0", + "numba>=0.60.0", + "numpy>=2.1.0", "openai>=1.3.7", - "anthropic>=0.15.0", "pydantic>=2.5.2", - "python-dotenv>=1.0.0", - "tiktoken>=0.5.1", - "numpy>=2.1.0", "pydantic-settings>=2.8.1", - "bertopic>=0.16.4,<0.17.0", - "structlog>=25.2.0", - "transformers>=4.30.0,<=4.50.3", - "numba>=0.60.0", - "nanoid>=2.0.0", - "mcp>=1.6.0", - "sentence-transformers>=3.4.1", - "accelerate>=1.6.0", + "python-dotenv>=1.0.0", + "pydocket>=0.6.3", "redisvl>=0.5.1", + "sentence-transformers>=3.4.1", + "structlog>=25.2.0", + "tiktoken>=0.5.1", + "transformers<=4.50.3,>=4.30.0", + "uvicorn>=0.24.0", ] [tool.hatch.build.targets.wheel] diff --git a/start_worker.sh b/start_worker.sh new file mode 100755 index 0000000..dda5247 --- /dev/null +++ b/start_worker.sh @@ -0,0 +1 @@ +worker_command="docket worker --tasks agent_memory_server.docket_tasks:task_collection" diff --git a/tests/conftest.py b/tests/conftest.py index fcaabf3..cb9c58f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pytest from dotenv import load_dotenv -from fastapi import BackgroundTasks, FastAPI +from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from redis import Redis from redis.asyncio import ConnectionPool, Redis as AsyncRedis @@ -14,6 +14,7 @@ from agent_memory_server.api import router as memory_router from agent_memory_server.config import settings +from agent_memory_server.dependencies import DocketBackgroundTasks, get_background_tasks from agent_memory_server.healthcheck import router as health_router from agent_memory_server.llms import OpenAIClientWrapper from agent_memory_server.messages import ( @@ -105,7 +106,6 @@ async def session(use_test_redis_connection, async_redis_client): session_id = "test-session" await index_long_term_memories( - async_redis_client, [ LongTermMemory( session_id=session_id, @@ -118,7 +118,6 @@ async def session(use_test_redis_connection, async_redis_client): namespace="test-namespace", ), ], - background_tasks=BackgroundTasks(), ) # Add messages to session memory @@ -137,7 +136,7 @@ async def session(use_test_redis_connection, async_redis_client): tokens=150, namespace="test-namespace", ), - background_tasks=BackgroundTasks(), + background_tasks=DocketBackgroundTasks(), ) return session_id @@ -243,7 +242,10 @@ def pytest_collection_modifyitems( item.add_marker(skip_api) -MockBackgroundTasks = mock.Mock(name="BackgroundTasks", spec=BackgroundTasks) +@pytest.fixture() +def mock_background_tasks(): + """Create a mock DocketBackgroundTasks instance""" + return mock.Mock(name="DocketBackgroundTasks", spec=DocketBackgroundTasks) @pytest.fixture() @@ -259,7 +261,7 @@ def app(use_test_redis_connection): @pytest.fixture() -def app_with_mock_background_tasks(use_test_redis_connection): +def app_with_mock_background_tasks(use_test_redis_connection, mock_background_tasks): """Create a test FastAPI app with routers""" app = FastAPI() @@ -267,8 +269,7 @@ def app_with_mock_background_tasks(use_test_redis_connection): app.include_router(health_router) app.include_router(memory_router) - mock_background_tasks = MockBackgroundTasks() - app.dependency_overrides[BackgroundTasks] = lambda: mock_background_tasks + app.dependency_overrides[get_background_tasks] = lambda: mock_background_tasks return app diff --git a/tests/test_api.py b/tests/test_api.py index f3646f2..020907a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -123,8 +123,11 @@ async def test_put_memory(self, client): @pytest.mark.requires_api_keys @pytest.mark.asyncio - async def test_put_memory_stores_messages_in_long_term_memory(self, client): + async def test_put_memory_stores_messages_in_long_term_memory( + self, client_with_mock_background_tasks, mock_background_tasks + ): """Test the put_memory endpoint""" + client = client_with_mock_background_tasks payload = { "messages": [ {"role": "user", "content": "Hello"}, @@ -133,12 +136,8 @@ async def test_put_memory_stores_messages_in_long_term_memory(self, client): "context": "Previous context", } mock_settings = Settings(long_term_memory=True) - mock_add_task = MagicMock() - with ( - patch("agent_memory_server.api.settings", mock_settings), - patch("agent_memory_server.api.BackgroundTasks.add_task", mock_add_task), - ): + with patch("agent_memory_server.api.settings", mock_settings): response = await client.put("/sessions/test-session/memory", json=payload) assert response.status_code == 200 @@ -148,15 +147,21 @@ async def test_put_memory_stores_messages_in_long_term_memory(self, client): assert data["status"] == "ok" # Check that background tasks were called - assert mock_add_task.call_count == 1 + assert mock_background_tasks.add_task.call_count == 1 # Check that the last call was for long-term memory indexing - assert mock_add_task.call_args_list[-1][0][0] == index_long_term_memories + assert ( + mock_background_tasks.add_task.call_args_list[-1][0][0] + == index_long_term_memories + ) @pytest.mark.requires_api_keys @pytest.mark.asyncio - async def test_post_memory_compacts_long_conversation(self, client): + async def test_post_memory_compacts_long_conversation( + self, client_with_mock_background_tasks, mock_background_tasks + ): """Test the post_memory endpoint""" + client = client_with_mock_background_tasks payload = { "messages": [ {"role": "user", "content": "Hello"}, @@ -165,12 +170,9 @@ async def test_post_memory_compacts_long_conversation(self, client): "context": "Previous context", } mock_settings = Settings(window_size=1, long_term_memory=False) - mock_add_task = MagicMock() + MagicMock() - with ( - patch("agent_memory_server.api.messages.settings", mock_settings), - patch("agent_memory_server.api.BackgroundTasks.add_task", mock_add_task), - ): + with patch("agent_memory_server.api.messages.settings", mock_settings): response = await client.put("/sessions/test-session/memory", json=payload) assert response.status_code == 200 @@ -180,10 +182,12 @@ async def test_post_memory_compacts_long_conversation(self, client): assert data["status"] == "ok" # Check that background tasks were called - assert mock_add_task.call_count == 1 + assert mock_background_tasks.add_task.call_count == 1 # Check that the last call was for compaction - assert mock_add_task.call_args_list[-1][0][0] == summarize_session + assert ( + mock_background_tasks.add_task.call_args_list[-1][0][0] == summarize_session + ) @pytest.mark.asyncio async def test_delete_memory(self, client, session): diff --git a/tests/test_long_term_memory.py b/tests/test_long_term_memory.py index a9717c1..875ffc2 100644 --- a/tests/test_long_term_memory.py +++ b/tests/test_long_term_memory.py @@ -5,7 +5,6 @@ import nanoid import numpy as np import pytest -from fastapi import BackgroundTasks from redis.commands.search.document import Document from agent_memory_server.filters import SessionId @@ -27,12 +26,14 @@ async def test_index_memories( LongTermMemory(text="France is a country in Europe", session_id=session), ] - mock_vector = np.array( - [[0.1, 0.2, 0.3, 0.4], [0.1, 0.2, 0.3, 0.4]], dtype=np.float32 - ) + # Create two separate embedding vectors + mock_vectors = [ + np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32).tobytes(), + np.array([0.5, 0.6, 0.7, 0.8], dtype=np.float32).tobytes(), + ] mock_vectorizer = MagicMock() - mock_vectorizer.aembed_many = AsyncMock(return_value=mock_vector) + mock_vectorizer.aembed_many = AsyncMock(return_value=mock_vectors) mock_async_redis_client.hset = AsyncMock() @@ -41,9 +42,7 @@ async def test_index_memories( return_value=mock_vectorizer, ): await index_long_term_memories( - mock_async_redis_client, long_term_memories, - background_tasks=BackgroundTasks(), ) # Check that create_embedding was called with the right arguments @@ -55,7 +54,7 @@ async def test_index_memories( ) # Verify one of the calls to make sure the data is correct - for call in mock_async_redis_client.hset.call_args_list: + for i, call in enumerate(mock_async_redis_client.hset.call_args_list): args, kwargs = call # Check that the key starts with the memory key prefix @@ -64,13 +63,13 @@ async def test_index_memories( # Check that the mapping contains the right keys mapping = kwargs["mapping"] assert mapping == { - "text": long_term_memories[0].text, - "id_": long_term_memories[0].id_, - "session_id": long_term_memories[0].session_id, - "user_id": long_term_memories[0].user_id, - "last_accessed": long_term_memories[0].last_accessed, - "created_at": long_term_memories[0].created_at, - "vector": mock_vector.tobytes(), + "text": long_term_memories[i].text, + "id_": long_term_memories[i].id_, + "session_id": long_term_memories[i].session_id, + "user_id": long_term_memories[i].user_id, + "last_accessed": long_term_memories[i].last_accessed, + "created_at": long_term_memories[i].created_at, + "vector": mock_vectors[i], } @pytest.mark.asyncio @@ -168,11 +167,13 @@ async def test_search_messages(self, async_redis_client): LongTermMemory(text="France is a country in Europe", session_id="123"), ] - await index_long_term_memories( - async_redis_client, - long_term_memories, - background_tasks=BackgroundTasks(), - ) + with mock.patch( + "agent_memory_server.long_term_memory.get_redis_conn", + return_value=async_redis_client, + ): + await index_long_term_memories( + long_term_memories, + ) results = await search_long_term_memories( "What is the capital of France?", @@ -195,11 +196,13 @@ async def test_search_messages_with_distance_threshold(self, async_redis_client) LongTermMemory(text="France is a country in Europe", session_id="123"), ] - await index_long_term_memories( - async_redis_client, - long_term_memories, - background_tasks=BackgroundTasks(), - ) + with mock.patch( + "agent_memory_server.long_term_memory.get_redis_conn", + return_value=async_redis_client, + ): + await index_long_term_memories( + long_term_memories, + ) results = await search_long_term_memories( "What is the capital of France?", diff --git a/tests/test_messages.py b/tests/test_messages.py index ca75e5a..7c4b2a5 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -160,6 +160,7 @@ async def test_set_session_memory_window_size_exceeded( mock_async_redis_client.pipeline = MagicMock(return_value=mock_pipeline) mock_background_tasks = MagicMock() + mock_background_tasks.add_task = AsyncMock() memory = SessionMemory( messages=[MemoryMessage(role="user", content="Hello")], @@ -181,7 +182,6 @@ async def test_set_session_memory_window_size_exceeded( # Verify summarization task was added mock_background_tasks.add_task.assert_called_with( summarize_session, - mock_async_redis_client, "test-session", "gpt-4o-mini", 20, @@ -205,6 +205,7 @@ async def test_set_session_memory_with_long_term_memory( mock_async_redis_client.pipeline = MagicMock(return_value=mock_pipeline) mock_background_tasks = MagicMock() + mock_background_tasks.add_task = AsyncMock() memory = SessionMemory( messages=[MemoryMessage(role="user", content="Hello")], @@ -228,9 +229,7 @@ async def test_set_session_memory_with_long_term_memory( assert mock_background_tasks.add_task.call_args_list == [ call( index_long_term_memories, - mock_async_redis_client, [LongTermMemory(session_id="test-session", text="user: Hello")], - mock_background_tasks, ), ] diff --git a/tests/test_summarization.py b/tests/test_summarization.py index 57723d2..cfb4bfb 100644 --- a/tests/test_summarization.py +++ b/tests/test_summarization.py @@ -111,13 +111,18 @@ async def test_summarize_session( mock_summarization.return_value = ("New summary", 300) - with patch( - "agent_memory_server.summarization.get_model_client" - ) as mock_get_model_client: + with ( + patch( + "agent_memory_server.summarization.get_model_client" + ) as mock_get_model_client, + patch( + "agent_memory_server.summarization.get_redis_conn", + return_value=mock_async_redis_client, + ), + ): mock_get_model_client.return_value = mock_openai_client await summarize_session( - mock_async_redis_client, session_id, model, window_size, @@ -178,12 +183,15 @@ async def test_handle_summarization_no_messages( pipeline_mock.lpop = AsyncMock(return_value=True) pipeline_mock.execute = AsyncMock(return_value=True) - await summarize_session( - mock_async_redis_client, - session_id, - model, - window_size, - ) + with patch( + "agent_memory_server.summarization.get_redis_conn", + return_value=mock_async_redis_client, + ): + await summarize_session( + session_id, + model, + window_size, + ) assert mock_summarization.call_count == 0 assert pipeline_mock.lrange.call_count == 0 diff --git a/uv.lock b/uv.lock index bf12ebb..2a4f835 100644 --- a/uv.lock +++ b/uv.lock @@ -35,8 +35,8 @@ dependencies = [ { name = "openai" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pydocket" }, { name = "python-dotenv" }, - { name = "redis" }, { name = "redisvl" }, { name = "sentence-transformers" }, { name = "structlog" }, @@ -68,8 +68,8 @@ requires-dist = [ { name = "openai", specifier = ">=1.3.7" }, { name = "pydantic", specifier = ">=2.5.2" }, { name = "pydantic-settings", specifier = ">=2.8.1" }, + { name = "pydocket", specifier = ">=0.6.3" }, { name = "python-dotenv", specifier = ">=1.0.0" }, - { name = "redis", specifier = ">=5.0.1" }, { name = "redisvl", specifier = ">=0.5.1" }, { name = "sentence-transformers", specifier = ">=3.4.1" }, { name = "structlog", specifier = ">=25.2.0" }, @@ -213,6 +213,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] +[[package]] +name = "cloudpickle" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -234,6 +243,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, ] +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, +] + [[package]] name = "distlib" version = "0.3.9" @@ -417,6 +438,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "importlib-metadata" +version = "8.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -512,6 +545,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193 }, ] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -569,6 +614,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + [[package]] name = "ml-dtypes" version = "0.4.1" @@ -826,6 +880,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/f7/049e85faf6a000890e5ca0edca8e9183f8a43c9e7bba869cad871da0caba/openai-1.71.0-py3-none-any.whl", hash = "sha256:e1c643738f1fff1af52bce6ef06a7716c95d089281e7011777179614f32937aa", size = 598975 }, ] +[[package]] +name = "opentelemetry-api" +version = "1.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/34/e701d77900123af17a11dbaf0c9f527fa7ef94b8f02b2c55bed94477890a/opentelemetry_api-1.32.0.tar.gz", hash = "sha256:2623280c916f9b19cad0aa4280cb171265f19fd2909b0d47e4f06f7c83b02cb5", size = 64134 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/e8/d05fd19c1c7e7e230ab44c366791179fd64c843bc587c257a56e853893c5/opentelemetry_api-1.32.0-py3-none-any.whl", hash = "sha256:15df743c765078611f376037b0d9111ec5c1febf2ec9440cdd919370faa1ce55", size = 65285 }, +] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.53b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/13/d1fe83281e40f050d04f45e6e865f36991e98d9c8f1c5f4614602af1431a/opentelemetry_exporter_prometheus-0.53b0.tar.gz", hash = "sha256:2d8dd0684b5229840974a85686028954a2b2170f5118cb36fa6497f11cb35f29", size = 14948 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/f3/4f0e7e33dcb41317905db2933f9ea86ce728d5710cf4f60063ee9763d4d5/opentelemetry_exporter_prometheus-0.53b0-py3-none-any.whl", hash = "sha256:a202262aa96f1840e0ebff75bef5b11e6d84cafd0e6a0f82979cccda0cdcee3b", size = 12950 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/0c/842aed73035cab0302ec70057f3180f4f023974d74bd9764ef3046f358fb/opentelemetry_sdk-1.32.0.tar.gz", hash = "sha256:5ff07fb371d1ab1189fa7047702e2e888b5403c5efcbb18083cae0d5aa5f58d2", size = 161043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/6a/b8cb562234bd94bcf12ad3058ef7f31319b94a8df65130ce9cc2ff3c8d55/opentelemetry_sdk-1.32.0-py3-none-any.whl", hash = "sha256:ed252d035c22a15536c1f603ca089298daab60850fc2f5ddfa95d95cc1c043ea", size = 118990 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.53b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c4/213d23239df175b420b74c6e25899c482701e6614822dc51f8c20dae7e2d/opentelemetry_semantic_conventions-0.53b0.tar.gz", hash = "sha256:05b7908e1da62d72f9bf717ed25c72f566fe005a2dd260c61b11e025f2552cf6", size = 114343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/23/0bef11f394f828f910f32567d057f097dbaba23edf33114018a380a0d0bf/opentelemetry_semantic_conventions-0.53b0-py3-none-any.whl", hash = "sha256:561da89f766ab51615c0e72b12329e0a1bc16945dbd62c8646ffc74e36a1edff", size = 188441 }, +] + [[package]] name = "packaging" version = "24.2" @@ -963,6 +1071,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, ] +[[package]] +name = "prometheus-client" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/14/7d0f567991f3a9af8d1cd4f619040c93b68f09a02b6d0b6ab1b2d1ded5fe/prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb", size = 78551 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/c2/ab7d37426c179ceb9aeb109a85cda8948bb269b7561a0be870cc656eefe4/prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301", size = 54682 }, +] + [[package]] name = "psutil" version = "7.0.0" @@ -1048,6 +1165,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, ] +[[package]] +name = "pydocket" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-prometheus" }, + { name = "prometheus-client" }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "typer" }, + { name = "uuid7" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/a5/925cea9bf8047c4c262f4d789c140f5bfb4a55d2e4dcfeccca1527e77403/pydocket-0.6.3.tar.gz", hash = "sha256:a7ffeb2c58fc8a98d5de27cdead5b5ec71f0eaae76ac4f9f0a32b7dc410057a6", size = 86026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/8f/710e6733a51ac4a8cb3480405275fd63b2c8cf061109e8ba0d525f0ba108/pydocket-0.6.3-py3-none-any.whl", hash = "sha256:2d7a148bc6341e463348ee9e375b1f25f8835289218c46707744dd69a5443c63", size = 32367 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + [[package]] name = "pynndescent" version = "0.5.13" @@ -1134,6 +1280,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] +[[package]] +name = "python-json-logger" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163 }, +] + [[package]] name = "python-ulid" version = "3.0.0" @@ -1274,6 +1429,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, +] + [[package]] name = "ruff" version = "0.11.4" @@ -1415,6 +1583,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + [[package]] name = "six" version = "1.17.0" @@ -1651,6 +1828,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/30/37a3384d1e2e9320331baca41e835e90a3767303642c7a80d4510152cbcf/triton-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0", size = 253154278 }, ] +[[package]] +name = "typer" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, +] + [[package]] name = "typing-extensions" version = "4.13.1" @@ -1707,6 +1899,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, ] +[[package]] +name = "uuid7" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/19/7472bd526591e2192926247109dbf78692e709d3e56775792fec877a7720/uuid7-0.1.0.tar.gz", hash = "sha256:8c57aa32ee7456d3cc68c95c4530bc571646defac01895cfc73545449894a63c", size = 14052 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/77/8852f89a91453956582a85024d80ad96f30a41fed4c2b3dce0c9f12ecc7e/uuid7-0.1.0-py2.py3-none-any.whl", hash = "sha256:5e259bb63c8cb4aded5927ff41b444a80d0c7124e8a0ced7cf44efa1f5cccf61", size = 7477 }, +] + [[package]] name = "uvicorn" version = "0.34.0" @@ -1775,3 +1976,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, ] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +]