Skip to content

Commit 8f5acd8

Browse files
Aayush KatariaAayush Kataria
authored andcommitted
resolving comments
1 parent 894d839 commit 8f5acd8

16 files changed

Lines changed: 293 additions & 83 deletions

File tree

agent_memory_toolkit/_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,18 @@ def _container_policies(
378378
],
379379
"vectorIndexes": [{"path": "/embedding", "type": "diskANN"}],
380380
"fullTextIndexes": [{"path": "/content"}],
381+
# Procedural synthesis selects TOP N by (salience DESC, created_at ASC, id ASC).
382+
# Cosmos requires a composite index for multi-property ORDER BY; without it the
383+
# query returns a non-deterministic 50 of N when many docs share the default
384+
# salience (0.5), which makes the source-id short-circuit in synthesize_procedural
385+
# thrash and burn LLM calls on every reconcile.
386+
"compositeIndexes": [
387+
[
388+
{"path": "/salience", "order": "descending"},
389+
{"path": "/created_at", "order": "ascending"},
390+
{"path": "/id", "order": "ascending"},
391+
]
392+
],
381393
}
382394

383395
full_text_policy = {

agent_memory_toolkit/aio/auto_trigger.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ async def maybe_trigger_steps(
7171
n_facts=n_facts,
7272
n_summary=n_summary,
7373
n_dedup_turns=n_dedup_turns,
74+
thresholds=thresholds,
7475
)
7576
await _trigger_user_steps(processor, counter_container, user_batch_counts, n_user=n_user)
7677

@@ -83,6 +84,7 @@ async def _trigger_thread_steps(
8384
n_facts: int,
8485
n_summary: int,
8586
n_dedup_turns: int,
87+
thresholds: Any = None,
8688
) -> dict[str, int]:
8789
user_batch_counts: dict[str, int] = {}
8890
for (user_id, thread_id), batch_count in turn_counts.items():
@@ -111,6 +113,7 @@ async def _trigger_thread_steps(
111113
fire_extract=n_facts > 0 and _counters.crosses_threshold(old_count, new_count, n_facts),
112114
fire_summary=n_summary > 0 and _counters.crosses_threshold(old_count, new_count, n_summary),
113115
fire_dedup=n_dedup_turns > 0 and _counters.crosses_threshold(old_count, new_count, n_dedup_turns),
116+
thresholds=thresholds,
114117
)
115118
return user_batch_counts
116119

@@ -125,10 +128,11 @@ async def _fire_thread_steps(
125128
fire_extract: bool,
126129
fire_summary: bool,
127130
fire_dedup: bool,
131+
thresholds: Any = None,
128132
) -> None:
129133
fire_procedural = fire_dedup and bool(
130134
_threshold_value(
131-
default_thresholds,
135+
thresholds,
132136
"get_procedural_synthesis_auto",
133137
"PROCEDURAL_SYNTHESIS_AUTO",
134138
default=True,

agent_memory_toolkit/aio/chat.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ def _build_kwargs(
136136
self,
137137
messages: list[dict[str, str]],
138138
*,
139-
temperature: float | None = None,
140139
response_format: dict | None = None,
141140
**extra: Any,
142141
) -> dict[str, Any]:
@@ -156,7 +155,6 @@ async def generate(
156155
self,
157156
messages: list[dict[str, str]],
158157
*,
159-
temperature: float | None = None,
160158
response_format: dict | None = None,
161159
max_retries: int = 3,
162160
base_delay: float = 2.0,
@@ -185,7 +183,6 @@ async def generate(
185183
client = self._ensure_client()
186184
kwargs = self._build_kwargs(
187185
messages,
188-
temperature=temperature,
189186
response_format=response_format,
190187
**extra,
191188
)

agent_memory_toolkit/aio/services/pipeline.py

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from datetime import datetime, timezone
1919
from typing import Any, Literal, Optional
2020

21-
from azure.cosmos.exceptions import CosmosResourceExistsError
21+
from azure.cosmos.exceptions import CosmosResourceExistsError, CosmosResourceNotFoundError
2222

2323
from agent_memory_toolkit._utils import DEFAULT_TTL_BY_TYPE, compute_content_hash
2424
from agent_memory_toolkit.aio.store import AsyncMemoryStore
@@ -672,7 +672,7 @@ async def synthesize_procedural(
672672
"AND ((IS_DEFINED(c.metadata.category) "
673673
"AND c.metadata.category IN ('preference', 'requirement')) "
674674
"OR (IS_DEFINED(c.salience) AND c.salience >= @min_salience)) "
675-
"ORDER BY c.salience DESC"
675+
"ORDER BY c.salience DESC, c.created_at ASC, c.id ASC"
676676
),
677677
parameters=[
678678
{"name": "@uid", "value": user_id},
@@ -681,14 +681,6 @@ async def synthesize_procedural(
681681
],
682682
enable_cross_partition_query=True,
683683
)
684-
behavioral_fact_docs.sort(
685-
key=lambda doc: (
686-
float(doc.get("salience") or 0.0),
687-
doc.get("created_at", ""),
688-
doc.get("id", ""),
689-
),
690-
reverse=True,
691-
)
692684
behavioral_fact_docs = [
693685
doc
694686
for doc in behavioral_fact_docs
@@ -703,22 +695,14 @@ async def synthesize_procedural(
703695
f"AND {active_filter} "
704696
"AND IS_DEFINED(c.metadata.lesson) "
705697
"AND c.metadata.lesson != null "
706-
"ORDER BY c.salience DESC"
698+
"ORDER BY c.salience DESC, c.created_at ASC, c.id ASC"
707699
),
708700
parameters=[
709701
{"name": "@uid", "value": user_id},
710702
{"name": "@type", "value": "episodic"},
711703
],
712704
enable_cross_partition_query=True,
713705
)
714-
episodic_docs.sort(
715-
key=lambda doc: (
716-
float(doc.get("salience") or 0.0),
717-
doc.get("created_at", ""),
718-
doc.get("id", ""),
719-
),
720-
reverse=True,
721-
)
722706
episodic_with_lessons = [
723707
doc
724708
for doc in episodic_docs
@@ -742,6 +726,13 @@ async def synthesize_procedural(
742726
)
743727
return {"status": "unchanged", "procedural": prior_doc}
744728

729+
if not current_source_ids:
730+
logger.info(
731+
"synthesize_procedural skipping LLM user_id=%s — no behavioral facts or episodic lessons",
732+
user_id,
733+
)
734+
return {"status": "unchanged", "procedural": prior_doc}
735+
745736
name_docs = await self._container.query_items(
746737
query=(
747738
"SELECT TOP 1 * FROM c WHERE c.user_id = @uid "
@@ -854,7 +845,7 @@ async def generate_thread_summary_dry(
854845
existing_summary: Optional[dict[str, Any]] = None
855846
try:
856847
existing_summary = await self._container.read_item(item=summary_id, partition_key=[user_id, thread_id])
857-
except Exception:
848+
except CosmosResourceNotFoundError:
858849
pass
859850

860851
query = "SELECT * FROM c WHERE c.user_id = @user_id AND c.thread_id = @thread_id AND c.type != 'summary'"
@@ -995,7 +986,7 @@ async def generate_user_summary_dry(
995986
item=user_summary_id,
996987
partition_key=[user_id, "__user_summary__"],
997988
)
998-
except Exception:
989+
except CosmosResourceNotFoundError:
999990
pass
1000991

1001992
query = "SELECT * FROM c WHERE c.user_id = @user_id AND c.type != 'user_summary'"

agent_memory_toolkit/aio/store/memory_store.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -339,14 +339,22 @@ async def get_memories(
339339
query = f"SELECT * FROM c{where}"
340340

341341
logger.debug("async get_memories query: %s", query)
342-
results = await self._query_items(
343-
query=query,
344-
parameters=parameters or None,
345-
operation="async get_memories query",
346-
container=self._container_for_query(memory_types),
347-
)
342+
# Iterate over all containers that may hold the requested memory types
343+
# so mixed turn + non-turn queries do not silently drop one container.
344+
results: list[dict] = []
345+
for container in self._containers_for_query(memory_types):
346+
results.extend(
347+
await self._query_items(
348+
query=query,
349+
parameters=parameters or None,
350+
operation="async get_memories query",
351+
container=container,
352+
)
353+
)
348354

349355
if recent_k is not None:
356+
results.sort(key=lambda i: i.get("_ts") or 0, reverse=True)
357+
results = results[:recent_k]
350358
results.reverse()
351359
if min_salience is not None:
352360
results = [i for i in results if (i.get("salience") or 0.0) >= min_salience]
@@ -501,13 +509,16 @@ async def list_tags(
501509
thread_id: Optional[str] = None,
502510
prefix: Optional[str] = None,
503511
include_sys: bool = False,
512+
include_superseded: bool = False,
504513
) -> list[str]:
505514
"""Return sorted distinct tags for a user, optionally scoped to one thread."""
506515
query = "SELECT VALUE c.tags FROM c WHERE c.user_id = @user_id AND ARRAY_LENGTH(c.tags) > 0"
507516
parameters = [{"name": "@user_id", "value": user_id}]
508517
if thread_id is not None:
509518
query += " AND c.thread_id = @thread_id"
510519
parameters.append({"name": "@thread_id", "value": thread_id})
520+
if not include_superseded:
521+
query += " AND (NOT IS_DEFINED(c.superseded_by) OR IS_NULL(c.superseded_by))"
511522

512523
prefix_norm = prefix.strip().lower() if prefix else None
513524
partition_key, cross_partition = query_scope(user_id, thread_id)

agent_memory_toolkit/auto_trigger.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def maybe_trigger_steps(
115115
n_facts=n_facts,
116116
n_summary=n_summary,
117117
n_dedup_turns=n_dedup_turns,
118+
thresholds=thresholds,
118119
)
119120
_trigger_user_steps(processor, counter_container, user_batch_counts, n_user=n_user)
120121

@@ -127,6 +128,7 @@ def _trigger_thread_steps(
127128
n_facts: int,
128129
n_summary: int,
129130
n_dedup_turns: int,
131+
thresholds: Any = None,
130132
) -> dict[str, int]:
131133
user_batch_counts: dict[str, int] = {}
132134
for (user_id, thread_id), batch_count in turn_counts.items():
@@ -155,6 +157,7 @@ def _trigger_thread_steps(
155157
fire_extract=n_facts > 0 and _counters.crosses_threshold(old_count, new_count, n_facts),
156158
fire_summary=n_summary > 0 and _counters.crosses_threshold(old_count, new_count, n_summary),
157159
fire_dedup=n_dedup_turns > 0 and _counters.crosses_threshold(old_count, new_count, n_dedup_turns),
160+
thresholds=thresholds,
158161
)
159162
return user_batch_counts
160163

@@ -169,10 +172,11 @@ def _fire_thread_steps(
169172
fire_extract: bool,
170173
fire_summary: bool,
171174
fire_dedup: bool,
175+
thresholds: Any = None,
172176
) -> None:
173177
fire_procedural = fire_dedup and bool(
174178
_threshold_value(
175-
default_thresholds,
179+
thresholds,
176180
"get_procedural_synthesis_auto",
177181
"PROCEDURAL_SYNTHESIS_AUTO",
178182
default=True,

agent_memory_toolkit/chat.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ def _build_kwargs(
144144
self,
145145
messages: list[dict[str, str]],
146146
*,
147-
temperature: float | None = None,
148147
response_format: dict | None = None,
149148
**extra: Any,
150149
) -> dict[str, Any]:
@@ -170,7 +169,6 @@ def generate(
170169
self,
171170
messages: list[dict[str, str]],
172171
*,
173-
temperature: float | None = None,
174172
response_format: dict | None = None,
175173
max_retries: int = 3,
176174
base_delay: float = 2.0,
@@ -201,7 +199,6 @@ def generate(
201199
client = self._ensure_client()
202200
kwargs = self._build_kwargs(
203201
messages,
204-
temperature=temperature,
205202
response_format=response_format,
206203
**extra,
207204
)

agent_memory_toolkit/prompts/dedup.prompty

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ N. ID: <uuid> | Content: "<text>" | Confidence: 0.85 | Salience: 0.7 | Created:
100100

101101
Some facts may show `Confidence: N/A`, `Salience: N/A`, or `Created: N/A`.
102102

103-
- Treat `N/A` confidence/salience as **unknown** — omit those fields from the
104-
`duplicate_groups` output; the pipeline will fall back to
105-
`max(source.confidence)` / `max(source.salience)`.
103+
- Treat `N/A` confidence/salience as **unknown** — set those fields to `null`
104+
in the `duplicate_groups` output (do NOT omit the keys); the pipeline will
105+
fall back to `max(source.confidence)` / `max(source.salience)`.
106106
- Treat `N/A` created_at as **older than any real timestamp** for
107107
winner-selection purposes (a fact with a real date beats one with N/A).
108108

@@ -121,9 +121,9 @@ You must output ONLY valid JSON matching this exact schema. No preamble, no expl
121121
"kept_ids": ["<id>", "<id>"]
122122
}
123123
```
124-
<!-- confidence/salience: float 0..1, or omit the field if unknown -->
124+
<!-- confidence/salience: float 0..1, or null if you cannot infer -->
125125

126-
The `confidence` and `salience` values **must** be real numbers between 0 and 1 in the actual output — the `0.0` above is a structural placeholder, not a literal value to echo. Compute them as the maximum across source facts in the group. If you cannot determine confidence or salience, omit the field entirely (the pipeline will fall back to `max(source.*)` from the source records).
126+
The `confidence` and `salience` values **must** be real numbers between 0 and 1 in the actual output — the `0.0` above is a structural placeholder, not a literal value to echo. Compute them as the maximum across source facts in the group. If you cannot determine confidence or salience, set the field to `null` (the pipeline will fall back to `max(source.*)` from the source records). **Do not omit the keys** — the response schema requires both fields to appear on every group, with either a number or `null` as the value.
127127

128128
If a bucket is empty, emit it as an empty array (`[]`) rather than omitting the key.
129129

agent_memory_toolkit/services/pipeline.py

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from datetime import datetime, timezone
1717
from typing import Any, Literal, Optional
1818

19-
from azure.cosmos.exceptions import CosmosResourceExistsError
19+
from azure.cosmos.exceptions import CosmosResourceExistsError, CosmosResourceNotFoundError
2020

2121
from agent_memory_toolkit._utils import DEFAULT_TTL_BY_TYPE, compute_content_hash
2222
from agent_memory_toolkit.exceptions import (
@@ -716,7 +716,7 @@ def synthesize_procedural(
716716
"AND ((IS_DEFINED(c.metadata.category) "
717717
"AND c.metadata.category IN ('preference', 'requirement')) "
718718
"OR (IS_DEFINED(c.salience) AND c.salience >= @min_salience)) "
719-
"ORDER BY c.salience DESC"
719+
"ORDER BY c.salience DESC, c.created_at ASC, c.id ASC"
720720
),
721721
parameters=[
722722
{"name": "@uid", "value": user_id},
@@ -726,14 +726,6 @@ def synthesize_procedural(
726726
enable_cross_partition_query=True,
727727
)
728728
)
729-
behavioral_fact_docs.sort(
730-
key=lambda doc: (
731-
float(doc.get("salience") or 0.0),
732-
doc.get("created_at", ""),
733-
doc.get("id", ""),
734-
),
735-
reverse=True,
736-
)
737729
behavioral_fact_docs = [
738730
doc
739731
for doc in behavioral_fact_docs
@@ -749,7 +741,7 @@ def synthesize_procedural(
749741
f"AND {active_filter} "
750742
"AND IS_DEFINED(c.metadata.lesson) "
751743
"AND c.metadata.lesson != null "
752-
"ORDER BY c.salience DESC"
744+
"ORDER BY c.salience DESC, c.created_at ASC, c.id ASC"
753745
),
754746
parameters=[
755747
{"name": "@uid", "value": user_id},
@@ -758,14 +750,6 @@ def synthesize_procedural(
758750
enable_cross_partition_query=True,
759751
)
760752
)
761-
episodic_docs.sort(
762-
key=lambda doc: (
763-
float(doc.get("salience") or 0.0),
764-
doc.get("created_at", ""),
765-
doc.get("id", ""),
766-
),
767-
reverse=True,
768-
)
769753
episodic_with_lessons = [
770754
doc
771755
for doc in episodic_docs
@@ -789,6 +773,13 @@ def synthesize_procedural(
789773
)
790774
return {"status": "unchanged", "procedural": prior_doc}
791775

776+
if not current_source_ids:
777+
logger.info(
778+
"synthesize_procedural skipping LLM user_id=%s — no behavioral facts or episodic lessons",
779+
user_id,
780+
)
781+
return {"status": "unchanged", "procedural": prior_doc}
782+
792783
name_docs = list(
793784
self._container.query_items(
794785
query=(
@@ -906,7 +897,7 @@ def generate_thread_summary_dry(
906897
existing_summary: Optional[dict[str, Any]] = None
907898
try:
908899
existing_summary = self._container.read_item(item=summary_id, partition_key=[user_id, thread_id])
909-
except Exception:
900+
except CosmosResourceNotFoundError:
910901
pass
911902

912903
query = "SELECT * FROM c WHERE c.user_id = @user_id AND c.thread_id = @thread_id AND c.type != 'summary'"
@@ -1052,7 +1043,7 @@ def generate_user_summary_dry(
10521043
item=user_summary_id,
10531044
partition_key=[user_id, "__user_summary__"],
10541045
)
1055-
except Exception:
1046+
except CosmosResourceNotFoundError:
10561047
pass
10571048

10581049
query = "SELECT * FROM c WHERE c.user_id = @user_id AND c.type != 'user_summary'"

0 commit comments

Comments
 (0)