Skip to content

Commit dde0f8c

Browse files
Aayush KatariaAayush Kataria
authored andcommitted
improvements to tagging, filtering
1 parent 1360c26 commit dde0f8c

46 files changed

Lines changed: 2697 additions & 1009 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Docs/architecture/orchestrators.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Durable orchestrator activity chains
2+
3+
W15 splits LLM extraction from persistence so Durable retries after a Cosmos write failure do not re-run the LLM activity.
4+
5+
```text
6+
ExtractMemoriesOrchestrator
7+
em_Extract (load recent turns + LLM + parse; no embeddings/writes)
8+
em_Persist (embeddings + deterministic create_item; 409 = already persisted)
9+
em_ReconcileMemories (optional; single activity for GA)
10+
11+
ThreadSummaryOrchestrator
12+
ts_Extract
13+
ts_PersistSummary
14+
15+
UserSummaryOrchestrator
16+
us_Extract
17+
us_PersistUserSummary
18+
19+
SynthesizeProceduralOrchestrator
20+
sp_SynthesizeProcedural (single activity for GA)
21+
```
22+
23+
Fact and episodic IDs are deterministic from user, thread, and normalized content. Thread and user summaries keep their deterministic summary IDs.

Docs/operations.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Operations
2+
3+
## Memory lifecycle (TTL)
4+
5+
| Type | Default TTL | Source |
6+
|---|---:|---|
7+
| turn | 30 d | container default (memories_turns) |
8+
| episodic | 90 d | per-doc ttl (memories container) |
9+
| thread_summary | never | container default (memories, -1) |
10+
| user_summary | never | container default |
11+
| fact | never | container default; supersession handles aging |
12+
| procedural | never | container default; supersession handles aging |
13+
14+
Override per write:
15+
client.add_memory(text, type="turn", ttl=60) # expires in 60 seconds
16+
17+
Override per container at provision time:
18+
azd env set MEMORIES_TURNS_DEFAULT_TTL 86400 # 1 day

Docs/public_api.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727
- `delete_local(memory_id) -> None` — remove a local buffered memory.
2828
- `add_cosmos(user_id, role, content, memory_type='turn', metadata=None, thread_id=None, tags=None, ttl=None, salience=None, embedding=None, embed=None) -> str` — upsert one memory to Cosmos and return its id.
2929
- `push_to_cosmos(batch_size=25) -> None` — flush local buffered memories to Cosmos.
30-
- `get_memories(memory_id=None, user_id=None, thread_id=None, role=None, memory_types=None, recent_k=None, tags=None, any_tags=None, exclude_tags=None, include_superseded=False, min_salience=None, min_confidence=None) -> list[dict]` — retrieve memories with filters.
30+
- `get_memories(memory_id=None, user_id=None, thread_id=None, role=None, memory_types=None, recent_k=None, tags_all=None, tags_any=None, exclude_tags=None, include_superseded=False, min_salience=None, min_confidence=None, created_after=None, created_before=None) -> list[dict]` — retrieve memories with filters.
3131
- `update_cosmos(memory_id, content=None, role=None, memory_type=None, metadata=None) -> None` — update a Cosmos memory.
3232
- `delete_cosmos(memory_id, thread_id, user_id) -> None` — delete a Cosmos memory.
33-
- `get_thread(thread_id, user_id=None, memory_types=None, recent_k=None, tags=None, exclude_tags=None, include_superseded=False) -> list[dict]` — retrieve a thread oldest-first.
33+
- `get_thread(thread_id, user_id=None, memory_types=None, recent_k=None, tags_all=None, tags_any=None, exclude_tags=None, include_superseded=False, created_after=None, created_before=None) -> list[dict]` — retrieve a thread oldest-first.
3434
- `get_user_summary(user_id) -> Optional[dict]` — retrieve the active user-summary document.
3535

3636
### Retrieval
3737

38-
- `search_cosmos(search_terms, memory_id=None, user_id=None, role=None, memory_types=None, thread_id=None, hybrid_search=False, top_k=5, tags=None, any_tags=None, exclude_tags=None, include_superseded=False, min_salience=None, min_confidence=None) -> list[dict]` — vector or hybrid search memories.
38+
- `search_cosmos(search_terms, memory_id=None, user_id=None, role=None, memory_types=None, thread_id=None, hybrid_search=False, top_k=5, tags_all=None, tags_any=None, exclude_tags=None, include_superseded=False, min_salience=None, min_confidence=None, created_after=None, created_before=None) -> list[dict]` — vector or hybrid search memories.
3939
- `get_procedural_prompt(user_id) -> Optional[str]` — read the active procedural prompt.
4040
- `get_procedural_history(user_id, limit=10) -> list[dict]` — read procedural prompt history.
4141
- `get_procedural_memories(user_id, priority=None, category=None, min_salience=None, include_superseded=False) -> list[dict]` — retrieve procedural memory documents.
@@ -57,6 +57,7 @@
5757

5858
- `add_tags(memory_id, user_id, thread_id, tags) -> None` — add tags to a memory.
5959
- `remove_tags(memory_id, user_id, thread_id, tags) -> None` — remove tags from a memory.
60+
- `list_tags(user_id, *, thread_id=None, prefix=None, include_sys=False) -> list[str]` — list sorted, deduped tags for a user; omits `sys:*` by default.
6061

6162
## AsyncCosmosMemoryClient
6263

@@ -77,15 +78,15 @@ Local-buffer methods remain synchronous in-memory operations; Cosmos, retrieval,
7778
- `delete_local(memory_id) -> None` — remove a local buffered memory.
7879
- `async add_cosmos(user_id, role, content, memory_type='turn', metadata=None, thread_id=None, tags=None, ttl=None, salience=None, embedding=None, embed=None) -> str` — upsert one memory to Cosmos and return its id.
7980
- `async push_to_cosmos(batch_size=25) -> None` — flush local buffered memories to Cosmos.
80-
- `async get_memories(memory_id=None, user_id=None, thread_id=None, role=None, memory_types=None, recent_k=None, tags=None, any_tags=None, exclude_tags=None, include_superseded=False, min_salience=None, min_confidence=None) -> list[dict]` — retrieve memories with filters.
81+
- `async get_memories(memory_id=None, user_id=None, thread_id=None, role=None, memory_types=None, recent_k=None, tags_all=None, tags_any=None, exclude_tags=None, include_superseded=False, min_salience=None, min_confidence=None, created_after=None, created_before=None) -> list[dict]` — retrieve memories with filters.
8182
- `async update_cosmos(memory_id, content=None, role=None, memory_type=None, metadata=None) -> None` — update a Cosmos memory.
8283
- `async delete_cosmos(memory_id, thread_id, user_id) -> None` — delete a Cosmos memory.
83-
- `async get_thread(thread_id, user_id=None, memory_types=None, recent_k=None, tags=None, exclude_tags=None, include_superseded=False) -> list[dict]` — retrieve a thread oldest-first.
84+
- `async get_thread(thread_id, user_id=None, memory_types=None, recent_k=None, tags_all=None, tags_any=None, exclude_tags=None, include_superseded=False, created_after=None, created_before=None) -> list[dict]` — retrieve a thread oldest-first.
8485
- `async get_user_summary(user_id) -> Optional[dict]` — retrieve the active user-summary document.
8586

8687
### Retrieval
8788

88-
- `async search_cosmos(search_terms, memory_id=None, user_id=None, role=None, memory_types=None, thread_id=None, hybrid_search=False, top_k=5, tags=None, any_tags=None, exclude_tags=None, include_superseded=False, min_salience=None, min_confidence=None) -> list[dict]` — vector or hybrid search memories.
89+
- `async search_cosmos(search_terms, memory_id=None, user_id=None, role=None, memory_types=None, thread_id=None, hybrid_search=False, top_k=5, tags_all=None, tags_any=None, exclude_tags=None, include_superseded=False, min_salience=None, min_confidence=None, created_after=None, created_before=None) -> list[dict]` — vector or hybrid search memories.
8990
- `async get_procedural_prompt(user_id) -> Optional[str]` — read the active procedural prompt.
9091
- `async get_procedural_history(user_id, limit=10) -> list[dict]` — read procedural prompt history.
9192
- `async get_procedural_memories(user_id, priority=None, category=None, min_salience=None, include_superseded=False) -> list[dict]` — retrieve procedural memory documents.
@@ -107,6 +108,7 @@ Local-buffer methods remain synchronous in-memory operations; Cosmos, retrieval,
107108

108109
- `async add_tags(memory_id, user_id, thread_id, tags) -> None` — add tags to a memory.
109110
- `async remove_tags(memory_id, user_id, thread_id, tags) -> None` — remove tags from a memory.
111+
- `async list_tags(user_id, *, thread_id=None, prefix=None, include_sys=False) -> list[str]` — list sorted, deduped tags for a user; omits `sys:*` by default.
110112

111113
## Extension Points
112114

Samples/Scenarios/scenario_tagging_and_filtering.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
feature. Tags are arbitrary string labels attached to a memory at write
55
time; at read time you can filter retrieval with three composable predicates:
66
7-
* ``tags=[...]`` — AND filter: every listed tag must be present
8-
* ``any_tags=[...]`` — OR filter: at least one listed tag must be present
7+
* ``tags_all=[...]`` — AND filter: every listed tag must be present
8+
* ``tags_any=[...]`` — OR filter: at least one listed tag must be present
99
* ``exclude_tags=[...]`` — NOT filter: none of these tags may be present
1010
1111
Tags are stored in Cosmos as a JSON array on the memory document. They work
@@ -90,24 +90,24 @@ def main() -> None:
9090
print(f" + {mem_type:>10} tags={tags}")
9191

9292
# ------------------------------------------------------------------
93-
# 2. AND filter: tags=[...] — every listed tag must be present
93+
# 2. AND filter: tags_all=[...] — every listed tag must be present
9494
# Use get_memories() for filter-only retrieval (no vector search).
9595
# ------------------------------------------------------------------
96-
print_section("AND filter — tags=['workflow', 'important']")
96+
print_section("AND filter — tags_all=['workflow', 'important']")
9797
results = client.get_memories(
9898
user_id=user_id,
99-
tags=["workflow", "important"],
99+
tags_all=["workflow", "important"],
100100
)
101101
print_results(results)
102102
# Expected: only the "Always confirm before purging" procedural memory.
103103

104104
# ------------------------------------------------------------------
105-
# 3. OR filter: any_tags=[...] — any listed tag may match
105+
# 3. OR filter: tags_any=[...] — any listed tag may match
106106
# ------------------------------------------------------------------
107-
print_section("OR filter — any_tags=['diet', 'travel']")
107+
print_section("OR filter — tags_any=['diet', 'travel']")
108108
results = client.get_memories(
109109
user_id=user_id,
110-
any_tags=["diet", "travel"],
110+
tags_any=["diet", "travel"],
111111
)
112112
print_results(results)
113113
# Expected: the peanut allergy fact and the PyCon episodic memory.
@@ -129,11 +129,11 @@ def main() -> None:
129129
# search_cosmos() runs vector similarity against search_terms and
130130
# composes naturally with structural / tag filters.
131131
# ------------------------------------------------------------------
132-
print_section("Semantic + tag filter — search_terms='shipping a service' tags=['work']")
132+
print_section("Semantic + tag filter — search_terms='shipping a service' tags_all=['work']")
133133
results = client.search_cosmos(
134134
search_terms="shipping a service",
135135
user_id=user_id,
136-
tags=["work"],
136+
tags_all=["work"],
137137
top_k=5,
138138
)
139139
print_results(results)
@@ -143,10 +143,10 @@ def main() -> None:
143143
# ------------------------------------------------------------------
144144
# 6. Compose AND + NOT filters across types
145145
# ------------------------------------------------------------------
146-
print_section("AND + NOT — tags=['preference'] exclude_tags=['health']")
146+
print_section("AND + NOT — tags_all=['preference'] exclude_tags=['health']")
147147
results = client.get_memories(
148148
user_id=user_id,
149-
tags=["preference"],
149+
tags_all=["preference"],
150150
exclude_tags=["health"],
151151
)
152152
print_results(results)

agent_memory_toolkit/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
CosmosNotConnectedError,
1010
CosmosOperationError,
1111
LLMError,
12+
MemoryConflictError,
1213
MemoryNotFoundError,
1314
ValidationError,
1415
)
@@ -50,6 +51,7 @@
5051
"CosmosNotConnectedError",
5152
"CosmosOperationError",
5253
"LLMError",
54+
"MemoryConflictError",
5355
"MemoryNotFoundError",
5456
"ValidationError",
5557
"DEFAULT_FACT_EXTRACTION_EVERY_N",

agent_memory_toolkit/_query_builder.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,31 @@ def add_gte(self, field: str, param_name: str, value: Any) -> None:
7777
self._conditions.append(f"{field} >= {param_name}")
7878
self._parameters.append({"name": param_name, "value": value})
7979

80+
def add_time_range(
81+
self,
82+
field: str,
83+
*,
84+
after: Any = None,
85+
before: Any = None,
86+
after_param: str = "@after",
87+
before_param: str = "@before",
88+
) -> None:
89+
"""Add inclusive lower/upper bounds for an ISO-sortable time field."""
90+
if after is not None:
91+
self._conditions.append(f"{field} >= {after_param}")
92+
self._parameters.append({"name": after_param, "value": after})
93+
if before is not None:
94+
self._conditions.append(f"{field} <= {before_param}")
95+
self._parameters.append({"name": before_param, "value": before})
96+
97+
def add_metadata_filter(self, path: str, op: str, value: Any, *, param_name: str | None = None) -> None:
98+
"""Add a parameterized comparison filter for a metadata path."""
99+
if op not in {"=", "!=", ">", "<", ">=", "<="}:
100+
raise ValueError(f"unsupported op: {op}")
101+
pname = param_name or f"@m_{len(self._parameters)}"
102+
self._conditions.append(f"{path} {op} {pname}")
103+
self._parameters.append({"name": pname, "value": value})
104+
80105
def build_where(self) -> str:
81106
"""Return the ``WHERE …`` clause (or empty string if no filters)."""
82107
if not self._conditions:

agent_memory_toolkit/_utils.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from ._query_builder import _QueryBuilder
1717
from .exceptions import ConfigurationError, ValidationError
18+
from .thresholds import DEFAULT_TTL_BY_TYPE as DEFAULT_TTL_BY_TYPE, default_ttl_for
1819

1920
VALID_ROLES = {"agent", "user", "tool", "system"}
2021
VALID_TYPES = {"turn", "summary", "fact", "user_summary", "procedural", "episodic"}
@@ -57,17 +58,14 @@ def new_user_summary_id() -> str:
5758
"""Return a fresh ``user_summary_*`` id."""
5859
return new_id("user_summary")
5960

60-
DEFAULT_TTL_BY_TYPE: dict[str, int | None] = {
61-
"turn": 2_592_000, # 30 days
62-
"summary": None,
63-
"fact": None,
64-
"user_summary": None,
65-
"procedural": None,
66-
"episodic": 7_776_000, # 90 days
67-
}
61+
_WHITESPACE_RE = re.compile(r"\s+")
6862

6963

70-
_WHITESPACE_RE = re.compile(r"\s+")
64+
def _coerce_datetime_iso(value: Optional[str | datetime]) -> Optional[str]:
65+
"""Return ISO text for datetime values while leaving strings unchanged."""
66+
if isinstance(value, datetime):
67+
return value.isoformat()
68+
return value
7169

7270

7371
def _normalize_for_hash(text: str) -> str:
@@ -114,7 +112,7 @@ def _make_memory(
114112
raise ValidationError(f"type must be one of {VALID_TYPES}, got '{memory_type}'")
115113

116114
if ttl is None:
117-
ttl = DEFAULT_TTL_BY_TYPE.get(memory_type)
115+
ttl = default_ttl_for(memory_type)
118116

119117
memory: dict[str, Any] = {
120118
"id": memory_id or str(uuid.uuid4()),

0 commit comments

Comments
 (0)