Skip to content

Commit 500fe82

Browse files
Aayush KatariaAayush Kataria
authored andcommitted
Resolving comments
1 parent 0821282 commit 500fe82

9 files changed

Lines changed: 274 additions & 25 deletions

File tree

azure/cosmos/agent_memory/aio/cosmos_memory_client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from azure.cosmos.agent_memory.aio.store import AsyncMemoryStore
2828
from azure.cosmos.agent_memory.exceptions import CosmosNotConnectedError, CosmosOperationError
2929
from azure.cosmos.agent_memory.logging import get_logger
30+
from azure.cosmos.agent_memory.services._pipeline_helpers import _normalize_metadata_keys
3031
from azure.cosmos.agent_memory.thresholds import DEFAULT_TTL_BY_TYPE
3132

3233
if TYPE_CHECKING: # pragma: no cover - typing-only import
@@ -136,9 +137,7 @@ def __init__(
136137
self._pipeline: Optional[AsyncPipelineService] = None
137138
self._processor: Optional[AsyncMemoryProcessor] = processor
138139
self._processor_explicit = processor is not None
139-
self._transcript_metadata_keys: Optional[tuple[str, ...]] = (
140-
tuple(transcript_metadata_keys) if transcript_metadata_keys else None
141-
)
140+
self._transcript_metadata_keys: Optional[tuple[str, ...]] = _normalize_metadata_keys(transcript_metadata_keys)
142141
logger.info("AsyncCosmosMemoryClient initialized")
143142

144143
async def __aenter__(self) -> "AsyncCosmosMemoryClient":

azure/cosmos/agent_memory/aio/services/pipeline.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import time
1717
from collections import defaultdict
1818
from datetime import datetime, timezone
19-
from typing import Any, Literal, Optional
19+
from typing import Any, Iterable, Literal, Optional
2020

2121
from azure.cosmos.exceptions import (
2222
CosmosHttpResponseError,
@@ -48,6 +48,7 @@
4848
from azure.cosmos.agent_memory.services._pipeline_helpers import (
4949
VALID_VALENCES,
5050
PromptyLoader,
51+
_normalize_metadata_keys,
5152
build_topic_tags,
5253
build_transcript,
5354
cap_structured_summary,
@@ -176,7 +177,7 @@ def __init__(
176177
prompts_dir: str | None = None,
177178
*,
178179
containers: dict[ContainerKey, Any],
179-
transcript_metadata_keys: Optional[tuple[str, ...]] = None,
180+
transcript_metadata_keys: Optional[Iterable[str]] = None,
180181
) -> None:
181182
self._store = store
182183
self._containers = containers
@@ -187,9 +188,7 @@ def __init__(
187188
self._chat_client = chat_client
188189
self._embeddings = embeddings_client
189190
self._prompty = PromptyLoader(prompts_dir)
190-
self._transcript_metadata_keys: Optional[tuple[str, ...]] = (
191-
tuple(transcript_metadata_keys) if transcript_metadata_keys else None
192-
)
191+
self._transcript_metadata_keys: Optional[tuple[str, ...]] = _normalize_metadata_keys(transcript_metadata_keys)
193192

194193
async def _query_items(self, container: Any, **kwargs: Any) -> list[dict[str, Any]]:
195194
result = container.query_items(**kwargs)
@@ -271,6 +270,8 @@ def _build_transcript(
271270
*,
272271
group_by_thread: bool = False,
273272
) -> str:
273+
# getattr fallback covers unit tests that build AsyncPipelineService
274+
# via __new__ to bypass __init__ (and therefore the metadata-keys stash).
274275
return build_transcript(
275276
items,
276277
group_by_thread=group_by_thread,

azure/cosmos/agent_memory/cosmos_memory_client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .embeddings import EmbeddingsClient
2626
from .exceptions import CosmosOperationError
2727
from .processors import InProcessProcessor, MemoryProcessor
28+
from .services._pipeline_helpers import _normalize_metadata_keys
2829
from .services.pipeline import PipelineService
2930
from .store import MemoryStore
3031
from .thresholds import DEFAULT_TTL_BY_TYPE
@@ -128,9 +129,7 @@ def __init__(
128129
self._pipeline: Optional[PipelineService] = None
129130
self._processor: Optional[MemoryProcessor] = processor
130131
self._processor_explicit = processor is not None
131-
self._transcript_metadata_keys: Optional[tuple[str, ...]] = (
132-
tuple(transcript_metadata_keys) if transcript_metadata_keys else None
133-
)
132+
self._transcript_metadata_keys: Optional[tuple[str, ...]] = _normalize_metadata_keys(transcript_metadata_keys)
134133
if self._cosmos_endpoint:
135134
self.create_memory_store()
136135
logger.info("CosmosMemoryClient initialized")

azure/cosmos/agent_memory/services/_pipeline_helpers.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,34 @@ def extract_prompty_params(p: Any) -> dict[str, Any]:
181181
return params
182182

183183

184+
def _normalize_metadata_keys(
185+
value: Optional[Iterable[str]],
186+
) -> Optional[tuple[str, ...]]:
187+
"""Validate + coerce a ``transcript_metadata_keys`` argument to a tuple.
188+
189+
Rejects ``str`` outright (a bare string is iterable char-by-char, which
190+
would silently produce a one-letter allow-list). Returns ``None`` for
191+
empty or missing input.
192+
"""
193+
if value is None:
194+
return None
195+
if isinstance(value, str):
196+
raise TypeError(
197+
"transcript_metadata_keys must be a sequence of keys "
198+
"(list/tuple/set), not a single str. "
199+
f"Got: {value!r}. Did you mean [{value!r}]?"
200+
)
201+
keys = tuple(str(k) for k in value if str(k))
202+
return keys or None
203+
204+
184205
def _format_metadata_segment(
185206
metadata: Any,
186-
metadata_keys: Optional[Iterable[str]],
207+
metadata_keys: Optional[tuple[str, ...]],
187208
) -> str:
188209
"""Render the trailing ``[metadata: {...}]`` segment for a transcript line.
189210
190-
Returns an empty string unless ``metadata_keys`` is a non-empty iterable
211+
Returns an empty string unless ``metadata_keys`` is a non-empty tuple
191212
AND at least one of those keys is present in ``metadata``. Only the
192213
explicitly allow-listed keys are serialized, in the iteration order of
193214
``metadata_keys``.
@@ -197,7 +218,8 @@ def _format_metadata_segment(
197218
filtered = {k: metadata[k] for k in metadata_keys if k in metadata}
198219
if not filtered:
199220
return ""
200-
return f" [metadata: {json.dumps(filtered)}]"
221+
payload = json.dumps(filtered, separators=(",", ":"), ensure_ascii=False, default=str)
222+
return f" [metadata: {payload}]"
201223

202224

203225
def build_transcript(
@@ -227,13 +249,18 @@ def build_transcript(
227249
metadata blobs (raw tool calls, IDE schema, etc.) out of every
228250
prompt — they're often 10-100x larger than the dialog itself and
229251
dilute extraction quality.
252+
253+
Accepts any iterable of strings except ``str`` itself (which would
254+
be interpreted char-by-char). Generators are coerced to a tuple so
255+
the allow-list is reusable across turns.
230256
"""
257+
keys = _normalize_metadata_keys(metadata_keys)
231258
if not group_by_thread:
232259
lines: list[str] = []
233260
for m in items:
234261
role = m.get("role", "unknown")
235262
content = m.get("content", "")
236-
meta_str = _format_metadata_segment(m.get("metadata", {}), metadata_keys)
263+
meta_str = _format_metadata_segment(m.get("metadata", {}), keys)
237264
lines.append(f"[{role}]: {content}{meta_str}")
238265
return "\n".join(lines)
239266

@@ -247,7 +274,7 @@ def build_transcript(
247274
for m in thread_items:
248275
role = m.get("role", "unknown")
249276
content = m.get("content", "")
250-
meta_str = _format_metadata_segment(m.get("metadata", {}), metadata_keys)
277+
meta_str = _format_metadata_segment(m.get("metadata", {}), keys)
251278
parts.append(f"[{role}]: {content}{meta_str}")
252279
parts.append("")
253280
return "\n".join(parts)

azure/cosmos/agent_memory/services/pipeline.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import time
1515
from collections import defaultdict
1616
from datetime import datetime, timezone
17-
from typing import Any, Literal, Optional
17+
from typing import Any, Iterable, Literal, Optional
1818

1919
from azure.cosmos.exceptions import (
2020
CosmosHttpResponseError,
@@ -46,6 +46,7 @@
4646
from azure.cosmos.agent_memory.services._pipeline_helpers import (
4747
VALID_VALENCES,
4848
PromptyLoader,
49+
_normalize_metadata_keys,
4950
build_topic_tags,
5051
build_transcript,
5152
cap_structured_summary,
@@ -161,7 +162,7 @@ def __init__(
161162
prompts_dir: str | None = None,
162163
*,
163164
containers: dict[ContainerKey, Any],
164-
transcript_metadata_keys: Optional[tuple[str, ...]] = None,
165+
transcript_metadata_keys: Optional[Iterable[str]] = None,
165166
) -> None:
166167
self._store = store
167168
self._containers = containers
@@ -172,9 +173,7 @@ def __init__(
172173
self._chat_client = chat_client
173174
self._embeddings = embeddings_client
174175
self._prompty = PromptyLoader(prompts_dir)
175-
self._transcript_metadata_keys: Optional[tuple[str, ...]] = (
176-
tuple(transcript_metadata_keys) if transcript_metadata_keys else None
177-
)
176+
self._transcript_metadata_keys: Optional[tuple[str, ...]] = _normalize_metadata_keys(transcript_metadata_keys)
178177

179178
def _run_prompty(
180179
self,
@@ -224,6 +223,8 @@ def _build_transcript(
224223
*,
225224
group_by_thread: bool = False,
226225
) -> str:
226+
# getattr fallback covers unit tests that build PipelineService via
227+
# __new__ to bypass __init__ (and therefore the metadata-keys stash).
227228
return build_transcript(
228229
items,
229230
group_by_thread=group_by_thread,

function_app/orchestrators/extract_memories.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ def ExtractMemoriesOrchestrator(context: df.DurableOrchestrationContext):
5656
)
5757
if config.get_procedural_synthesis_auto():
5858
count = payload.get("count")
59-
instance_id = (
60-
f"procedural:{user_id}:{thread_id}:{count}" if count is not None else None
61-
)
59+
instance_id = f"procedural:{user_id}:{thread_id}:{count}" if count is not None else None
6260
try:
6361
procedural = yield context.call_sub_orchestrator_with_retry(
6462
"SynthesizeProceduralOrchestrator",

function_app/shared/pipeline_factory.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import os
910
from typing import Any
1011

1112
from . import config
@@ -18,6 +19,20 @@
1819
_pipeline: Any | None = None
1920

2021

22+
def _read_transcript_metadata_keys() -> tuple[str, ...] | None:
23+
"""Parse ``AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS`` (comma-separated allow-list).
24+
25+
Mirrors the ``transcript_metadata_keys`` ctor kwarg on
26+
:class:`CosmosMemoryClient` so the Durable-Functions backend produces
27+
the same prompt content as the in-process backend.
28+
"""
29+
raw = os.environ.get("AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS", "").strip()
30+
if not raw:
31+
return None
32+
keys = tuple(part.strip() for part in raw.split(",") if part.strip())
33+
return keys or None
34+
35+
2136
def get_pipeline():
2237
"""Return the cached :class:`PipelineService` for this worker."""
2338
global _pipeline
@@ -59,5 +74,11 @@ def get_pipeline():
5974
ContainerKey.SUMMARIES: summaries_container,
6075
}
6176
store = MemoryStore(containers=containers, embeddings_client=embeddings)
62-
_pipeline = PipelineService(store, chat, embeddings, containers=containers)
77+
_pipeline = PipelineService(
78+
store,
79+
chat,
80+
embeddings,
81+
containers=containers,
82+
transcript_metadata_keys=_read_transcript_metadata_keys(),
83+
)
6384
return _pipeline

tests/unit/function_app/test_pipeline_factory.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def test_builds_pipeline_from_complete_env(mocks):
107107
mocks.chat_instance,
108108
mocks.embed_instance,
109109
containers=expected_containers,
110+
transcript_metadata_keys=None,
110111
)
111112

112113

@@ -144,6 +145,31 @@ def test_passes_credential_to_both_clients(mocks):
144145
assert mocks.embed_ctor.call_args.kwargs["credential"] is mocks.credential
145146

146147

148+
def test_transcript_metadata_keys_env_threads_into_pipeline(monkeypatch, mocks):
149+
monkeypatch.setenv(
150+
"AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS",
151+
" agent_id , timestamp , , model_id",
152+
)
153+
pipeline_factory.get_pipeline()
154+
155+
kwargs = mocks.pipeline_ctor.call_args.kwargs
156+
assert kwargs["transcript_metadata_keys"] == ("agent_id", "timestamp", "model_id")
157+
158+
159+
def test_transcript_metadata_keys_env_unset_yields_none(monkeypatch, mocks):
160+
monkeypatch.delenv("AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS", raising=False)
161+
pipeline_factory.get_pipeline()
162+
163+
assert mocks.pipeline_ctor.call_args.kwargs["transcript_metadata_keys"] is None
164+
165+
166+
def test_transcript_metadata_keys_env_empty_string_yields_none(monkeypatch, mocks):
167+
monkeypatch.setenv("AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS", " ")
168+
pipeline_factory.get_pipeline()
169+
170+
assert mocks.pipeline_ctor.call_args.kwargs["transcript_metadata_keys"] is None
171+
172+
147173
# ---------------------------------------------------------------------------
148174
# Caching / idempotence
149175
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)