Files
Theo van Kraay a057cd505c Python: Add agent-framework-azure-cosmos-memory context provider (#6719)
* Add agent-framework-azure-cosmos-memory context provider (draft)

Introduces CosmosMemoryContextProvider, a ContextProvider that wraps the azure-cosmos-agent-memory toolkit to give agents long-term, Cosmos DB-backed memory (fact/procedural recall + user summaries). Includes package scaffolding, unit tests (mocked client), live Azure integration tests (marked), samples, README, and AGENTS.md.

Draft: uv.lock is intentionally left unchanged. This package depends on azure-cosmos-agent-memory (requires Python >=3.11), which is unsatisfiable against the workspace's current >=3.10 floor, so adding it to the shared lock requires a workspace decision (raise floor to 3.11 or exclude from workspace). Test coverage to be expanded.

* ci: exclude azure-cosmos-memory from uv workspace resolution

The package depends on azure-cosmos-agent-memory which requires Python
>=3.11 and a prompty pre-release (>=2.0.0a9). Both are unsatisfiable
against the workspace's >=3.10 floor and pre-release policy, causing
uv sync to fail in every Python CI job. Exclude the package from the
shared workspace so it is resolved and tested as a standalone package.

* ci: fix code-quality failures for azure-cosmos-memory

- Strip trailing whitespace from package files (pre-commit trailing-whitespace hook)
- Exclude the package README from markdown-code-lint: the package is excluded
  from the uv workspace, so its README snippets import a module that is not
  installed in the workspace env and Pyright cannot resolve it

* Exclude azure-cosmos-memory README from markdown-code-lint task

* Address PR review comments on cosmos-memory context provider

- Wire credential into Cosmos and AI Foundry clients; let toolkit own
  DefaultAzureCredential when none supplied (remove dead import).
- Honor auto_extract=False by zeroing extraction/summary cadence thresholds.
- Skip whitespace-only conversation turns and store stripped content.
- Show confidence 0.0 and coerce confidence to float in _format_memories.
- Register both 'integration' and 'azure' pytest markers accurately.
- Fix duplicated install block in README.
- Update and extend unit tests for new credential wiring and fixes.

* Include azure-cosmos-memory in the uv workspace

Follow the github_copilot pattern for a package with a Python 3.11-only
dependency: lower requires-python to >=3.10 and gate azure-cosmos-agent-memory
behind a python_version >= '3.11' marker. Add a direct, gated prompty
pre-release dependency so the workspace's if-necessary-or-explicit prerelease
policy permits the toolkit's transitive prompty requirement. Guard the test
modules with pytest.importorskip so the 3.10 CI leg skips cleanly. Remove the
workspace exclude and the markdown-code-lint exclude, and regenerate uv.lock.

* Address review feedback on cosmos-memory provider

Rename provider parameters to match Agent Framework conventions:
foundry_endpoint (was ai_foundry_endpoint) and embedding_model/chat_model
(were *_deployment_name). Move DEFAULT_* to module-level constants, type
memory_types as a Literal, use DEFAULT_CONTEXT_PROMPT as the default value,
and add ProcessorConfig/CosmosMemorySettings TypedDicts. Resolve connection
settings via agent_framework load_settings with required-field validation,
replacing the manual getenv/raise blocks. Scope user_id/thread_id to the
provider state and drop the unpreventable first-turn warning.

Rewrite the samples around Agent (not raw SessionContext), provider-scoped
state, and session-id threading; use PEP 723 inline dependencies instead of a
samples dependency group; use a plain input() loop; remove the dead custom
processor stub. Update README/AGENTS for the renamed parameters and env vars.
Add a samples ruff per-file-ignores entry now that the package is linted in CI.

* Add emulator-backed vector search integration test

Bump azure-cosmos-agent-memory to >=0.2.0b2 (adds the embeddings/chat client
injection seam) and add tests/test_emulator.py: an integration (not azure)
suite that exercises real Cosmos vector search with a quantizedFlat index
against a local Cosmos DB emulator, using deterministic in-memory fakes for
embeddings and chat so no Azure AI Foundry account or LLM is required.

To run on a stock emulator the fixture strips the toolkit's full-text index
(the provider only does pure vector search) and requests provisioned autoscale
throughput instead of serverless. The suite skips cleanly when no emulator is
reachable.

* Fix CI typing and package checks for azure-cosmos-memory

The package recently joined the uv workspace, so its source and tests are now covered by the Test Typing Checks and Package Checks gates for the first time.

tests: rename stale constructor kwargs to the current provider API (foundry_endpoint/embedding_model/chat_model); use a typed _STUB_AGENT for the unused agent param so pyright/pyrefly/ty/zuban all accept it; make processor_config values ints; assert non-None memory_client in the emulator tests.

source: relax reportUnknown*/reportOptional* for this package only (the toolkit ships no py.typed; mirrors the hosting-telegram precedent); decouple the conditional toolkit import from the annotation type; use settings.get(); fix memory_types list invariance; drop a redundant None guard; read role via getattr.

* Apply pyupgrade: single-arg AsyncGenerator in test_integration

* Make Cosmos memory extraction drain transparently on provider exit

The provider now drains in-flight background memory extraction in __aexit__, so applications no longer need to call flush() in their own control flow; the client's close() would otherwise cancel pending extraction tasks. flush() is hardened against clients that expose no usable background-task registry.

sample: interactive_chat reads input via asyncio.to_thread so the event loop stays free and background extraction runs during the session; removes the manual flush now that the provider drains on exit.

tests: add explicit transparent-extraction integration tests (emulator: after_run schedules extraction and __aexit__ drains it; live Azure: a fact is extracted and recalled in a later session with no manual flush). Emulator tests reuse a single fixed database to avoid exhausting the emulator's partition budget across runs.

* Add custom extraction-prompt seam and sample to cosmos-memory provider

Adds a prompts_dir option to CosmosMemoryContextProvider that points the Agent Memory Toolkit pipeline at a caller-supplied directory of Prompty templates, so callers can override extract_memories.prompty to control what the extraction LLM produces. The toolkit exposes no public prompts-directory seam, so the provider contains the one internal touch (swapping the pipeline's template loader after the store connects); applies to both provider-built and supplied clients.

sample: interactive_chat_custom_extraction.py - the interactive chat wired with a custom coding-assistant extraction rubric. It derives a complete prompts directory at runtime (copies the bundled templates and augments extract_memories.prompty) so it stays schema-compatible with the installed toolkit.

tests: unit tests assert the provider redirects the pipeline loader only when prompts_dir is set; an emulator integration test proves end to end that a unique marker in a custom extract_memories.prompty reaches the extraction LLM call.

* docs: document prompts_dir custom-extraction seam in cosmos-memory README

Replaces the stale, non-functional CustomMemoryProcessor snippet with the working prompts_dir approach, lists the new interactive_chat_custom_extraction.py sample, and corrects the interactive-sample feature list.

* Address review: rename _new_session, drop defensive toolkit import guard

Sample (comment): rename _new_thread to _new_session in both interactive samples (a new session is the new thread).

Provider (comment): replace the _memory_toolkit_available flag + __init__ ImportError guard with a plain guarded import that re-raises a clear ImportError, matching the github_copilot package's pattern for its 3.11-only SDK. Kept requires-python >=3.10 (bumping this one workspace member to 3.11 would force the entire uv workspace lock floor to 3.11). Tests now run importorskip before importing the package, mirroring github_copilot.

* Pass cadence via cadence_thresholds instead of mutating os.environ

* Mark package alpha and drop private naming in samples

* Require Python 3.11 and inject user summary as untrusted context

* CI: exclude azure-cosmos-memory from uv sync on Python 3.10

* Re-trigger CI (flaky external link check)

* Require chat/embedding models instead of silent defaults

* Fix pyright: narrow resolved chat/embedding models to str

---------

Co-authored-by: Theo van Kraay <thvankra@microsoft.com>
2026-07-20 09:44:11 +00:00

116 lines
4.4 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "agent-framework-azure-cosmos-memory",
# "agent-framework-foundry",
# "python-dotenv",
# ]
# ///
"""Basic usage of CosmosMemoryContextProvider with an agent.
Attach the provider to an ``Agent`` and it transparently searches long-term memory
before each run (injecting relevant memories) and stores the conversation turns
afterwards for background fact/summary extraction.
Set these environment variables (or put them in a ``.env`` file) before running:
COSMOS_ENDPOINT Azure Cosmos DB account endpoint
FOUNDRY_ENDPOINT Azure AI Foundry project endpoint (chat + embeddings)
Optional:
COSMOS_DATABASE Database name (default: ai_memory)
CHAT_MODEL Chat deployment (default: gpt-4o-mini)
EMBEDDING_MODEL Embedding deployment (default: text-embedding-3-large)
Run:
python samples/basic_usage.py
"""
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import DefaultAzureCredential
from dotenv import load_dotenv
from agent_framework_azure_cosmos_memory import CosmosMemoryContextProvider
def _build_agent(provider: CosmosMemoryContextProvider, credential: DefaultAzureCredential) -> Agent:
"""Build an agent that uses the memory provider and the same Foundry endpoint for chat."""
return Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_ENDPOINT"],
model=os.getenv("CHAT_MODEL", "gpt-4o-mini"),
credential=credential,
),
name="Memory Assistant",
instructions="You are a helpful assistant with long-term memory about the user.",
context_providers=[provider],
)
async def user_scoped_memory() -> None:
"""Memory scoped to a stable user id, so it persists across sessions and threads."""
credential = DefaultAzureCredential()
provider = CosmosMemoryContextProvider(
cosmos_endpoint=os.environ["COSMOS_ENDPOINT"],
foundry_endpoint=os.environ["FOUNDRY_ENDPOINT"],
embedding_model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-large"),
chat_model=os.getenv("CHAT_MODEL", "gpt-4o-mini"),
credential=credential,
)
agent = _build_agent(provider, credential)
async with provider:
session = agent.create_session()
# Provider state is scoped by source id; set a stable user id there so memory
# persists across sessions rather than being limited to this one.
session.state.setdefault(provider.source_id, {})["user_id"] = "alice"
first = await agent.run("I love hiking and I'm allergic to peanuts.", session=session)
print("Assistant:", first.text)
# A brand-new session for the same user still recalls the earlier facts.
new_session = agent.create_session()
new_session.state.setdefault(provider.source_id, {})["user_id"] = "alice"
recall = await agent.run("What do you remember about me?", session=new_session)
print("Assistant:", recall.text)
# Let background extraction finish and persist before the client closes.
await provider.flush()
async def session_scoped_memory() -> None:
"""Without a user id, memory is scoped to the session id (single-session recall)."""
credential = DefaultAzureCredential()
provider = CosmosMemoryContextProvider(
cosmos_endpoint=os.environ["COSMOS_ENDPOINT"],
foundry_endpoint=os.environ["FOUNDRY_ENDPOINT"],
embedding_model=os.getenv("EMBEDDING_MODEL", "text-embedding-3-large"),
chat_model=os.getenv("CHAT_MODEL", "gpt-4o-mini"),
credential=credential,
)
agent = _build_agent(provider, credential)
async with provider:
# No user_id in provider state -> memory is scoped to this session's id.
session = agent.create_session()
await agent.run("Remember that my project uses FastAPI and PostgreSQL.", session=session)
followup = await agent.run("Which web framework am I using?", session=session)
print("Assistant:", followup.text)
await provider.flush()
async def main() -> None:
load_dotenv()
print("=== User-scoped memory ===")
await user_scoped_memory()
print("\n=== Session-scoped memory ===")
await session_scoped_memory()
if __name__ == "__main__":
asyncio.run(main())