Skip to content

Commit f0104db

Browse files
authored
Merge pull request #5 from redis-developer/add-cli
Add a CLI
2 parents 38b91cc + 110e686 commit f0104db

11 files changed

+420
-125
lines changed

README.md

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,91 @@ Agent Memory Server offers an MCP (Model Context Protocol) server interface powe
120120
- **search_memory**: Perform semantic search across long-term memories.
121121
- **memory_prompt**: Generate prompts enriched with session context and long-term memories.
122122

123+
## Command Line Interface
124+
125+
The `agent-memory-server` provides a command-line interface (CLI) for managing the server and related tasks. You can access the CLI using the `agent-memory` command (assuming the package is installed in a way that makes the script available in your PATH, e.g., via `pip install ...`).
126+
127+
### Available Commands
128+
129+
Here's a list of available commands and their functions:
130+
131+
#### `version`
132+
Displays the current version of `agent-memory-server`.
133+
```bash
134+
agent-memory version
135+
```
136+
137+
#### `api`
138+
Starts the REST API server.
139+
```bash
140+
agent-memory api [OPTIONS]
141+
```
142+
**Options:**
143+
* `--port INTEGER`: Port to run the server on. (Default: value from `settings.port`, usually 8000)
144+
* `--host TEXT`: Host to run the server on. (Default: "0.0.0.0")
145+
* `--reload`: Enable auto-reload for development.
146+
147+
Example:
148+
```bash
149+
agent-memory api --port 8080 --reload
150+
```
151+
152+
#### `mcp`
153+
Starts the Model Context Protocol (MCP) server.
154+
```bash
155+
agent-memory mcp [OPTIONS]
156+
```
157+
**Options:**
158+
* `--port INTEGER`: Port to run the MCP server on. (Default: value from `settings.mcp_port`, usually 9000)
159+
* `--sse`: Run the MCP server in Server-Sent Events (SSE) mode. If not provided, it runs in stdio mode.
160+
161+
Example (SSE mode):
162+
```bash
163+
agent-memory mcp --port 9001 --sse
164+
```
165+
Example (stdio mode):
166+
```bash
167+
agent-memory mcp --port 9001
168+
```
169+
170+
#### `schedule-task`
171+
Schedules a background task to be processed by a Docket worker.
172+
```bash
173+
agent-memory schedule-task <TASK_PATH> [OPTIONS]
174+
```
175+
**Arguments:**
176+
* `TASK_PATH`: The Python import path to the task function. For example: `"agent_memory_server.long_term_memory.compact_long_term_memories"`
177+
178+
**Options:**
179+
* `--args TEXT` / `-a TEXT`: Arguments to pass to the task in `key=value` format. Can be specified multiple times. Values are automatically converted to boolean, integer, or float if possible, otherwise they remain strings.
180+
181+
Example:
182+
```bash
183+
agent-memory schedule-task "agent_memory_server.long_term_memory.compact_long_term_memories" -a limit=500 -a namespace=my_namespace -a compact_semantic_duplicates=false
184+
```
185+
186+
#### `task-worker`
187+
Starts a Docket worker to process background tasks from the queue. This worker uses the Docket name configured in settings.
188+
```bash
189+
agent-memory task-worker [OPTIONS]
190+
```
191+
**Options:**
192+
* `--concurrency INTEGER`: Number of tasks to process concurrently. (Default: 10)
193+
* `--redelivery-timeout INTEGER`: Seconds to wait before a task is redelivered to another worker if the current worker fails or times out. (Default: 30)
194+
195+
Example:
196+
```bash
197+
agent-memory task-worker --concurrency 5 --redelivery-timeout 60
198+
```
199+
200+
To see help for any command, you can use `--help`:
201+
```bash
202+
agent-memory --help
203+
agent-memory api --help
204+
agent-memory mcp --help
205+
# etc.
206+
```
207+
123208
## Getting Started
124209

125210
### Local Install
@@ -297,7 +382,7 @@ Currently, memory compaction is only available as a function in `agent_memory_se
297382
- **Semantic Deduplication**: Finds and merges memories with similar meaning using vector search
298383
- **LLM-powered Merging**: Uses language models to intelligently combine memories
299384

300-
### Contributing
385+
## Contributing
301386
1. Fork the repository
302387
2. Create a feature branch
303388
3. Commit your changes
@@ -308,8 +393,5 @@ Currently, memory compaction is only available as a function in `agent_memory_se
308393

309394
```bash
310395
# Run all tests
311-
python -m pytest tests/test_memory_compaction.py
312-
313-
# Run specific integration test
314-
python -m pytest tests/test_memory_compaction.py::TestMemoryCompaction::test_compact_memories_integration -v
396+
pytest tests
315397
```

agent_memory_server/cli.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env python
2+
"""
3+
Command-line interface for agent-memory-server.
4+
"""
5+
6+
import datetime
7+
import importlib
8+
import sys
9+
10+
import click
11+
import uvicorn
12+
13+
from agent_memory_server.config import settings
14+
from agent_memory_server.logging import configure_logging, get_logger
15+
from agent_memory_server.utils.redis import ensure_search_index_exists, get_redis_conn
16+
17+
18+
configure_logging()
19+
logger = get_logger(__name__)
20+
21+
VERSION = "0.2.0"
22+
23+
24+
@click.group()
25+
def cli():
26+
"""Command-line interface for agent-memory-server."""
27+
pass
28+
29+
30+
@cli.command()
31+
def version():
32+
"""Show the version of agent-memory-server."""
33+
click.echo(f"agent-memory-server version {VERSION}")
34+
35+
36+
@cli.command()
37+
@click.option("--port", default=settings.port, help="Port to run the server on")
38+
@click.option("--host", default="0.0.0.0", help="Host to run the server on")
39+
@click.option("--reload", is_flag=True, help="Enable auto-reload")
40+
def api(port: int, host: str, reload: bool):
41+
"""Run the REST API server."""
42+
import asyncio
43+
44+
from agent_memory_server.main import app, on_start_logger
45+
46+
async def setup_redis():
47+
redis = await get_redis_conn()
48+
await ensure_search_index_exists(redis)
49+
50+
# Run the async setup
51+
asyncio.run(setup_redis())
52+
53+
on_start_logger(port)
54+
uvicorn.run(
55+
app,
56+
host=host,
57+
port=port,
58+
reload=reload,
59+
)
60+
61+
62+
@cli.command()
63+
@click.option("--port", default=settings.mcp_port, help="Port to run the MCP server on")
64+
@click.option("--sse", is_flag=True, help="Run the MCP server in SSE mode")
65+
def mcp(port: int, sse: bool):
66+
"""Run the MCP server."""
67+
import asyncio
68+
69+
# Update the port in settings FIRST
70+
settings.mcp_port = port
71+
72+
# Import mcp_app AFTER settings have been updated
73+
from agent_memory_server.mcp import mcp_app
74+
75+
async def setup_and_run():
76+
redis = await get_redis_conn()
77+
await ensure_search_index_exists(redis)
78+
79+
# Run the MCP server
80+
if sse:
81+
await mcp_app.run_sse_async()
82+
else:
83+
await mcp_app.run_stdio_async()
84+
85+
# Update the port in settings
86+
settings.mcp_port = port
87+
88+
click.echo(f"Starting MCP server on port {port}")
89+
90+
if sse:
91+
click.echo("Running in SSE mode")
92+
else:
93+
click.echo("Running in stdio mode")
94+
95+
asyncio.run(setup_and_run())
96+
97+
98+
@cli.command()
99+
@click.argument("task_path")
100+
@click.option(
101+
"--args",
102+
"-a",
103+
multiple=True,
104+
help="Arguments to pass to the task in the format key=value",
105+
)
106+
def schedule_task(task_path: str, args: list[str]):
107+
"""
108+
Schedule a background task by path.
109+
110+
TASK_PATH is the import path to the task function, e.g.,
111+
"agent_memory_server.long_term_memory.compact_long_term_memories"
112+
"""
113+
import asyncio
114+
115+
from docket import Docket
116+
117+
# Parse the arguments
118+
task_args = {}
119+
for arg in args:
120+
try:
121+
key, value = arg.split("=", 1)
122+
# Try to convert to appropriate type
123+
if value.lower() == "true":
124+
task_args[key] = True
125+
elif value.lower() == "false":
126+
task_args[key] = False
127+
elif value.isdigit():
128+
task_args[key] = int(value)
129+
elif value.replace(".", "", 1).isdigit() and value.count(".") <= 1:
130+
task_args[key] = float(value)
131+
else:
132+
task_args[key] = value
133+
except ValueError:
134+
click.echo(f"Invalid argument format: {arg}. Use key=value format.")
135+
sys.exit(1)
136+
137+
async def setup_and_run_task():
138+
redis = await get_redis_conn()
139+
await ensure_search_index_exists(redis)
140+
141+
# Import the task function
142+
module_path, function_name = task_path.rsplit(".", 1)
143+
try:
144+
module = importlib.import_module(module_path)
145+
task_func = getattr(module, function_name)
146+
except (ImportError, AttributeError) as e:
147+
click.echo(f"Error importing task: {e}")
148+
sys.exit(1)
149+
150+
# Initialize Docket client
151+
async with Docket(
152+
name=settings.docket_name,
153+
url=settings.redis_url,
154+
) as docket:
155+
click.echo(f"Scheduling task {task_path} with arguments: {task_args}")
156+
await docket.add(task_func)(**task_args)
157+
click.echo("Task scheduled successfully")
158+
159+
asyncio.run(setup_and_run_task())
160+
161+
162+
@cli.command()
163+
@click.option(
164+
"--concurrency", default=10, help="Number of tasks to process concurrently"
165+
)
166+
@click.option(
167+
"--redelivery-timeout",
168+
default=30,
169+
help="Seconds to wait before redelivering a task to another worker",
170+
)
171+
def task_worker(concurrency: int, redelivery_timeout: int):
172+
"""
173+
Start a Docket worker using the Docket name from settings.
174+
175+
This command starts a worker that processes background tasks registered
176+
with Docket. The worker uses the Docket name from settings.
177+
"""
178+
import asyncio
179+
180+
from docket import Worker
181+
182+
if not settings.use_docket:
183+
click.echo("Docket is disabled in settings. Cannot run worker.")
184+
sys.exit(1)
185+
186+
asyncio.run(
187+
Worker.run(
188+
docket_name=settings.docket_name,
189+
url=settings.redis_url,
190+
concurrency=concurrency,
191+
redelivery_timeout=datetime.timedelta(seconds=redelivery_timeout),
192+
tasks=["agent_memory_server.docket_tasks:task_collection"],
193+
)
194+
)
195+
196+
197+
if __name__ == "__main__":
198+
cli()

agent_memory_server/docket_tasks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from agent_memory_server.config import settings
1010
from agent_memory_server.long_term_memory import (
11+
compact_long_term_memories,
1112
extract_memory_structure,
1213
index_long_term_memories,
1314
)
@@ -22,6 +23,7 @@
2223
extract_memory_structure,
2324
summarize_session,
2425
index_long_term_memories,
26+
compact_long_term_memories,
2527
]
2628

2729

agent_memory_server/long_term_memory.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,6 @@ async def compact_long_term_memories(
283283
if compact_hash_duplicates:
284284
logger.info("Starting hash-based duplicate compaction")
285285
try:
286-
# TODO: Use RedisVL index
287286
index_name = Keys.search_index_name()
288287

289288
# Create aggregation query to group by memory_hash and find duplicates
@@ -386,7 +385,7 @@ async def compact_long_term_memories(
386385
logger.warning(f"Error checking index: {info_e}")
387386

388387
# Get all memories matching the filters
389-
index = await get_search_index(redis_client)
388+
index = get_search_index(redis_client)
390389
query_str = filter_str if filter_str != "*" else ""
391390

392391
# Create a query to get all memories
@@ -675,8 +674,6 @@ async def index_long_term_memories(
675674
redis_client: Optional Redis client to use. If None, a new connection will be created.
676675
"""
677676
redis = redis_client or await get_redis_conn()
678-
# Ensure search index exists before indexing memories
679-
await ensure_search_index_exists(redis)
680677
background_tasks = get_background_tasks()
681678
vectorizer = OpenAITextVectorizer()
682679
embeddings = await vectorizer.aembed_many(

agent_memory_server/utils/keys.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import logging
44

5+
from agent_memory_server.config import settings
6+
57

68
logger = logging.getLogger(__name__)
79

@@ -56,4 +58,4 @@ def metadata_key(session_id: str, namespace: str | None = None) -> str:
5658
@staticmethod
5759
def search_index_name() -> str:
5860
"""Return the name of the search index."""
59-
return "memory_idx"
61+
return settings.redisvl_index_name

0 commit comments

Comments
 (0)