Skip to content

Commit fdf70ab

Browse files
committed
Merge branch 'main' into feature/memory-duplication-fix
2 parents 056b71a + 09ce62a commit fdf70ab

File tree

7 files changed

+84
-40
lines changed

7 files changed

+84
-40
lines changed

agent_memory_server/api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,12 +308,12 @@ async def put_working_memory(
308308
if user_id is not None:
309309
memory.user_id = user_id
310310

311-
# Validate that all structured memories have id (if any)
312-
for mem in memory.memories:
313-
if not mem.id:
311+
# Validate that all long-term memories have id (if any)
312+
for long_term_mem in memory.memories:
313+
if not long_term_mem.id:
314314
raise HTTPException(
315315
status_code=400,
316-
detail="All memory records in working memory must have an ID",
316+
detail="All long-term memory records in working memory must have an ID",
317317
)
318318

319319
# Validate that all messages have non-empty content

agent_memory_server/cli.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
#!/usr/bin/env python
21
"""
32
Command-line interface for agent-memory-server.
43
"""
54

65
import datetime
76
import importlib
8-
import logging
97
import sys
108

119
import click
1210
import uvicorn
1311

1412
from agent_memory_server import __version__
1513
from agent_memory_server.config import settings
16-
from agent_memory_server.logging import configure_logging, get_logger
14+
from agent_memory_server.logging import (
15+
configure_logging,
16+
configure_mcp_logging,
17+
get_logger,
18+
)
1719
from agent_memory_server.migrations import (
1820
migrate_add_discrete_memory_extracted_2,
1921
migrate_add_memory_hashes_1,
@@ -22,7 +24,6 @@
2224
from agent_memory_server.utils.redis import ensure_search_index_exists, get_redis_conn
2325

2426

25-
configure_logging()
2627
logger = get_logger(__name__)
2728

2829
VERSION = __version__
@@ -45,6 +46,8 @@ def rebuild_index():
4546
"""Rebuild the search index."""
4647
import asyncio
4748

49+
configure_logging()
50+
4851
async def setup_and_run():
4952
redis = await get_redis_conn()
5053
await ensure_search_index_exists(redis, overwrite=True)
@@ -57,6 +60,7 @@ def migrate_memories():
5760
"""Migrate memories from the old format to the new format."""
5861
import asyncio
5962

63+
configure_logging()
6064
click.echo("Starting memory migrations...")
6165

6266
async def run_migrations():
@@ -82,6 +86,7 @@ def api(port: int, host: str, reload: bool):
8286
"""Run the REST API server."""
8387
from agent_memory_server.main import on_start_logger
8488

89+
configure_logging()
8590
on_start_logger(port)
8691
uvicorn.run(
8792
"agent_memory_server.main:app",
@@ -103,6 +108,12 @@ def mcp(port: int, mode: str):
103108
"""Run the MCP server."""
104109
import asyncio
105110

111+
# Configure MCP-specific logging BEFORE any imports to avoid stdout contamination
112+
if mode == "stdio":
113+
configure_mcp_logging()
114+
else:
115+
configure_logging()
116+
106117
# Update the port in settings FIRST
107118
settings.mcp_port = port
108119

@@ -117,14 +128,7 @@ async def setup_and_run():
117128
logger.info(f"Starting MCP server on port {port}\n")
118129
await mcp_app.run_sse_async()
119130
elif mode == "stdio":
120-
# Try to force all logging to stderr because stdio-mode MCP servers
121-
# use standard output for the protocol.
122-
logging.basicConfig(
123-
level=settings.log_level,
124-
stream=sys.stderr,
125-
force=True, # remove any existing handlers
126-
format="%(asctime)s %(name)s %(levelname)s %(message)s",
127-
)
131+
# Logging already configured above
128132
await mcp_app.run_stdio_async()
129133
else:
130134
raise ValueError(f"Invalid mode: {mode}")
@@ -154,6 +158,8 @@ def schedule_task(task_path: str, args: list[str]):
154158

155159
from docket import Docket
156160

161+
configure_logging()
162+
157163
# Parse the arguments
158164
task_args = {}
159165
for arg in args:
@@ -219,6 +225,8 @@ def task_worker(concurrency: int, redelivery_timeout: int):
219225

220226
from docket import Worker
221227

228+
configure_logging()
229+
222230
if not settings.use_docket:
223231
click.echo("Docket is disabled in settings. Cannot run worker.")
224232
sys.exit(1)

agent_memory_server/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ class Settings(BaseSettings):
116116

117117
# Other Application settings
118118
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
119+
default_mcp_user_id: str | None = None
120+
default_mcp_namespace: str | None = None
119121

120122
class Config:
121123
env_file = ".env"

agent_memory_server/logging.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def configure_logging():
1717

1818
# Configure standard library logging based on settings.log_level
1919
level = getattr(logging, settings.log_level.upper(), logging.INFO)
20-
handler = logging.StreamHandler(sys.stdout)
20+
handler = logging.StreamHandler(sys.stderr) # Use stderr instead of stdout
2121
handler.setLevel(level)
2222
logging.basicConfig(level=level, handlers=[handler], format="%(message)s")
2323

@@ -46,6 +46,41 @@ def configure_logging():
4646
_configured = True
4747

4848

49+
def configure_mcp_logging():
50+
"""Configure logging specifically for MCP server in stdio mode"""
51+
global _configured
52+
53+
# Clear any existing handlers and configuration
54+
root_logger = logging.getLogger()
55+
root_logger.handlers.clear()
56+
57+
# Configure stderr-only logging for MCP stdio mode
58+
level = getattr(logging, settings.log_level.upper(), logging.INFO)
59+
stderr_handler = logging.StreamHandler(sys.stderr)
60+
stderr_handler.setLevel(level)
61+
stderr_handler.setFormatter(
62+
logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")
63+
)
64+
root_logger.addHandler(stderr_handler)
65+
root_logger.setLevel(level)
66+
67+
# Configure structlog to also use stderr
68+
structlog.configure(
69+
processors=[
70+
structlog.stdlib.filter_by_level,
71+
structlog.stdlib.add_logger_name,
72+
structlog.stdlib.add_log_level,
73+
structlog.processors.TimeStamper(fmt="iso"),
74+
structlog.processors.format_exc_info,
75+
structlog.processors.JSONRenderer(),
76+
],
77+
wrapper_class=structlog.stdlib.BoundLogger,
78+
logger_factory=structlog.stdlib.LoggerFactory(),
79+
cache_logger_on_first_use=True,
80+
)
81+
_configured = True
82+
83+
4984
def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger:
5085
"""
5186
Get a configured logger instance.

agent_memory_server/mcp.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
import os
32
from typing import Any
43

54
import ulid
@@ -43,9 +42,6 @@
4342

4443
logger = logging.getLogger(__name__)
4544

46-
# Default namespace for STDIO mode
47-
DEFAULT_NAMESPACE = os.getenv("MCP_NAMESPACE")
48-
4945

5046
class FastMCP(_FastMCPBase):
5147
"""Extend FastMCP to support optional URL namespace and default STDIO namespace."""
@@ -165,7 +161,7 @@ async def run_stdio_async(self):
165161
"Redis Agent Memory Server",
166162
port=settings.mcp_port,
167163
instructions=INSTRUCTIONS,
168-
default_namespace=DEFAULT_NAMESPACE,
164+
default_namespace=settings.default_mcp_namespace,
169165
)
170166

171167

@@ -301,10 +297,11 @@ async def create_long_term_memories(
301297
An acknowledgement response indicating success
302298
"""
303299
# Apply default namespace for STDIO if not provided in memory entries
304-
if DEFAULT_NAMESPACE:
305-
for mem in memories:
306-
if mem.namespace is None:
307-
mem.namespace = DEFAULT_NAMESPACE
300+
for mem in memories:
301+
if mem.namespace is None and settings.default_mcp_namespace:
302+
mem.namespace = settings.default_mcp_namespace
303+
if mem.user_id is None and settings.default_mcp_user_id:
304+
mem.user_id = settings.default_mcp_user_id
308305

309306
payload = CreateMemoryRecordRequest(
310307
memories=[MemoryRecord(**mem.model_dump()) for mem in memories]
@@ -418,6 +415,11 @@ async def search_long_term_memory(
418415
Returns:
419416
MemoryRecordResults containing matched memories sorted by relevance
420417
"""
418+
if user_id is None and settings.default_mcp_user_id:
419+
user_id = UserId(eq=settings.default_mcp_user_id)
420+
if namespace is None and settings.default_mcp_namespace:
421+
namespace = Namespace(eq=settings.default_mcp_namespace)
422+
421423
try:
422424
payload = SearchRequest(
423425
text=text,
@@ -545,7 +547,7 @@ async def memory_prompt(
545547
546548
Args:
547549
- text: The user's query
548-
- session_id: Add conversation history from a session
550+
- session_id: Add conversation history from a working memory session
549551
- namespace: Filter session and long-term memory namespace
550552
- topics: Search for long-term memories matching topics
551553
- entities: Search for long-term memories matching entities
@@ -562,6 +564,9 @@ async def memory_prompt(
562564
_session_id = session_id.eq if session_id and session_id.eq else None
563565
session = None
564566

567+
if user_id is None and settings.default_mcp_user_id:
568+
user_id = UserId(eq=settings.default_mcp_user_id)
569+
565570
if _session_id is not None:
566571
session = WorkingMemoryRequest(
567572
session_id=_session_id,
@@ -602,8 +607,8 @@ async def set_working_memory(
602607
messages: list[MemoryMessage] | None = None,
603608
context: str | None = None,
604609
data: dict[str, Any] | None = None,
605-
namespace: str | None = None,
606-
user_id: str | None = None,
610+
namespace: str | None = settings.default_mcp_namespace,
611+
user_id: str | None = settings.default_mcp_user_id,
607612
ttl_seconds: int = 3600,
608613
) -> WorkingMemoryResponse:
609614
"""
@@ -701,11 +706,6 @@ async def set_working_memory(
701706
Returns:
702707
Updated working memory response (may include summarization if window exceeded)
703708
"""
704-
# Apply default namespace if configured
705-
memory_namespace = namespace
706-
if not memory_namespace and DEFAULT_NAMESPACE:
707-
memory_namespace = DEFAULT_NAMESPACE
708-
709709
# Auto-generate IDs for memories that don't have them
710710
processed_memories = []
711711
if memories:
@@ -717,6 +717,7 @@ async def set_working_memory(
717717
processed_memory = memory.model_copy(
718718
update={
719719
"id": memory_id,
720+
"user_id": user_id,
720721
"persisted_at": None, # Mark as pending promotion
721722
}
722723
)
@@ -756,7 +757,7 @@ async def set_working_memory(
756757
# Create the working memory object
757758
working_memory_obj = WorkingMemory(
758759
session_id=session_id,
759-
namespace=memory_namespace,
760+
namespace=namespace,
760761
memories=processed_memories,
761762
messages=processed_messages,
762763
context=context,

agent_memory_server/vectorstore_adapter.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -829,15 +829,13 @@ async def search_memories(
829829
search_kwargs["score_threshold"] = score_threshold
830830

831831
logger.debug(f"[search_memories] Search kwargs: {search_kwargs}")
832-
833832
search_results = (
834833
await self.vectorstore.asimilarity_search_with_relevance_scores(
835834
**search_kwargs
836835
)
837836
)
838837

839838
logger.debug(f"[search_memories] Search results: {search_results}")
840-
841839
# Convert results to MemoryRecordResult objects
842840
memory_results = []
843841
for i, (doc, score) in enumerate(search_results):

tests/test_cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ def test_mcp_command_sse_mode(self, mock_mcp_app, mock_settings):
157157
assert result.exit_code == 0
158158
mock_mcp_app.run_sse_async.assert_called_once()
159159

160-
@patch("agent_memory_server.cli.logging.basicConfig")
160+
@patch("agent_memory_server.cli.configure_mcp_logging")
161161
@patch("agent_memory_server.cli.settings")
162162
@patch("agent_memory_server.mcp.mcp_app")
163163
def test_mcp_command_stdio_logging_config(
164-
self, mock_mcp_app, mock_settings, mock_basic_config
164+
self, mock_mcp_app, mock_settings, mock_configure_mcp_logging
165165
):
166166
"""Test that stdio mode configures logging to stderr."""
167167
mock_settings.mcp_port = 3001
@@ -174,7 +174,7 @@ def test_mcp_command_stdio_logging_config(
174174

175175
assert result.exit_code == 0
176176
mock_mcp_app.run_stdio_async.assert_called_once()
177-
mock_basic_config.assert_called_once()
177+
mock_configure_mcp_logging.assert_called_once()
178178

179179

180180
class TestScheduleTask:

0 commit comments

Comments
 (0)