Skip to content

Commit 629e6c7

Browse files
committed
Fixes to deduplication, publish to GHCR, prompt agent example
1 parent 39ac947 commit 629e6c7

File tree

15 files changed

+517
-595
lines changed

15 files changed

+517
-595
lines changed

.github/workflows/python-tests.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,12 @@ jobs:
8181
- name: Set up Docker Buildx
8282
uses: docker/setup-buildx-action@v3
8383

84-
- name: Log in to Docker Hub
84+
- name: Log in to GitHub Container Registry
8585
uses: docker/login-action@v3
8686
with:
87-
username: ${{ secrets.DOCKER_USERNAME }}
88-
password: ${{ secrets.DOCKER_TOKEN }}
87+
registry: ghcr.io
88+
username: ${{ github.actor }}
89+
password: ${{ secrets.GITHUB_TOKEN }}
8990

9091
- name: Extract version from __init__.py
9192
id: version
@@ -102,7 +103,7 @@ jobs:
102103
platforms: linux/amd64,linux/arm64
103104
push: true
104105
tags: |
105-
andrewbrookins510/agent-memory-server:latest
106-
andrewbrookins510/agent-memory-server:${{ steps.version.outputs.version }}
106+
ghcr.io/${{ github.repository }}:latest
107+
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
107108
cache-from: type=gha
108109
cache-to: type=gha,mode=max

agent-memory-client/agent_memory_client/client.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ async def search_long_term_memory(
676676

677677
try:
678678
response = await self._client.post(
679-
"/v1/memory/search",
679+
"/v1/long-term-memory/search",
680680
json=payload,
681681
)
682682
response.raise_for_status()
@@ -2114,14 +2114,17 @@ async def append_messages_to_working_memory(
21142114
final_messages = existing_messages + converted_messages
21152115

21162116
# Create updated working memory
2117-
working_memory = WorkingMemory(
2118-
session_id=session_id,
2119-
namespace=namespace or self.config.default_namespace,
2120-
messages=final_messages,
2121-
memories=existing_memory.memories if existing_memory else [],
2122-
data=existing_memory.data if existing_memory else {},
2123-
context=existing_memory.context if existing_memory else None,
2124-
user_id=existing_memory.user_id if existing_memory else None,
2117+
working_memory = (
2118+
existing_memory.model_copy(
2119+
update={"messages": final_messages},
2120+
)
2121+
if existing_memory
2122+
else WorkingMemory(
2123+
session_id=session_id,
2124+
namespace=namespace or self.config.default_namespace,
2125+
messages=final_messages,
2126+
user_id=user_id or None,
2127+
)
21252128
)
21262129

21272130
return await self.put_working_memory(
@@ -2216,7 +2219,6 @@ async def memory_prompt(
22162219
payload["long_term_search"] = long_term_search
22172220

22182221
try:
2219-
print("Payload: ", payload)
22202222
response = await self._client.post(
22212223
"/v1/memory/prompt",
22222224
json=payload,

agent-memory-client/agent_memory_client/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ class MemoryMessage(BaseModel):
6161
default=None,
6262
description="Server-assigned timestamp when message was persisted to long-term storage",
6363
)
64+
discrete_memory_extracted: Literal["t", "f"] = Field(
65+
default="f",
66+
description="Whether memory extraction has run for this message",
67+
)
6468

6569

6670
class MemoryRecord(BaseModel):

agent_memory_server/api.py

Lines changed: 61 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from agent_memory_server.auth import UserInfo, get_current_user
88
from agent_memory_server.config import settings
99
from agent_memory_server.dependencies import get_background_tasks
10+
from agent_memory_server.filters import SessionId, UserId
1011
from agent_memory_server.llms import get_model_client, get_model_config
1112
from agent_memory_server.logging import get_logger
1213
from agent_memory_server.models import (
@@ -234,7 +235,6 @@ async def get_working_memory(
234235
"""
235236
redis = await get_redis_conn()
236237

237-
# Get unified working memory
238238
working_mem = await working_memory.get_working_memory(
239239
session_id=session_id,
240240
namespace=namespace,
@@ -243,7 +243,7 @@ async def get_working_memory(
243243
)
244244

245245
if not working_mem:
246-
# Return empty working memory if none exists
246+
# Create empty working memory if none exists
247247
working_mem = WorkingMemory(
248248
messages=[],
249249
memories=[],
@@ -267,6 +267,8 @@ async def get_working_memory(
267267
break
268268
working_mem.messages = truncated_messages
269269

270+
logger.debug(f"Working mem: {working_mem}")
271+
270272
return working_mem
271273

272274

@@ -314,6 +316,14 @@ async def put_working_memory(
314316
detail="All memory records in working memory must have an ID",
315317
)
316318

319+
# Validate that all messages have non-empty content
320+
for msg in memory.messages:
321+
if not msg.content or not msg.content.strip():
322+
raise HTTPException(
323+
status_code=400,
324+
detail=f"Message content cannot be empty (message ID: {msg.id})",
325+
)
326+
317327
# Handle summarization if needed (before storing) - now token-based
318328
updated_memory = memory
319329
if memory.messages:
@@ -333,8 +343,9 @@ async def put_working_memory(
333343
# Promote structured memories from working memory to long-term storage
334344
await background_tasks.add_task(
335345
long_term_memory.promote_working_memory_to_long_term,
336-
session_id,
337-
updated_memory.namespace,
346+
session_id=session_id,
347+
user_id=updated_memory.user_id,
348+
namespace=updated_memory.namespace,
338349
)
339350

340351
return updated_memory
@@ -431,7 +442,7 @@ async def search_long_term_memory(
431442
# Extract filter objects from the payload
432443
filters = payload.get_filters()
433444

434-
print("Long-term search filters: ", filters)
445+
logger.debug(f"Long-term search filters: {filters}")
435446

436447
kwargs = {
437448
"distance_threshold": payload.distance_threshold,
@@ -440,10 +451,10 @@ async def search_long_term_memory(
440451
**filters,
441452
}
442453

443-
print("Kwargs: ", kwargs)
444-
445454
kwargs["text"] = payload.text or ""
446455

456+
logger.debug(f"Long-term search kwargs: {kwargs}")
457+
447458
# Pass text and filter objects to the search function (no redis needed for vectorstore adapter)
448459
return await long_term_memory.search_long_term_memories(**kwargs)
449460

@@ -466,53 +477,6 @@ async def delete_long_term_memory(
466477
return AckResponse(status=f"ok, deleted {count} memories")
467478

468479

469-
@router.post("/v1/memory/search", response_model=MemoryRecordResultsResponse)
470-
async def search_memory(
471-
payload: SearchRequest,
472-
current_user: UserInfo = Depends(get_current_user),
473-
):
474-
"""
475-
Run a search across all memory types (working memory and long-term memory).
476-
477-
This endpoint searches both working memory (ephemeral, session-scoped) and
478-
long-term memory (persistent, indexed) to provide comprehensive results.
479-
480-
For working memory:
481-
- Uses simple text matching
482-
- Searches across all sessions (unless session_id filter is provided)
483-
- Returns memories that haven't been promoted to long-term storage
484-
485-
For long-term memory:
486-
- Uses semantic vector search
487-
- Includes promoted memories from working memory
488-
- Supports advanced filtering by topics, entities, etc.
489-
490-
Args:
491-
payload: Search payload with filter objects for precise queries
492-
493-
Returns:
494-
Search results from both memory types, sorted by relevance
495-
"""
496-
redis = await get_redis_conn()
497-
498-
# Extract filter objects from the payload
499-
filters = payload.get_filters()
500-
501-
kwargs = {
502-
"redis": redis,
503-
"distance_threshold": payload.distance_threshold,
504-
"limit": payload.limit,
505-
"offset": payload.offset,
506-
**filters,
507-
}
508-
509-
if payload.text:
510-
kwargs["text"] = payload.text
511-
512-
# Use the search function
513-
return await long_term_memory.search_memories(**kwargs)
514-
515-
516480
@router.post("/v1/memory/prompt", response_model=MemoryPromptResponse)
517481
async def memory_prompt(
518482
params: MemoryPromptRequest,
@@ -546,7 +510,7 @@ async def memory_prompt(
546510
redis = await get_redis_conn()
547511
_messages = []
548512

549-
print("Received params: ", params)
513+
logger.debug(f"Memory prompt params: {params}")
550514

551515
if params.session:
552516
# Use token limit for memory prompt, fallback to message count for backward compatibility
@@ -569,6 +533,8 @@ async def memory_prompt(
569533
redis_client=redis,
570534
)
571535

536+
logger.debug(f"Found working memory: {working_mem}")
537+
572538
if working_mem:
573539
if working_mem.context:
574540
# TODO: Weird to use MCP types here?
@@ -580,7 +546,7 @@ async def memory_prompt(
580546
),
581547
)
582548
)
583-
# Apply token-based or message-based truncation
549+
# Apply token-based truncation if model info is provided
584550
if params.session.model_name or params.session.context_window_max:
585551
# Token-based truncation
586552
if (
@@ -598,39 +564,55 @@ async def memory_prompt(
598564
break
599565
else:
600566
recent_messages = working_mem.messages
567+
568+
for msg in recent_messages:
569+
if msg.role == "user":
570+
msg_class = base.UserMessage
571+
else:
572+
msg_class = base.AssistantMessage
573+
_messages.append(
574+
msg_class(
575+
content=TextContent(type="text", text=msg.content),
576+
)
577+
)
601578
else:
602-
# Message-based truncation (backward compatibility)
603-
recent_messages = (
604-
working_mem.messages[-effective_window_size:]
605-
if len(working_mem.messages) > effective_window_size
606-
else working_mem.messages
607-
)
608-
for msg in recent_messages:
609-
if msg.role == "user":
610-
msg_class = base.UserMessage
611-
else:
612-
msg_class = base.AssistantMessage
613-
_messages.append(
614-
msg_class(
615-
content=TextContent(type="text", text=msg.content),
579+
# No token-based truncation - use all messages
580+
for msg in working_mem.messages:
581+
if msg.role == "user":
582+
msg_class = base.UserMessage
583+
else:
584+
msg_class = base.AssistantMessage
585+
_messages.append(
586+
msg_class(
587+
content=TextContent(type="text", text=msg.content),
588+
)
616589
)
617-
)
618590

619591
if params.long_term_search:
620-
# TODO: Exclude session messages if we already included them from session memory
621-
622-
# If no text is provided in long_term_search, use the user's query
623-
if not params.long_term_search.text:
624-
# Create a new SearchRequest with the query as text
625-
search_payload = params.long_term_search.model_copy()
626-
search_payload.text = params.query
592+
logger.debug(
593+
f"[memory_prompt] Long-term search args: {params.long_term_search}"
594+
)
595+
if isinstance(params.long_term_search, bool):
596+
search_kwargs = {}
597+
if params.session:
598+
# Exclude memories from the current session because we already included them
599+
search_kwargs["session_id"] = SessionId(ne=params.session.session_id)
600+
if params.session and params.session.user_id:
601+
search_kwargs["user_id"] = UserId(eq=params.session.user_id)
602+
search_payload = SearchRequest(**search_kwargs, limit=20, offset=0)
627603
else:
628-
search_payload = params.long_term_search
604+
search_payload = params.long_term_search.model_copy()
605+
# Merge session user_id into the search request if not already specified
606+
if params.session and params.session.user_id and not search_payload.user_id:
607+
search_payload.user_id = UserId(eq=params.session.user_id)
629608

609+
logger.debug(f"[memory_prompt] Search payload: {search_payload}")
630610
long_term_memories = await search_long_term_memory(
631611
search_payload,
632612
)
633613

614+
logger.debug(f"[memory_prompt] Long-term memories: {long_term_memories}")
615+
634616
if long_term_memories.total > 0:
635617
long_term_memories_text = "\n".join(
636618
[f"- {m.text}" for m in long_term_memories.memories]

agent_memory_server/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class Settings(BaseSettings):
8787
# Used for extracting entities from text
8888
ner_model: str = "dbmdz/bert-large-cased-finetuned-conll03-english"
8989
enable_ner: bool = True
90+
index_all_messages_in_long_term_memory: bool = False
9091

9192
# RedisVL Settings
9293
# TODO: Adapt to vector store settings

agent_memory_server/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ class MemoryHash(TagFilter):
245245

246246

247247
class Id(TagFilter):
248-
field: str = "id_"
248+
field: str = "id"
249249

250250

251251
class DiscreteMemoryExtracted(TagFilter):

0 commit comments

Comments
 (0)