From 1b8b3063a7575f86a3ecafbc947dd66f540f7731 Mon Sep 17 00:00:00 2001 From: Aayush Kataria Date: Mon, 1 Jun 2026 11:54:06 -0700 Subject: [PATCH 1/5] Renaming the toolkit to azure.cosmos.agent_memory --- .github/workflows/ci.yml | 6 +- CHANGELOG.md | 49 +++++++ Docs/RELEASING.md | 128 ++++++++++++++++++ Docs/azure_testing.md | 4 +- Docs/design_patterns.md | 2 +- Docs/local_testing.md | 6 +- Docs/public_api.md | 10 +- Docs/troubleshooting.md | 6 +- README.md | 28 ++-- Samples/Advanced/advanced_memory_lifecycle.py | 2 +- Samples/Advanced/advanced_search_patterns.py | 2 +- Samples/Notebooks/Demo.ipynb | 2 +- Samples/Notebooks/Demo_async.ipynb | 2 +- Samples/Notebooks/Demo_function_app.ipynb | 2 +- .../Notebooks/Demo_function_app_async.ipynb | 2 +- .../Processing/processing_fact_extraction.py | 2 +- .../Processing/processing_thread_summary.py | 2 +- Samples/Processing/processing_user_profile.py | 2 +- Samples/Quickstarts/quickstart_cosmos.py | 2 +- Samples/Quickstarts/quickstart_local.py | 2 +- Samples/Scenarios/scenario_chat_memory.py | 2 +- Samples/Scenarios/scenario_counter_tuning.py | 2 +- .../Scenarios/scenario_customer_support.py | 2 +- .../scenario_memory_reconciliation.py | 2 +- Samples/Scenarios/scenario_multi_agent.py | 2 +- Samples/Scenarios/scenario_rag_with_memory.py | 2 +- .../Scenarios/scenario_remote_processor.py | 2 +- .../scenario_remote_processor_async.py | 2 +- .../scenario_tagging_and_filtering.py | 2 +- agent_memory_toolkit/_base/__init__.py | 5 - agent_memory_toolkit/aio/services/__init__.py | 12 -- azure.yaml | 4 +- .../cosmos/agent_memory}/__init__.py | 14 +- azure/cosmos/agent_memory/_base/__init__.py | 5 + .../cosmos/agent_memory}/_base/base_client.py | 8 +- .../agent_memory}/_container_routing.py | 0 .../cosmos/agent_memory}/_counters.py | 2 +- .../cosmos/agent_memory}/_query_builder.py | 0 .../cosmos/agent_memory}/_utils.py | 0 .../cosmos/agent_memory}/aio/__init__.py | 8 +- .../cosmos/agent_memory}/aio/auto_trigger.py | 14 +- .../cosmos/agent_memory}/aio/chat.py | 8 +- .../agent_memory}/aio/cosmos_memory_client.py | 30 ++-- .../cosmos/agent_memory}/aio/embeddings.py | 6 +- .../agent_memory}/aio/processors/__init__.py | 2 +- .../agent_memory}/aio/processors/base.py | 4 +- .../agent_memory}/aio/processors/durable.py | 4 +- .../agent_memory}/aio/processors/inprocess.py | 10 +- .../agent_memory/aio/services/__init__.py | 12 ++ .../agent_memory}/aio/services/pipeline.py | 30 ++-- .../agent_memory}/aio/store/__init__.py | 2 +- .../agent_memory}/aio/store/memory_store.py | 18 +-- .../cosmos/agent_memory}/auto_trigger.py | 12 +- .../cosmos/agent_memory}/chat.py | 4 +- .../agent_memory}/cosmos_memory_client.py | 2 +- .../cosmos/agent_memory}/embeddings.py | 4 +- .../cosmos/agent_memory}/exceptions.py | 0 .../cosmos/agent_memory}/logging.py | 6 +- .../cosmos/agent_memory}/models.py | 2 +- .../agent_memory}/processors/__init__.py | 0 .../cosmos/agent_memory}/processors/base.py | 6 +- .../agent_memory}/processors/durable.py | 2 +- .../agent_memory}/processors/inprocess.py | 2 +- .../cosmos/agent_memory}/prompts/__init__.py | 0 .../cosmos/agent_memory}/prompts/_schemas.py | 0 .../agent_memory}/prompts/dedup.prompty | 0 .../prompts/extract_memories.prompty | 0 .../agent_memory}/prompts/summarize.prompty | 0 .../prompts/summarize_update.prompty | 0 .../prompts/synthesize_procedural.prompty | 0 .../prompts/user_summary.prompty | 0 .../prompts/user_summary_update.prompty | 0 .../cosmos/agent_memory}/services/__init__.py | 0 .../services/_pipeline_helpers.py | 6 +- .../cosmos/agent_memory}/services/pipeline.py | 30 ++-- .../cosmos/agent_memory}/store/__init__.py | 2 +- .../agent_memory}/store/_search_helpers.py | 8 +- .../agent_memory}/store/memory_store.py | 16 +-- .../cosmos/agent_memory}/thresholds.py | 2 +- ...gent_memory_toolkit-0.1.0-py3-none-any.whl | Bin 165132 -> 0 bytes function_app/orchestrators/__init__.py | 2 +- .../orchestrators/extract_memories.py | 2 +- function_app/requirements.txt | 4 +- function_app/shared/config.py | 2 +- function_app/shared/counters.py | 2 +- function_app/shared/pipeline_factory.py | 14 +- function_app/triggers/change_feed.py | 2 +- infra/main.bicep | 2 +- pyproject.toml | 57 +++++++- tests/integration/test_full_pipeline.py | 2 +- .../integration/test_processor_integration.py | 6 +- .../test_processor_integration_async.py | 6 +- tests/integration/test_ttl_lifecycle.py | 2 +- tests/unit/_base/test_base_client.py | 4 +- tests/unit/aio/processors/test_base.py | 2 +- tests/unit/aio/processors/test_durable.py | 2 +- tests/unit/aio/processors/test_inprocess.py | 2 +- .../processors/test_protocol_satisfaction.py | 2 +- tests/unit/aio/store/test_memory_store.py | 6 +- tests/unit/aio/test_auto_trigger.py | 6 +- tests/unit/aio/test_chat.py | 4 +- tests/unit/aio/test_cosmos_memory_client.py | 4 +- tests/unit/aio/test_embeddings.py | 4 +- tests/unit/aio/test_procedural_synthesis.py | 4 +- tests/unit/aio/test_process_now.py | 6 +- tests/unit/aio/test_reconcile_telemetry.py | 4 +- tests/unit/function_app/conftest.py | 2 +- .../function_app/test_pipeline_factory.py | 10 +- tests/unit/processors/test_base.py | 2 +- tests/unit/processors/test_durable.py | 2 +- tests/unit/processors/test_inprocess.py | 2 +- .../processors/test_protocol_satisfaction.py | 2 +- .../test_schema_prompty_conformance.py | 8 +- .../services/test_chaos_extract_persist.py | 6 +- tests/unit/services/test_extract_dry.py | 6 +- tests/unit/services/test_persist_extracted.py | 10 +- tests/unit/services/test_pipeline_service.py | 4 +- tests/unit/services/test_prompty_loader.py | 2 +- .../services/test_user_summary_topic_tags.py | 4 +- tests/unit/store/test_list_tags.py | 6 +- tests/unit/store/test_memory_store.py | 6 +- tests/unit/store/test_tag_mutation.py | 8 +- tests/unit/test_async_hygiene.py | 8 +- tests/unit/test_auto_trigger.py | 22 +-- tests/unit/test_chat.py | 6 +- tests/unit/test_container_routing.py | 4 +- tests/unit/test_cosmos_memory_client.py | 4 +- tests/unit/test_counters.py | 4 +- tests/unit/test_embeddings.py | 4 +- tests/unit/test_exceptions.py | 4 +- tests/unit/test_logging.py | 26 ++-- tests/unit/test_memory_type_multi.py | 4 +- tests/unit/test_min_confidence.py | 2 +- tests/unit/test_models.py | 4 +- tests/unit/test_pipeline_confidence.py | 20 +-- tests/unit/test_procedural_synthesis.py | 10 +- tests/unit/test_process_now.py | 6 +- tests/unit/test_query_builder.py | 4 +- tests/unit/test_reconcile.py | 38 +++--- tests/unit/test_thresholds.py | 2 +- tests/unit/test_utils.py | 6 +- 141 files changed, 606 insertions(+), 386 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 Docs/RELEASING.md delete mode 100644 agent_memory_toolkit/_base/__init__.py delete mode 100644 agent_memory_toolkit/aio/services/__init__.py rename {agent_memory_toolkit => azure/cosmos/agent_memory}/__init__.py (77%) create mode 100644 azure/cosmos/agent_memory/_base/__init__.py rename {agent_memory_toolkit => azure/cosmos/agent_memory}/_base/base_client.py (97%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/_container_routing.py (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/_counters.py (99%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/_query_builder.py (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/_utils.py (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/__init__.py (62%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/auto_trigger.py (93%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/chat.py (97%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/cosmos_memory_client.py (96%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/embeddings.py (96%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/processors/__init__.py (88%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/processors/base.py (93%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/processors/durable.py (96%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/processors/inprocess.py (93%) create mode 100644 azure/cosmos/agent_memory/aio/services/__init__.py rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/services/pipeline.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/store/__init__.py (52%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/aio/store/memory_store.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/auto_trigger.py (95%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/chat.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/cosmos_memory_client.py (99%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/embeddings.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/exceptions.py (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/logging.py (94%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/models.py (99%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/processors/__init__.py (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/processors/base.py (93%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/processors/durable.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/processors/inprocess.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/__init__.py (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/_schemas.py (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/dedup.prompty (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/extract_memories.prompty (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/summarize.prompty (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/summarize_update.prompty (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/synthesize_procedural.prompty (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/user_summary.prompty (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/prompts/user_summary_update.prompty (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/services/__init__.py (100%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/services/_pipeline_helpers.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/services/pipeline.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/store/__init__.py (52%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/store/_search_helpers.py (91%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/store/memory_store.py (98%) rename {agent_memory_toolkit => azure/cosmos/agent_memory}/thresholds.py (99%) delete mode 100644 function_app/_vendor/agent_memory_toolkit-0.1.0-py3-none-any.whl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05e80a9..a7852c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,9 @@ jobs: - name: Install dependencies run: pip install 'ruff>=0.15,<0.16' - name: Ruff check - run: ruff check agent_memory_toolkit/ tests/ + run: ruff check azure/cosmos/agent_memory/ tests/ - name: Ruff format check - run: ruff format --check agent_memory_toolkit/ tests/ + run: ruff format --check azure/cosmos/agent_memory/ tests/ test: runs-on: ubuntu-latest @@ -40,7 +40,7 @@ jobs: - name: Install package with dev dependencies run: pip install -e ".[dev]" - name: Run unit tests with coverage - run: pytest tests/unit/ --cov=agent_memory_toolkit --cov-report=xml --cov-report=term-missing -v + run: pytest tests/unit/ --cov=azure.cosmos.agent_memory --cov-report=xml --cov-report=term-missing -v - name: Upload coverage if: always() uses: actions/upload-artifact@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..20c0902 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +All notable changes to `azure-cosmos-agent-memory` are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) +and this project adheres to [PEP 440](https://peps.python.org/pep-0440/) versioning. + +## [0.1.0b1] — 2026-05-31 + +Initial public preview release. + +This is a **beta release**. The public surface may evolve in +backward-incompatible ways before the `1.0.0` general-availability cut. +Pin a specific version when integrating. + +### Added + +- Sync (`CosmosMemoryClient`) and async (`AsyncCosmosMemoryClient`) clients + for storing, retrieving, and transforming agent memories backed by Azure + Cosmos DB. +- Typed memory record hierarchy (Pydantic): `TurnRecord`, `FactRecord`, + `EpisodicRecord`, `ProceduralRecord`, `ThreadSummaryRecord`, + `UserSummaryRecord`. +- Vector + full-text + hybrid search over memories with metadata filters, + tag filters, and per-type scoping. +- Built-in memory processing pipeline: fact extraction, thread/user + summarization, procedural-memory synthesis, contradiction handling, and + deduplication — all driven by versioned `.prompty` prompts. +- Two processor backends: `InProcessProcessor` (default, runs in your + application process) and `DurableFunctionProcessor` (offloads work to a + sibling Azure Function app via Cosmos DB change feed). +- One-command `azd up` deployment that provisions Cosmos DB (with vector + + full-text search enabled), Azure AI Foundry (chat + embedding + deployments), Azure Function app (Flex Consumption), Storage, App + Insights, and the User-Assigned Managed Identity wiring all of it + together. +- Focused exception hierarchy: `AgentMemoryError`, `ConfigurationError`, + `ValidationError`, `CosmosNotConnectedError`, `CosmosOperationError`, + `MemoryNotFoundError`, `MemoryTypeMismatchError`, `LLMError`. +- Structured JSON logging via `azure.cosmos.agent_memory.logging` + (`configure_logging`, `JsonFormatter`). + +### Package layout + +- Distribution name: **`azure-cosmos-agent-memory`** (PyPI) +- Import path: **`azure.cosmos.agent_memory`** (Azure SDK namespace + convention, coexists side-by-side with `azure-cosmos`) + +[0.1.0b1]: https://github.com/AzureCosmosDB/AgentMemoryToolkit/releases/tag/v0.1.0b1 diff --git a/Docs/RELEASING.md b/Docs/RELEASING.md new file mode 100644 index 0000000..2521af4 --- /dev/null +++ b/Docs/RELEASING.md @@ -0,0 +1,128 @@ +# Release process + +Maintainer runbook for cutting a new release of `azure-cosmos-agent-memory`. + +## Versioning + +This project uses [PEP 440](https://peps.python.org/pep-0440/) versioning: + +| Type | Example | Notes | +|------|---------|-------| +| Beta | `0.1.0b1`, `0.1.0b2` | Public preview; breaking changes allowed | +| Release candidate | `0.1.0rc1` | Stabilizing; no breaking changes from this point | +| Final | `0.1.0` | GA; semver rules apply afterwards | +| Patch | `0.1.1` | Bug fixes only | +| Minor | `0.2.0` | Backwards-compatible additions | +| Major | `1.0.0` | Allowed to break; document the migration path | + +## Pre-flight checklist + +Before cutting a release: + +1. **CI green on `main`** — every workflow in `.github/workflows/` passes. +2. **Unit tests pass locally** in a fresh venv: + ```bash + python -m venv /tmp/release-venv + source /tmp/release-venv/bin/activate + pip install -e ".[dev]" + pytest tests/unit -q + ``` +3. **Integration tests pass** against a live environment (`azd up` + environment is fine): + ```bash + AGENT_MEMORY_RUN_INTEGRATION=true pytest tests/integration -q + ``` +4. **Samples and notebooks work** — every script under `Samples/` runs to + completion against the live environment. +5. **No uncommitted local changes** — `git status` clean. + +## Cutting a release + +1. **Bump the version** in `pyproject.toml`: + ```toml + [project] + version = "0.1.0b2" + ``` +2. **Update `CHANGELOG.md`** — add a new section with the version, the + date, and a summary of changes. Move entries from the unreleased + section if you keep one. +3. **Rebuild the vendored wheel** for the Function app (the FA installs + the SDK from `function_app/_vendor/` rather than PyPI): + ```bash + rm -rf dist/ build/ + python -m build --wheel + rm function_app/_vendor/azure_cosmos_agent_memory-*.whl + cp dist/azure_cosmos_agent_memory--py3-none-any.whl function_app/_vendor/ + ``` + And update the wheel filename in `function_app/requirements.txt`. +4. **Commit** the version bump + CHANGELOG + rebuilt vendor wheel + + updated `requirements.txt` together. Suggested message: + `Release v`. +5. **Tag** the commit: + ```bash + git tag -a v -m "Release v" + git push origin main v + ``` +6. **Build the release artifacts**: + ```bash + rm -rf dist/ build/ + python -m build + twine check dist/* + ``` +7. **Smoke-test the wheel in a fresh venv**: + ```bash + python -m venv /tmp/release-smoke + source /tmp/release-smoke/bin/activate + pip install dist/azure_cosmos_agent_memory--py3-none-any.whl + python -c "from azure.cosmos.agent_memory import CosmosMemoryClient; print(CosmosMemoryClient)" + python -c "from azure.cosmos import CosmosClient; print(CosmosClient)" # side-by-side check + ``` +8. **Upload to TestPyPI** (sanity check before pushing to the real index): + ```bash + twine upload --repository testpypi dist/* + ``` + Verify the package page renders correctly at + . +9. **Upload to PyPI**: + ```bash + twine upload dist/* + ``` +10. **Create a GitHub Release** from the tag with the changelog entry as + the body. + +## Trusted Publishing (recommended) + +Configure [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) +so the GitHub Actions workflow uploads with a short-lived OIDC token +instead of a long-lived API token. See `.github/workflows/release.yml` +(once added) for the tag-triggered build → TestPyPI → PyPI flow. + +## Namespace package note + +`azure-cosmos-agent-memory` installs files under `azure/cosmos/agent_memory/`. +It MUST NOT ship `azure/__init__.py` or `azure/cosmos/__init__.py` — those +are owned by the `azure-cosmos` package. The wheel build is configured +(`[tool.setuptools.packages.find]` with `include = ["azure.cosmos.agent_memory*"]` +and `namespaces = true`) so that only the `agent_memory` subtree is packaged. +After every release, sanity-check with: + +```bash +python -m zipfile -l dist/azure_cosmos_agent_memory-*.whl | grep '__init__.py' | head +``` + +You should see `azure/cosmos/agent_memory/__init__.py` and its descendants, +but never the standalone `azure/__init__.py` or `azure/cosmos/__init__.py`. + +## Yanking a bad release + +If a release ships a regression: + +1. **Yank** the version on PyPI (does not delete; marks installs as a warning): + ```bash + twine yank azure-cosmos-agent-memory --version + ``` +2. **Cut a new patch release** with the fix and updated CHANGELOG. +3. Add a deprecation note to the GitHub Release pointing at the + replacement version. + +Never delete a PyPI release — yank it and ship a fixed version instead. diff --git a/Docs/azure_testing.md b/Docs/azure_testing.md index 91b01e0..590dc4b 100644 --- a/Docs/azure_testing.md +++ b/Docs/azure_testing.md @@ -206,7 +206,7 @@ Run once if the database and container do not already exist: import os from dotenv import load_dotenv from azure.identity import DefaultAzureCredential -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() @@ -235,7 +235,7 @@ memory.connect_cosmos() import os from dotenv import load_dotenv from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential -from agent_memory_toolkit.aio import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient load_dotenv() diff --git a/Docs/design_patterns.md b/Docs/design_patterns.md index 02f3a95..ce8e72b 100644 --- a/Docs/design_patterns.md +++ b/Docs/design_patterns.md @@ -11,7 +11,7 @@ This guide shows when and how to use the toolkit's main operations in real appli Write a turn memory every time a user or agent message is produced. If the application runs locally first and syncs later, use the local + bulk-upload pattern. ```python -from agent_memory_toolkit.aio import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient mem = AsyncCosmosMemoryClient( cosmos_endpoint=COSMOS_DB_ENDPOINT, diff --git a/Docs/local_testing.md b/Docs/local_testing.md index 75d8b9f..703e50f 100644 --- a/Docs/local_testing.md +++ b/Docs/local_testing.md @@ -115,7 +115,7 @@ No Azure resources are required for local in-memory operations. ```python import uuid -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient memory = CosmosMemoryClient(use_default_credential=False) @@ -153,7 +153,7 @@ Then run a minimal smoke test: import os, uuid from dotenv import load_dotenv from azure.identity import DefaultAzureCredential -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() @@ -196,7 +196,7 @@ for r in results: import os, uuid from dotenv import load_dotenv from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential -from agent_memory_toolkit.aio import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient load_dotenv() diff --git a/Docs/public_api.md b/Docs/public_api.md index 5f5515f..1a99f3c 100644 --- a/Docs/public_api.md +++ b/Docs/public_api.md @@ -120,12 +120,12 @@ Use `validate_topology()` (sync) or `await validate_topology()` (async) after `c ## Extension Points -Sync extension protocols live in `agent_memory_toolkit.services`; async variants live in `agent_memory_toolkit.aio.services`. +Sync extension protocols live in `azure.cosmos.agent_memory.services`; async variants live in `azure.cosmos.agent_memory.aio.services`. -- `MemoryStoreProtocol` (`agent_memory_toolkit.services`): persistence primitives (`query`, `read_item`, `add_cosmos`, `mark_superseded`) consumed by the pipeline. +- `MemoryStoreProtocol` (`azure.cosmos.agent_memory.services`): persistence primitives (`query`, `read_item`, `add_cosmos`, `mark_superseded`) consumed by the pipeline. Concrete service classes are exported from their respective packages: -- Sync: `RetrievalService`, `PipelineService` from `agent_memory_toolkit.services` (sub-modules `retrieval`, `pipeline`). -- Async: `AsyncRetrievalService` and `AsyncPipelineService` from `agent_memory_toolkit.aio.services` (sub-modules `retrieval`, `pipeline`). The async pipeline is a fully-native asyncio implementation — not an `asyncio.to_thread` shim over the sync pipeline. -- Threshold-driven auto-trigger: `maybe_trigger_steps` from `agent_memory_toolkit.auto_trigger` (sync) and `agent_memory_toolkit.aio.auto_trigger` (async). +- Sync: `RetrievalService`, `PipelineService` from `azure.cosmos.agent_memory.services` (sub-modules `retrieval`, `pipeline`). +- Async: `AsyncRetrievalService` and `AsyncPipelineService` from `azure.cosmos.agent_memory.aio.services` (sub-modules `retrieval`, `pipeline`). The async pipeline is a fully-native asyncio implementation — not an `asyncio.to_thread` shim over the sync pipeline. +- Threshold-driven auto-trigger: `maybe_trigger_steps` from `azure.cosmos.agent_memory.auto_trigger` (sync) and `azure.cosmos.agent_memory.aio.auto_trigger` (async). diff --git a/Docs/troubleshooting.md b/Docs/troubleshooting.md index 84ea675..27f500f 100644 --- a/Docs/troubleshooting.md +++ b/Docs/troubleshooting.md @@ -30,8 +30,8 @@ pip install -r function_app/requirements.txt The public clients are: ```python -from agent_memory_toolkit import CosmosMemoryClient -from agent_memory_toolkit.aio import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient +from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient ``` If notebooks cannot import the package, run them from the repo root with paths such as `Samples/Notebooks/Demo.ipynb`, or add the repository root to `sys.path`. @@ -184,7 +184,7 @@ Use async Azure credentials with the async client: ```python from azure.identity.aio import DefaultAzureCredential -from agent_memory_toolkit.aio import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient ``` Always `await` cloud operations and close the client when done: diff --git a/README.md b/README.md index 6578ce9..971e62e 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Agent Memory Toolkit is a Python SDK for storing, retrieving, and transforming a ### 1. Install ```bash -pip install . +pip install azure-cosmos-agent-memory -# With dev/test dependencies +# With dev/test dependencies (from a checkout) pip install ".[dev]" ``` @@ -75,7 +75,7 @@ cp .env.template .env ```python import os, uuid from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() @@ -111,7 +111,7 @@ print(memory.get_user_summary(user_id=USER)) > Async API is identical — just `await` each call: > ```python -> from agent_memory_toolkit.aio import AsyncCosmosMemoryClient +> from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient > ``` ### 4. Run a sample @@ -231,7 +231,7 @@ Pick at construction time via the `processor=` kwarg. | `process_now_and_wait()` | Returns immediately after flush | Polls until summary visible (RU-costly; tests/demos) | ```python -from agent_memory_toolkit import CosmosMemoryClient, DurableFunctionProcessor +from azure.cosmos.agent_memory import CosmosMemoryClient, DurableFunctionProcessor memory = CosmosMemoryClient(..., processor=DurableFunctionProcessor()) ``` @@ -267,16 +267,16 @@ memory = CosmosMemoryClient(..., processor=DurableFunctionProcessor()) | Symbol | Module | Purpose | |---|---|---| -| `CosmosMemoryClient` | `agent_memory_toolkit` | Sync client — local CRUD, Cosmos DB I/O, processing | -| `AsyncCosmosMemoryClient` | `agent_memory_toolkit.aio` | Async mirror | -| `MemoryProcessor` | `agent_memory_toolkit` | Protocol that any processor backend implements | -| `InProcessProcessor` | `agent_memory_toolkit` | Default backend — runs the pipeline in-process | -| `DurableFunctionProcessor` | `agent_memory_toolkit` | Marker backend — work runs in sibling Function app via change feed | +| `CosmosMemoryClient` | `azure.cosmos.agent_memory` | Sync client — local CRUD, Cosmos DB I/O, processing | +| `AsyncCosmosMemoryClient` | `azure.cosmos.agent_memory.aio` | Async mirror | +| `MemoryProcessor` | `azure.cosmos.agent_memory` | Protocol that any processor backend implements | +| `InProcessProcessor` | `azure.cosmos.agent_memory` | Default backend — runs the pipeline in-process | +| `DurableFunctionProcessor` | `azure.cosmos.agent_memory` | Marker backend — work runs in sibling Function app via change feed | | `client.process_now()` | — | Run the pipeline for recent turns (in-process) or no-op (remote) | | `client.process_now_and_wait()` | — | Opt-in poll until processing completes; useful for tests/demos with the remote backend | -| `MemoryRecord`, `MemoryType`, `Role` | `agent_memory_toolkit` | Pydantic models / enums | +| `MemoryRecord`, `MemoryType`, `Role` | `azure.cosmos.agent_memory` | Pydantic models / enums | -Async equivalents (`AsyncInProcessProcessor`, `AsyncDurableFunctionProcessor`) live in `agent_memory_toolkit.aio`. +Async equivalents (`AsyncInProcessProcessor`, `AsyncDurableFunctionProcessor`) live in `azure.cosmos.agent_memory.aio`. --- @@ -294,7 +294,7 @@ Async equivalents (`AsyncInProcessProcessor`, `AsyncDurableFunctionProcessor`) l ## Project structure ``` -agent_memory_toolkit/ Python SDK (sync + aio mirror) +azure/cosmos/agent_memory/ Python SDK (sync + aio mirror) processors/ MemoryProcessor Protocol + InProcess/Durable backends function_app/ Sibling Azure Durable Function app infra/ Bicep modules + main.bicep for `azd up` @@ -308,7 +308,7 @@ tests/ Unit + integration tests (pytest) ## Migration notes -- **`agent_memory_toolkit.processing.ProcessingClient` is removed.** Drop the import and call `client.process_now()` (or `client.process_now_and_wait()`) instead. Same for the async `AsyncProcessingClient`. +- **`azure.cosmos.agent_memory.processing.ProcessingClient` is removed.** Drop the import and call `client.process_now()` (or `client.process_now_and_wait()`) instead. Same for the async `AsyncProcessingClient`. - **New `processor=` kwarg.** Defaults to `InProcessProcessor()` — existing code keeps its current behavior with no edits. - **`adf_endpoint` / `adf_key` constructor kwargs are gone.** The SDK no longer makes HTTP calls to the Function app at runtime; the Function app reads from the Cosmos change feed. diff --git a/Samples/Advanced/advanced_memory_lifecycle.py b/Samples/Advanced/advanced_memory_lifecycle.py index 2537f65..c45541f 100644 --- a/Samples/Advanced/advanced_memory_lifecycle.py +++ b/Samples/Advanced/advanced_memory_lifecycle.py @@ -23,7 +23,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Advanced/advanced_search_patterns.py b/Samples/Advanced/advanced_search_patterns.py index 2034615..2d9a518 100644 --- a/Samples/Advanced/advanced_search_patterns.py +++ b/Samples/Advanced/advanced_search_patterns.py @@ -14,7 +14,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Notebooks/Demo.ipynb b/Samples/Notebooks/Demo.ipynb index f32d167..d3178b9 100644 --- a/Samples/Notebooks/Demo.ipynb +++ b/Samples/Notebooks/Demo.ipynb @@ -59,7 +59,7 @@ "from dotenv import load_dotenv\n", "\n", "sys.path.insert(0, os.path.abspath(\"../..\"))\n", - "from agent_memory_toolkit import CosmosMemoryClient\n", + "from azure.cosmos.agent_memory import CosmosMemoryClient\n", "\n", "# Load environment variables from .env in the repo root\n", "load_dotenv(os.path.join(\"../..\", \".env\"))\n", diff --git a/Samples/Notebooks/Demo_async.ipynb b/Samples/Notebooks/Demo_async.ipynb index 6fe08a8..bf413f8 100644 --- a/Samples/Notebooks/Demo_async.ipynb +++ b/Samples/Notebooks/Demo_async.ipynb @@ -35,7 +35,7 @@ "sys.path.insert(0, os.path.abspath(\"../..\"))\n", "from dotenv import load_dotenv\n", "\n", - "from agent_memory_toolkit.aio import AsyncCosmosMemoryClient\n", + "from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient\n", "\n", "# Load environment variables from .env in the repo root\n", "\n", diff --git a/Samples/Notebooks/Demo_function_app.ipynb b/Samples/Notebooks/Demo_function_app.ipynb index a1eff43..a9f031b 100644 --- a/Samples/Notebooks/Demo_function_app.ipynb +++ b/Samples/Notebooks/Demo_function_app.ipynb @@ -64,7 +64,7 @@ "\n", "from dotenv import load_dotenv\n", "\n", - "from agent_memory_toolkit import CosmosMemoryClient, DurableFunctionProcessor\n", + "from azure.cosmos.agent_memory import CosmosMemoryClient, DurableFunctionProcessor\n", "\n", "load_dotenv(override=True)\n", "\n", diff --git a/Samples/Notebooks/Demo_function_app_async.ipynb b/Samples/Notebooks/Demo_function_app_async.ipynb index 9c5b4e9..b6002ea 100644 --- a/Samples/Notebooks/Demo_function_app_async.ipynb +++ b/Samples/Notebooks/Demo_function_app_async.ipynb @@ -41,7 +41,7 @@ "\n", "from dotenv import load_dotenv\n", "\n", - "from agent_memory_toolkit.aio import AsyncCosmosMemoryClient, AsyncDurableFunctionProcessor\n", + "from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient, AsyncDurableFunctionProcessor\n", "\n", "load_dotenv(override=True)\n", "\n", diff --git a/Samples/Processing/processing_fact_extraction.py b/Samples/Processing/processing_fact_extraction.py index cafe862..36a3010 100644 --- a/Samples/Processing/processing_fact_extraction.py +++ b/Samples/Processing/processing_fact_extraction.py @@ -24,7 +24,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Processing/processing_thread_summary.py b/Samples/Processing/processing_thread_summary.py index a532c53..89561e4 100644 --- a/Samples/Processing/processing_thread_summary.py +++ b/Samples/Processing/processing_thread_summary.py @@ -19,7 +19,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Processing/processing_user_profile.py b/Samples/Processing/processing_user_profile.py index d5b7ef5..de4bb9b 100644 --- a/Samples/Processing/processing_user_profile.py +++ b/Samples/Processing/processing_user_profile.py @@ -19,7 +19,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Quickstarts/quickstart_cosmos.py b/Samples/Quickstarts/quickstart_cosmos.py index 0acb005..4d06e96 100644 --- a/Samples/Quickstarts/quickstart_cosmos.py +++ b/Samples/Quickstarts/quickstart_cosmos.py @@ -13,7 +13,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Quickstarts/quickstart_local.py b/Samples/Quickstarts/quickstart_local.py index 9d61924..c51fcfc 100644 --- a/Samples/Quickstarts/quickstart_local.py +++ b/Samples/Quickstarts/quickstart_local.py @@ -4,7 +4,7 @@ python Samples/Quickstarts/quickstart_local.py """ -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient def main(): diff --git a/Samples/Scenarios/scenario_chat_memory.py b/Samples/Scenarios/scenario_chat_memory.py index 86d26c3..3a51c09 100644 --- a/Samples/Scenarios/scenario_chat_memory.py +++ b/Samples/Scenarios/scenario_chat_memory.py @@ -21,7 +21,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Scenarios/scenario_counter_tuning.py b/Samples/Scenarios/scenario_counter_tuning.py index 0f3def0..cd696a1 100644 --- a/Samples/Scenarios/scenario_counter_tuning.py +++ b/Samples/Scenarios/scenario_counter_tuning.py @@ -44,7 +44,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient, DurableFunctionProcessor +from azure.cosmos.agent_memory import CosmosMemoryClient, DurableFunctionProcessor load_dotenv() diff --git a/Samples/Scenarios/scenario_customer_support.py b/Samples/Scenarios/scenario_customer_support.py index 1714510..1a5e459 100644 --- a/Samples/Scenarios/scenario_customer_support.py +++ b/Samples/Scenarios/scenario_customer_support.py @@ -20,7 +20,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Scenarios/scenario_memory_reconciliation.py b/Samples/Scenarios/scenario_memory_reconciliation.py index 52ce60c..70d9930 100644 --- a/Samples/Scenarios/scenario_memory_reconciliation.py +++ b/Samples/Scenarios/scenario_memory_reconciliation.py @@ -29,7 +29,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Scenarios/scenario_multi_agent.py b/Samples/Scenarios/scenario_multi_agent.py index 36d76d9..e6222f6 100644 --- a/Samples/Scenarios/scenario_multi_agent.py +++ b/Samples/Scenarios/scenario_multi_agent.py @@ -24,7 +24,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Scenarios/scenario_rag_with_memory.py b/Samples/Scenarios/scenario_rag_with_memory.py index db9ce74..83bfaae 100644 --- a/Samples/Scenarios/scenario_rag_with_memory.py +++ b/Samples/Scenarios/scenario_rag_with_memory.py @@ -32,7 +32,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/Samples/Scenarios/scenario_remote_processor.py b/Samples/Scenarios/scenario_remote_processor.py index 06ea8b7..23abf89 100644 --- a/Samples/Scenarios/scenario_remote_processor.py +++ b/Samples/Scenarios/scenario_remote_processor.py @@ -25,7 +25,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient, DurableFunctionProcessor +from azure.cosmos.agent_memory import CosmosMemoryClient, DurableFunctionProcessor load_dotenv() diff --git a/Samples/Scenarios/scenario_remote_processor_async.py b/Samples/Scenarios/scenario_remote_processor_async.py index 1c05cb4..2c447e1 100644 --- a/Samples/Scenarios/scenario_remote_processor_async.py +++ b/Samples/Scenarios/scenario_remote_processor_async.py @@ -13,7 +13,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit.aio import ( +from azure.cosmos.agent_memory.aio import ( AsyncCosmosMemoryClient, AsyncDurableFunctionProcessor, ) diff --git a/Samples/Scenarios/scenario_tagging_and_filtering.py b/Samples/Scenarios/scenario_tagging_and_filtering.py index aea3440..e4401d8 100644 --- a/Samples/Scenarios/scenario_tagging_and_filtering.py +++ b/Samples/Scenarios/scenario_tagging_and_filtering.py @@ -26,7 +26,7 @@ from dotenv import load_dotenv -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient load_dotenv() diff --git a/agent_memory_toolkit/_base/__init__.py b/agent_memory_toolkit/_base/__init__.py deleted file mode 100644 index a9e60ef..0000000 --- a/agent_memory_toolkit/_base/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Shared client mixins.""" - -from agent_memory_toolkit._base.base_client import _BaseMemoryClient - -__all__ = ["_BaseMemoryClient"] diff --git a/agent_memory_toolkit/aio/services/__init__.py b/agent_memory_toolkit/aio/services/__init__.py deleted file mode 100644 index 71c70d3..0000000 --- a/agent_memory_toolkit/aio/services/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Async service re-exports. - -Mirrors :mod:`agent_memory_toolkit.services` but only contains the async -variants. Sync code should import from ``agent_memory_toolkit.services``; -async code should import from ``agent_memory_toolkit.aio.services``. -""" - -from __future__ import annotations - -from agent_memory_toolkit.aio.services.pipeline import AsyncPipelineService - -__all__ = ["AsyncPipelineService"] diff --git a/azure.yaml b/azure.yaml index 6c39f19..8c36f98 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,8 +1,8 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json -name: agent-memory-toolkit +name: azure-cosmos-agent-memory metadata: - template: agent-memory-toolkit@0.1.0 + template: azure-cosmos-agent-memory@0.1.0b1 infra: provider: bicep diff --git a/agent_memory_toolkit/__init__.py b/azure/cosmos/agent_memory/__init__.py similarity index 77% rename from agent_memory_toolkit/__init__.py rename to azure/cosmos/agent_memory/__init__.py index fe3cf6c..dbe8b4f 100644 --- a/agent_memory_toolkit/__init__.py +++ b/azure/cosmos/agent_memory/__init__.py @@ -1,9 +1,9 @@ """Agent Memory Toolkit – local and cloud agent memory management.""" -from agent_memory_toolkit.aio import AsyncCosmosMemoryClient -from agent_memory_toolkit.chat import ChatClient -from agent_memory_toolkit.cosmos_memory_client import CosmosMemoryClient -from agent_memory_toolkit.exceptions import ( +from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.chat import ChatClient +from azure.cosmos.agent_memory.cosmos_memory_client import CosmosMemoryClient +from azure.cosmos.agent_memory.exceptions import ( AgentMemoryError, ConfigurationError, CosmosNotConnectedError, @@ -14,15 +14,15 @@ MemoryTypeMismatchError, ValidationError, ) -from agent_memory_toolkit.models import MemoryRecord, MemoryRole, MemoryType, SearchResult -from agent_memory_toolkit.processors import ( +from azure.cosmos.agent_memory.models import MemoryRecord, MemoryRole, MemoryType, SearchResult +from azure.cosmos.agent_memory.processors import ( DurableFunctionProcessor, InProcessProcessor, MemoryProcessor, ProcessThreadResult, UserSummaryResult, ) -from agent_memory_toolkit.thresholds import ( +from azure.cosmos.agent_memory.thresholds import ( DEFAULT_FACT_EXTRACTION_EVERY_N, DEFAULT_THREAD_SUMMARY_EVERY_N, DEFAULT_USER_SUMMARY_EVERY_N, diff --git a/azure/cosmos/agent_memory/_base/__init__.py b/azure/cosmos/agent_memory/_base/__init__.py new file mode 100644 index 0000000..61229f6 --- /dev/null +++ b/azure/cosmos/agent_memory/_base/__init__.py @@ -0,0 +1,5 @@ +"""Shared client mixins.""" + +from azure.cosmos.agent_memory._base.base_client import _BaseMemoryClient + +__all__ = ["_BaseMemoryClient"] diff --git a/agent_memory_toolkit/_base/base_client.py b/azure/cosmos/agent_memory/_base/base_client.py similarity index 97% rename from agent_memory_toolkit/_base/base_client.py rename to azure/cosmos/agent_memory/_base/base_client.py index 99b9ef9..e7aef1c 100644 --- a/agent_memory_toolkit/_base/base_client.py +++ b/azure/cosmos/agent_memory/_base/base_client.py @@ -6,8 +6,8 @@ from datetime import datetime, timezone from typing import Any, Optional -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit._utils import ( +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory._utils import ( VALID_ROLES, VALID_TYPES, _make_memory, @@ -15,8 +15,8 @@ _resolve_cosmos_throughput_mode, _resolve_embedding_dimensions, ) -from agent_memory_toolkit.exceptions import CosmosNotConnectedError, MemoryNotFoundError, ValidationError -from agent_memory_toolkit.logging import configure_logging, get_logger +from azure.cosmos.agent_memory.exceptions import CosmosNotConnectedError, MemoryNotFoundError, ValidationError +from azure.cosmos.agent_memory.logging import configure_logging, get_logger logger = get_logger(__name__) diff --git a/agent_memory_toolkit/_container_routing.py b/azure/cosmos/agent_memory/_container_routing.py similarity index 100% rename from agent_memory_toolkit/_container_routing.py rename to azure/cosmos/agent_memory/_container_routing.py diff --git a/agent_memory_toolkit/_counters.py b/azure/cosmos/agent_memory/_counters.py similarity index 99% rename from agent_memory_toolkit/_counters.py rename to azure/cosmos/agent_memory/_counters.py index 765e0d7..2223d06 100644 --- a/agent_memory_toolkit/_counters.py +++ b/azure/cosmos/agent_memory/_counters.py @@ -34,7 +34,7 @@ from azure.core import MatchConditions from azure.cosmos.exceptions import CosmosHttpResponseError, CosmosResourceNotFoundError -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.logging import get_logger logger = get_logger(__name__) diff --git a/agent_memory_toolkit/_query_builder.py b/azure/cosmos/agent_memory/_query_builder.py similarity index 100% rename from agent_memory_toolkit/_query_builder.py rename to azure/cosmos/agent_memory/_query_builder.py diff --git a/agent_memory_toolkit/_utils.py b/azure/cosmos/agent_memory/_utils.py similarity index 100% rename from agent_memory_toolkit/_utils.py rename to azure/cosmos/agent_memory/_utils.py diff --git a/agent_memory_toolkit/aio/__init__.py b/azure/cosmos/agent_memory/aio/__init__.py similarity index 62% rename from agent_memory_toolkit/aio/__init__.py rename to azure/cosmos/agent_memory/aio/__init__.py index 7b1723d..c903548 100644 --- a/agent_memory_toolkit/aio/__init__.py +++ b/azure/cosmos/agent_memory/aio/__init__.py @@ -1,12 +1,12 @@ """Async variants of the Agent Memory Toolkit clients. -This subpackage mirrors the sync API surface at ``agent_memory_toolkit`` +This subpackage mirrors the sync API surface at ``azure.cosmos.agent_memory`` and follows the ``azure.cosmos`` / ``azure.cosmos.aio`` convention. """ -from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryClient -from agent_memory_toolkit.aio.embeddings import AsyncEmbeddingsClient -from agent_memory_toolkit.aio.processors import ( +from azure.cosmos.agent_memory.aio.cosmos_memory_client import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio.embeddings import AsyncEmbeddingsClient +from azure.cosmos.agent_memory.aio.processors import ( AsyncDurableFunctionProcessor, AsyncInProcessProcessor, AsyncMemoryProcessor, diff --git a/agent_memory_toolkit/aio/auto_trigger.py b/azure/cosmos/agent_memory/aio/auto_trigger.py similarity index 93% rename from agent_memory_toolkit/aio/auto_trigger.py rename to azure/cosmos/agent_memory/aio/auto_trigger.py index 4c7c963..b0d4c47 100644 --- a/agent_memory_toolkit/aio/auto_trigger.py +++ b/azure/cosmos/agent_memory/aio/auto_trigger.py @@ -1,6 +1,6 @@ """Async per-step threshold auto-trigger. -Mirrors :mod:`agent_memory_toolkit.auto_trigger` for the async client. +Mirrors :mod:`azure.cosmos.agent_memory.auto_trigger` for the async client. """ from __future__ import annotations @@ -10,11 +10,11 @@ from collections.abc import Callable from typing import Any -from agent_memory_toolkit import _counters -from agent_memory_toolkit import thresholds as default_thresholds -from agent_memory_toolkit.aio.processors import AsyncInProcessProcessor -from agent_memory_toolkit.auto_trigger import _threshold_int, _threshold_value -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory import _counters +from azure.cosmos.agent_memory import thresholds as default_thresholds +from azure.cosmos.agent_memory.aio.processors import AsyncInProcessProcessor +from azure.cosmos.agent_memory.auto_trigger import _threshold_int, _threshold_value +from azure.cosmos.agent_memory.logging import get_logger logger = get_logger(__name__) @@ -50,7 +50,7 @@ async def maybe_trigger_steps( *, thresholds: Any = default_thresholds, ) -> None: - """Async counterpart of :func:`agent_memory_toolkit.auto_trigger.maybe_trigger_steps`.""" + """Async counterpart of :func:`azure.cosmos.agent_memory.auto_trigger.maybe_trigger_steps`.""" if counter_container is None or not turn_counts: return if not _should_trigger(processor, thresholds): diff --git a/agent_memory_toolkit/aio/chat.py b/azure/cosmos/agent_memory/aio/chat.py similarity index 97% rename from agent_memory_toolkit/aio/chat.py rename to azure/cosmos/agent_memory/aio/chat.py index 195fdfd..2f0edaf 100644 --- a/agent_memory_toolkit/aio/chat.py +++ b/azure/cosmos/agent_memory/aio/chat.py @@ -11,15 +11,15 @@ import asyncio from typing import Any -from agent_memory_toolkit.chat import ( +from azure.cosmos.agent_memory.chat import ( RETRYABLE_STATUS_CODES, TOKEN_SCOPE, extract_content, resolve_api_version, unsupported_param, ) -from agent_memory_toolkit.exceptions import ConfigurationError -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.exceptions import ConfigurationError +from azure.cosmos.agent_memory.logging import get_logger logger = get_logger(__name__) @@ -62,7 +62,7 @@ async def _provider() -> str: class AsyncChatClient: """Asynchronous LLM chat completion client backed by Azure OpenAI. - Mirrors :class:`agent_memory_toolkit.chat.ChatClient` but uses the + Mirrors :class:`azure.cosmos.agent_memory.chat.ChatClient` but uses the ``openai.AsyncAzureOpenAI`` client and ``asyncio``-aware retry sleeps. """ diff --git a/agent_memory_toolkit/aio/cosmos_memory_client.py b/azure/cosmos/agent_memory/aio/cosmos_memory_client.py similarity index 96% rename from agent_memory_toolkit/aio/cosmos_memory_client.py rename to azure/cosmos/agent_memory/aio/cosmos_memory_client.py index b18c07c..befb775 100644 --- a/agent_memory_toolkit/aio/cosmos_memory_client.py +++ b/azure/cosmos/agent_memory/aio/cosmos_memory_client.py @@ -6,9 +6,9 @@ from datetime import datetime from typing import TYPE_CHECKING, Any, Optional -from agent_memory_toolkit._base import _BaseMemoryClient -from agent_memory_toolkit._container_routing import container_key_for_type -from agent_memory_toolkit._utils import ( +from azure.cosmos.agent_memory._base import _BaseMemoryClient +from azure.cosmos.agent_memory._container_routing import container_key_for_type +from azure.cosmos.agent_memory._utils import ( _build_container_kwargs, _container_policies, _cosmos_container_offer_throughput, @@ -19,18 +19,18 @@ _resolve_full_text_language, _validate_connection, ) -from agent_memory_toolkit.aio.auto_trigger import maybe_trigger_steps -from agent_memory_toolkit.aio.chat import AsyncChatClient -from agent_memory_toolkit.aio.embeddings import AsyncEmbeddingsClient -from agent_memory_toolkit.aio.processors import AsyncInProcessProcessor, AsyncMemoryProcessor -from agent_memory_toolkit.aio.services.pipeline import AsyncPipelineService -from agent_memory_toolkit.aio.store import AsyncMemoryStore -from agent_memory_toolkit.exceptions import CosmosNotConnectedError, CosmosOperationError -from agent_memory_toolkit.logging import get_logger -from agent_memory_toolkit.thresholds import DEFAULT_TTL_BY_TYPE +from azure.cosmos.agent_memory.aio.auto_trigger import maybe_trigger_steps +from azure.cosmos.agent_memory.aio.chat import AsyncChatClient +from azure.cosmos.agent_memory.aio.embeddings import AsyncEmbeddingsClient +from azure.cosmos.agent_memory.aio.processors import AsyncInProcessProcessor, AsyncMemoryProcessor +from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService +from azure.cosmos.agent_memory.aio.store import AsyncMemoryStore +from azure.cosmos.agent_memory.exceptions import CosmosNotConnectedError, CosmosOperationError +from azure.cosmos.agent_memory.logging import get_logger +from azure.cosmos.agent_memory.thresholds import DEFAULT_TTL_BY_TYPE if TYPE_CHECKING: # pragma: no cover - typing-only import - from agent_memory_toolkit.processors.base import ProcessThreadResult, UserSummaryResult # noqa: F401 + from azure.cosmos.agent_memory.processors.base import ProcessThreadResult, UserSummaryResult # noqa: F401 logger = get_logger(__name__) @@ -72,7 +72,7 @@ def _log_auto_trigger_task_failure(task: "asyncio.Task[Any]") -> None: class AsyncCosmosMemoryClient(_BaseMemoryClient): - """Async variant of :class:`agent_memory_toolkit.CosmosMemoryClient`.""" + """Async variant of :class:`azure.cosmos.agent_memory.CosmosMemoryClient`.""" def __init__( self, @@ -806,7 +806,7 @@ async def generate_user_summary( return await self._get_pipeline().generate_user_summary(user_id, thread_ids, recent_k) async def reconcile(self, user_id: str, n: Optional[int] = None) -> dict[str, int]: - from agent_memory_toolkit.thresholds import get_dedup_pool_size + from azure.cosmos.agent_memory.thresholds import get_dedup_pool_size return await self._get_pipeline().reconcile_memories(user_id, n if n is not None else get_dedup_pool_size()) diff --git a/agent_memory_toolkit/aio/embeddings.py b/azure/cosmos/agent_memory/aio/embeddings.py similarity index 96% rename from agent_memory_toolkit/aio/embeddings.py rename to azure/cosmos/agent_memory/aio/embeddings.py index b308543..503de8d 100644 --- a/agent_memory_toolkit/aio/embeddings.py +++ b/azure/cosmos/agent_memory/aio/embeddings.py @@ -10,9 +10,9 @@ import asyncio from typing import Any -from agent_memory_toolkit.chat import resolve_api_version -from agent_memory_toolkit.exceptions import ConfigurationError -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.chat import resolve_api_version +from azure.cosmos.agent_memory.exceptions import ConfigurationError +from azure.cosmos.agent_memory.logging import get_logger logger = get_logger(__name__) diff --git a/agent_memory_toolkit/aio/processors/__init__.py b/azure/cosmos/agent_memory/aio/processors/__init__.py similarity index 88% rename from agent_memory_toolkit/aio/processors/__init__.py rename to azure/cosmos/agent_memory/aio/processors/__init__.py index c29996a..f626d09 100644 --- a/agent_memory_toolkit/aio/processors/__init__.py +++ b/azure/cosmos/agent_memory/aio/processors/__init__.py @@ -1,6 +1,6 @@ """Async pluggable :class:`AsyncMemoryProcessor` backends.""" -from agent_memory_toolkit.processors.base import ( +from azure.cosmos.agent_memory.processors.base import ( ProcessThreadResult, UserSummaryResult, ) diff --git a/agent_memory_toolkit/aio/processors/base.py b/azure/cosmos/agent_memory/aio/processors/base.py similarity index 93% rename from agent_memory_toolkit/aio/processors/base.py rename to azure/cosmos/agent_memory/aio/processors/base.py index 49fb0fc..cdab0c0 100644 --- a/agent_memory_toolkit/aio/processors/base.py +++ b/azure/cosmos/agent_memory/aio/processors/base.py @@ -2,14 +2,14 @@ Re-exports the sync result dataclasses (they are pure data) and defines an ``async``-flavoured Protocol parallel to -:class:`agent_memory_toolkit.processors.base.MemoryProcessor`. +:class:`azure.cosmos.agent_memory.processors.base.MemoryProcessor`. """ from __future__ import annotations from typing import Any, Optional, Protocol, runtime_checkable -from agent_memory_toolkit.processors.base import ( +from azure.cosmos.agent_memory.processors.base import ( ProcessThreadResult, UserSummaryResult, ) diff --git a/agent_memory_toolkit/aio/processors/durable.py b/azure/cosmos/agent_memory/aio/processors/durable.py similarity index 96% rename from agent_memory_toolkit/aio/processors/durable.py rename to azure/cosmos/agent_memory/aio/processors/durable.py index fb414bd..677855d 100644 --- a/agent_memory_toolkit/aio/processors/durable.py +++ b/azure/cosmos/agent_memory/aio/processors/durable.py @@ -4,8 +4,8 @@ from typing import Any, Optional -from agent_memory_toolkit.logging import get_logger -from agent_memory_toolkit.processors.base import ( +from azure.cosmos.agent_memory.logging import get_logger +from azure.cosmos.agent_memory.processors.base import ( ProcessThreadResult, UserSummaryResult, ) diff --git a/agent_memory_toolkit/aio/processors/inprocess.py b/azure/cosmos/agent_memory/aio/processors/inprocess.py similarity index 93% rename from agent_memory_toolkit/aio/processors/inprocess.py rename to azure/cosmos/agent_memory/aio/processors/inprocess.py index 3e710ef..9e10071 100644 --- a/agent_memory_toolkit/aio/processors/inprocess.py +++ b/azure/cosmos/agent_memory/aio/processors/inprocess.py @@ -1,6 +1,6 @@ """Async in-process :class:`AsyncMemoryProcessor` backed by :class:`AsyncPipelineService`. -The underlying :class:`agent_memory_toolkit.aio.services.pipeline.AsyncPipelineService` +The underlying :class:`azure.cosmos.agent_memory.aio.services.pipeline.AsyncPipelineService` exposes native ``async def`` methods, so every call here is a direct ``await`` — no ``asyncio.to_thread`` adapter, no sync sub-clients. """ @@ -10,7 +10,7 @@ import time from typing import Any, Optional -from agent_memory_toolkit.processors.base import ( +from azure.cosmos.agent_memory.processors.base import ( ProcessThreadResult, UserSummaryResult, ) @@ -42,9 +42,9 @@ def __init__( "or `cosmos_container`, `turns_container`, `summaries_container`, " "`chat_client`, and `embeddings_client`." ) - from agent_memory_toolkit._container_routing import ContainerKey - from agent_memory_toolkit.aio.services.pipeline import AsyncPipelineService - from agent_memory_toolkit.aio.store import AsyncMemoryStore + from azure.cosmos.agent_memory._container_routing import ContainerKey + from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService + from azure.cosmos.agent_memory.aio.store import AsyncMemoryStore containers = { ContainerKey.TURNS: turns_container, diff --git a/azure/cosmos/agent_memory/aio/services/__init__.py b/azure/cosmos/agent_memory/aio/services/__init__.py new file mode 100644 index 0000000..23e0fa9 --- /dev/null +++ b/azure/cosmos/agent_memory/aio/services/__init__.py @@ -0,0 +1,12 @@ +"""Async service re-exports. + +Mirrors :mod:`azure.cosmos.agent_memory.services` but only contains the async +variants. Sync code should import from ``azure.cosmos.agent_memory.services``; +async code should import from ``azure.cosmos.agent_memory.aio.services``. +""" + +from __future__ import annotations + +from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService + +__all__ = ["AsyncPipelineService"] diff --git a/agent_memory_toolkit/aio/services/pipeline.py b/azure/cosmos/agent_memory/aio/services/pipeline.py similarity index 98% rename from agent_memory_toolkit/aio/services/pipeline.py rename to azure/cosmos/agent_memory/aio/services/pipeline.py index bf2f1f1..1becf6f 100644 --- a/agent_memory_toolkit/aio/services/pipeline.py +++ b/azure/cosmos/agent_memory/aio/services/pipeline.py @@ -1,9 +1,9 @@ """Async pipeline service for LLM-driven memory extraction, summaries, and reconciliation. This module is the asynchronous sibling of -:class:`agent_memory_toolkit.services.pipeline.PipelineService`. The two +:class:`azure.cosmos.agent_memory.services.pipeline.PipelineService`. The two share all pure helpers via -:mod:`agent_memory_toolkit.services._pipeline_helpers`; only the IO call +:mod:`azure.cosmos.agent_memory.services._pipeline_helpers`; only the IO call sites differ — every Cosmos query, chat completion, and embedding call is ``await``-ed against the async clients/store. """ @@ -24,16 +24,16 @@ CosmosResourceNotFoundError, ) -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit._utils import DEFAULT_TTL_BY_TYPE, compute_content_hash -from agent_memory_toolkit.aio.store import AsyncMemoryStore -from agent_memory_toolkit.exceptions import ( +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory._utils import DEFAULT_TTL_BY_TYPE, compute_content_hash +from azure.cosmos.agent_memory.aio.store import AsyncMemoryStore +from azure.cosmos.agent_memory.exceptions import ( LLMError, MemoryConflictError, ValidationError, ) -from agent_memory_toolkit.logging import get_logger -from agent_memory_toolkit.models import ( +from azure.cosmos.agent_memory.logging import get_logger +from azure.cosmos.agent_memory.models import ( EpisodicRecord, FactRecord, ProceduralRecord, @@ -41,11 +41,11 @@ UserSummaryRecord, construct_internal, ) -from agent_memory_toolkit.prompts._schemas import response_format_for -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.prompts._schemas import response_format_for +from azure.cosmos.agent_memory.services._pipeline_helpers import ( ID_SEED_SEP as _ID_SEED_SEP, ) -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.services._pipeline_helpers import ( VALID_VALENCES, PromptyLoader, build_topic_tags, @@ -55,15 +55,15 @@ coerce_valence, parse_llm_json, ) -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.services._pipeline_helpers import ( is_real_number as _is_real_number, ) -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.services._pipeline_helpers import ( max_or_none as _max_or_none, ) -from agent_memory_toolkit.store._search_helpers import top_literal +from azure.cosmos.agent_memory.store._search_helpers import top_literal -logger = get_logger("agent_memory_toolkit.pipeline.aio") +logger = get_logger("azure.cosmos.agent_memory.pipeline.aio") _coerce_valence = coerce_valence diff --git a/agent_memory_toolkit/aio/store/__init__.py b/azure/cosmos/agent_memory/aio/store/__init__.py similarity index 52% rename from agent_memory_toolkit/aio/store/__init__.py rename to azure/cosmos/agent_memory/aio/store/__init__.py index 7e9e211..bf42364 100644 --- a/agent_memory_toolkit/aio/store/__init__.py +++ b/azure/cosmos/agent_memory/aio/store/__init__.py @@ -1,5 +1,5 @@ """Asynchronous Cosmos DB memory store primitives.""" -from agent_memory_toolkit.aio.store.memory_store import AsyncMemoryStore +from azure.cosmos.agent_memory.aio.store.memory_store import AsyncMemoryStore __all__ = ["AsyncMemoryStore"] diff --git a/agent_memory_toolkit/aio/store/memory_store.py b/azure/cosmos/agent_memory/aio/store/memory_store.py similarity index 98% rename from agent_memory_toolkit/aio/store/memory_store.py rename to azure/cosmos/agent_memory/aio/store/memory_store.py index f87914f..4d70a5a 100644 --- a/agent_memory_toolkit/aio/store/memory_store.py +++ b/azure/cosmos/agent_memory/aio/store/memory_store.py @@ -7,29 +7,29 @@ from datetime import datetime, timezone from typing import Any, Optional -from agent_memory_toolkit._container_routing import ( +from azure.cosmos.agent_memory._container_routing import ( _CONTAINER_FOR_TYPE, ContainerKey, container_key_for_type, ) -from agent_memory_toolkit._query_builder import _QueryBuilder -from agent_memory_toolkit._utils import ( +from azure.cosmos.agent_memory._query_builder import _QueryBuilder +from azure.cosmos.agent_memory._utils import ( _build_memory_query_builder, _coerce_datetime_iso, _validate_hybrid_search, compute_content_hash, new_id, ) -from agent_memory_toolkit.exceptions import ( +from azure.cosmos.agent_memory.exceptions import ( ConfigurationError, CosmosOperationError, MemoryConflictError, MemoryNotFoundError, MemoryTypeMismatchError, ) -from agent_memory_toolkit.logging import get_logger -from agent_memory_toolkit.models import MemoryRecord -from agent_memory_toolkit.store._search_helpers import ( +from azure.cosmos.agent_memory.logging import get_logger +from azure.cosmos.agent_memory.models import MemoryRecord +from azure.cosmos.agent_memory.store._search_helpers import ( add_salience_filter, add_tag_filters, build_search_sql, @@ -39,12 +39,12 @@ require_search_terms, top_literal, ) -from agent_memory_toolkit.store.memory_store import ( +from azure.cosmos.agent_memory.store.memory_store import ( _validate_taggable_type, _validated_memories_types, _wrap_cosmos_exception, ) -from agent_memory_toolkit.thresholds import default_ttl_for +from azure.cosmos.agent_memory.thresholds import default_ttl_for logger = get_logger(__name__) diff --git a/agent_memory_toolkit/auto_trigger.py b/azure/cosmos/agent_memory/auto_trigger.py similarity index 95% rename from agent_memory_toolkit/auto_trigger.py rename to azure/cosmos/agent_memory/auto_trigger.py index 31a0dfb..10ef595 100644 --- a/agent_memory_toolkit/auto_trigger.py +++ b/azure/cosmos/agent_memory/auto_trigger.py @@ -7,7 +7,7 @@ This module owns that orchestration. Both the sync and async clients import :func:`maybe_trigger_steps`; the async variant lives in -:mod:`agent_memory_toolkit.aio.auto_trigger`. +:mod:`azure.cosmos.agent_memory.aio.auto_trigger`. """ from __future__ import annotations @@ -15,10 +15,10 @@ from collections.abc import Mapping from typing import Any -from agent_memory_toolkit import _counters -from agent_memory_toolkit import thresholds as default_thresholds -from agent_memory_toolkit.logging import get_logger -from agent_memory_toolkit.processors import InProcessProcessor +from azure.cosmos.agent_memory import _counters +from azure.cosmos.agent_memory import thresholds as default_thresholds +from azure.cosmos.agent_memory.logging import get_logger +from azure.cosmos.agent_memory.processors import InProcessProcessor logger = get_logger(__name__) @@ -29,7 +29,7 @@ def _threshold_value(source: Any, getter_name: str, mapping_key: str, default: A """Read a threshold from ``source`` falling back to module defaults. ``source`` may be a module exposing ``get_*`` callables (the default - :mod:`agent_memory_toolkit.thresholds` shape), a plain mapping keyed + :mod:`azure.cosmos.agent_memory.thresholds` shape), a plain mapping keyed by env-style names (``FACT_EXTRACTION_EVERY_N``), or an object with matching attributes. """ diff --git a/agent_memory_toolkit/chat.py b/azure/cosmos/agent_memory/chat.py similarity index 98% rename from agent_memory_toolkit/chat.py rename to azure/cosmos/agent_memory/chat.py index 22da052..e036c6c 100644 --- a/agent_memory_toolkit/chat.py +++ b/azure/cosmos/agent_memory/chat.py @@ -5,7 +5,7 @@ built-in retry logic with exponential backoff for rate-limit and transient errors. -The async counterpart lives in :mod:`agent_memory_toolkit.aio.chat` as +The async counterpart lives in :mod:`azure.cosmos.agent_memory.aio.chat` as :class:`AsyncChatClient`. """ @@ -16,7 +16,7 @@ import time from typing import Any -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.logging import get_logger from .exceptions import ConfigurationError, LLMError diff --git a/agent_memory_toolkit/cosmos_memory_client.py b/azure/cosmos/agent_memory/cosmos_memory_client.py similarity index 99% rename from agent_memory_toolkit/cosmos_memory_client.py rename to azure/cosmos/agent_memory/cosmos_memory_client.py index 44ffa93..ec41032 100644 --- a/agent_memory_toolkit/cosmos_memory_client.py +++ b/azure/cosmos/agent_memory/cosmos_memory_client.py @@ -5,7 +5,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Any, Optional -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.logging import get_logger from ._base import _BaseMemoryClient from ._container_routing import container_key_for_type diff --git a/agent_memory_toolkit/embeddings.py b/azure/cosmos/agent_memory/embeddings.py similarity index 98% rename from agent_memory_toolkit/embeddings.py rename to azure/cosmos/agent_memory/embeddings.py index 161da1e..493c25f 100644 --- a/agent_memory_toolkit/embeddings.py +++ b/azure/cosmos/agent_memory/embeddings.py @@ -8,8 +8,8 @@ from typing import Any -from agent_memory_toolkit.chat import resolve_api_version -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.chat import resolve_api_version +from azure.cosmos.agent_memory.logging import get_logger from .exceptions import ConfigurationError diff --git a/agent_memory_toolkit/exceptions.py b/azure/cosmos/agent_memory/exceptions.py similarity index 100% rename from agent_memory_toolkit/exceptions.py rename to azure/cosmos/agent_memory/exceptions.py diff --git a/agent_memory_toolkit/logging.py b/azure/cosmos/agent_memory/logging.py similarity index 94% rename from agent_memory_toolkit/logging.py rename to azure/cosmos/agent_memory/logging.py index 2636e01..624959b 100644 --- a/agent_memory_toolkit/logging.py +++ b/azure/cosmos/agent_memory/logging.py @@ -1,7 +1,7 @@ """Structured logging for the Agent Memory Toolkit. Provides a JSON log formatter and an idempotent :func:`configure_logging` -that attaches a single handler to the ``agent_memory_toolkit`` root logger. +that attaches a single handler to the ``azure.cosmos.agent_memory`` root logger. """ from __future__ import annotations @@ -12,9 +12,9 @@ from datetime import datetime, timezone from typing import Any -from agent_memory_toolkit.exceptions import AgentMemoryError +from azure.cosmos.agent_memory.exceptions import AgentMemoryError -_ROOT_LOGGER_NAME = "agent_memory_toolkit" +_ROOT_LOGGER_NAME = "azure.cosmos.agent_memory" _OPTIONAL_EXTRA_KEYS: tuple[str, ...] = ( "user_id", diff --git a/agent_memory_toolkit/models.py b/azure/cosmos/agent_memory/models.py similarity index 99% rename from agent_memory_toolkit/models.py rename to azure/cosmos/agent_memory/models.py index 3f186d1..6ae3526 100644 --- a/agent_memory_toolkit/models.py +++ b/azure/cosmos/agent_memory/models.py @@ -28,7 +28,7 @@ model_validator, ) -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.logging import get_logger logger = get_logger(__name__) diff --git a/agent_memory_toolkit/processors/__init__.py b/azure/cosmos/agent_memory/processors/__init__.py similarity index 100% rename from agent_memory_toolkit/processors/__init__.py rename to azure/cosmos/agent_memory/processors/__init__.py diff --git a/agent_memory_toolkit/processors/base.py b/azure/cosmos/agent_memory/processors/base.py similarity index 93% rename from agent_memory_toolkit/processors/base.py rename to azure/cosmos/agent_memory/processors/base.py index 4898c4f..271ff98 100644 --- a/agent_memory_toolkit/processors/base.py +++ b/azure/cosmos/agent_memory/processors/base.py @@ -4,8 +4,8 @@ to turn raw turns into thread summaries / extracted memories / deduplicated facts. Two built-in implementations satisfy the protocol: -* :class:`agent_memory_toolkit.processors.inprocess.InProcessProcessor` -* :class:`agent_memory_toolkit.processors.durable.DurableFunctionProcessor` +* :class:`azure.cosmos.agent_memory.processors.inprocess.InProcessProcessor` +* :class:`azure.cosmos.agent_memory.processors.durable.DurableFunctionProcessor` """ from __future__ import annotations @@ -44,7 +44,7 @@ class MemoryProcessor(Protocol): """Backend that turns raw turns into summaries + extracted memories. Implementations must be safe to call from a sync context. The async - mirror lives at :mod:`agent_memory_toolkit.aio.processors`. + mirror lives at :mod:`azure.cosmos.agent_memory.aio.processors`. The protocol exposes both a fused :meth:`process_thread` (used by :meth:`CosmosMemoryClient.process_now`) and per-step methods diff --git a/agent_memory_toolkit/processors/durable.py b/azure/cosmos/agent_memory/processors/durable.py similarity index 98% rename from agent_memory_toolkit/processors/durable.py rename to azure/cosmos/agent_memory/processors/durable.py index 1e1dedc..d63b24f 100644 --- a/agent_memory_toolkit/processors/durable.py +++ b/azure/cosmos/agent_memory/processors/durable.py @@ -10,7 +10,7 @@ from typing import Any, Optional -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.logging import get_logger from .base import ProcessThreadResult, UserSummaryResult diff --git a/agent_memory_toolkit/processors/inprocess.py b/azure/cosmos/agent_memory/processors/inprocess.py similarity index 98% rename from agent_memory_toolkit/processors/inprocess.py rename to azure/cosmos/agent_memory/processors/inprocess.py index 47f6124..5e66e4e 100644 --- a/agent_memory_toolkit/processors/inprocess.py +++ b/azure/cosmos/agent_memory/processors/inprocess.py @@ -12,7 +12,7 @@ class InProcessProcessor: """Runs the summarize → extract → dedup pipeline inline. This is the default backend. Wraps an existing - :class:`agent_memory_toolkit.services.pipeline.PipelineService` instance, + :class:`azure.cosmos.agent_memory.services.pipeline.PipelineService` instance, or constructs one from the supplied container / LLM / embeddings clients. """ diff --git a/agent_memory_toolkit/prompts/__init__.py b/azure/cosmos/agent_memory/prompts/__init__.py similarity index 100% rename from agent_memory_toolkit/prompts/__init__.py rename to azure/cosmos/agent_memory/prompts/__init__.py diff --git a/agent_memory_toolkit/prompts/_schemas.py b/azure/cosmos/agent_memory/prompts/_schemas.py similarity index 100% rename from agent_memory_toolkit/prompts/_schemas.py rename to azure/cosmos/agent_memory/prompts/_schemas.py diff --git a/agent_memory_toolkit/prompts/dedup.prompty b/azure/cosmos/agent_memory/prompts/dedup.prompty similarity index 100% rename from agent_memory_toolkit/prompts/dedup.prompty rename to azure/cosmos/agent_memory/prompts/dedup.prompty diff --git a/agent_memory_toolkit/prompts/extract_memories.prompty b/azure/cosmos/agent_memory/prompts/extract_memories.prompty similarity index 100% rename from agent_memory_toolkit/prompts/extract_memories.prompty rename to azure/cosmos/agent_memory/prompts/extract_memories.prompty diff --git a/agent_memory_toolkit/prompts/summarize.prompty b/azure/cosmos/agent_memory/prompts/summarize.prompty similarity index 100% rename from agent_memory_toolkit/prompts/summarize.prompty rename to azure/cosmos/agent_memory/prompts/summarize.prompty diff --git a/agent_memory_toolkit/prompts/summarize_update.prompty b/azure/cosmos/agent_memory/prompts/summarize_update.prompty similarity index 100% rename from agent_memory_toolkit/prompts/summarize_update.prompty rename to azure/cosmos/agent_memory/prompts/summarize_update.prompty diff --git a/agent_memory_toolkit/prompts/synthesize_procedural.prompty b/azure/cosmos/agent_memory/prompts/synthesize_procedural.prompty similarity index 100% rename from agent_memory_toolkit/prompts/synthesize_procedural.prompty rename to azure/cosmos/agent_memory/prompts/synthesize_procedural.prompty diff --git a/agent_memory_toolkit/prompts/user_summary.prompty b/azure/cosmos/agent_memory/prompts/user_summary.prompty similarity index 100% rename from agent_memory_toolkit/prompts/user_summary.prompty rename to azure/cosmos/agent_memory/prompts/user_summary.prompty diff --git a/agent_memory_toolkit/prompts/user_summary_update.prompty b/azure/cosmos/agent_memory/prompts/user_summary_update.prompty similarity index 100% rename from agent_memory_toolkit/prompts/user_summary_update.prompty rename to azure/cosmos/agent_memory/prompts/user_summary_update.prompty diff --git a/agent_memory_toolkit/services/__init__.py b/azure/cosmos/agent_memory/services/__init__.py similarity index 100% rename from agent_memory_toolkit/services/__init__.py rename to azure/cosmos/agent_memory/services/__init__.py diff --git a/agent_memory_toolkit/services/_pipeline_helpers.py b/azure/cosmos/agent_memory/services/_pipeline_helpers.py similarity index 98% rename from agent_memory_toolkit/services/_pipeline_helpers.py rename to azure/cosmos/agent_memory/services/_pipeline_helpers.py index 041c3eb..8ec86f6 100644 --- a/agent_memory_toolkit/services/_pipeline_helpers.py +++ b/azure/cosmos/agent_memory/services/_pipeline_helpers.py @@ -16,7 +16,7 @@ from pathlib import Path from typing import Any, Optional -from agent_memory_toolkit.exceptions import LLMError +from azure.cosmos.agent_memory.exceptions import LLMError # Separator for deterministic id seeds. Using NUL ensures user_id / # thread_id values can never collide with literal section markers @@ -243,7 +243,7 @@ def parse_llm_json(text: str | None) -> dict[str, Any]: def default_prompts_dir() -> str: - """Default ``prompts/`` directory location: under ``agent_memory_toolkit/``.""" + """Default ``prompts/`` directory location: under ``azure/cosmos/agent_memory/``.""" pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) return os.path.join(pkg_dir, "prompts") @@ -309,7 +309,7 @@ def prepare(self, filename: str, inputs: dict[str, Any]) -> tuple[list[dict[str, # Allowed values for the EpisodicRecord ``outcome_valence`` field — mirrors -# ``agent_memory_toolkit.models._EPISODIC_ALLOWED_VALENCES`` but kept inline +# ``azure.cosmos.agent_memory.models._EPISODIC_ALLOWED_VALENCES`` but kept inline # to avoid an import cycle (helpers must not import models). VALID_VALENCES = frozenset({"positive", "negative", "neutral", "mixed"}) diff --git a/agent_memory_toolkit/services/pipeline.py b/azure/cosmos/agent_memory/services/pipeline.py similarity index 98% rename from agent_memory_toolkit/services/pipeline.py rename to azure/cosmos/agent_memory/services/pipeline.py index 051b24d..effaebf 100644 --- a/agent_memory_toolkit/services/pipeline.py +++ b/azure/cosmos/agent_memory/services/pipeline.py @@ -3,8 +3,8 @@ Owns LLM-call orchestration; all persistence goes through the injected memory store and all model calls go through the injected chat client. Pure helpers (chat-text extraction, transcript building, prompty handling, LLM JSON parsing) -live in :mod:`agent_memory_toolkit.services._pipeline_helpers` and are shared -with :class:`agent_memory_toolkit.aio.services.pipeline.AsyncPipelineService`. +live in :mod:`azure.cosmos.agent_memory.services._pipeline_helpers` and are shared +with :class:`azure.cosmos.agent_memory.aio.services.pipeline.AsyncPipelineService`. """ from __future__ import annotations @@ -22,15 +22,15 @@ CosmosResourceNotFoundError, ) -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit._utils import DEFAULT_TTL_BY_TYPE, compute_content_hash -from agent_memory_toolkit.exceptions import ( +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory._utils import DEFAULT_TTL_BY_TYPE, compute_content_hash +from azure.cosmos.agent_memory.exceptions import ( LLMError, MemoryConflictError, ValidationError, ) -from agent_memory_toolkit.logging import get_logger -from agent_memory_toolkit.models import ( +from azure.cosmos.agent_memory.logging import get_logger +from azure.cosmos.agent_memory.models import ( EpisodicRecord, FactRecord, ProceduralRecord, @@ -38,12 +38,12 @@ UserSummaryRecord, construct_internal, ) -from agent_memory_toolkit.prompts._schemas import response_format_for -from agent_memory_toolkit.services import MemoryStoreProtocol -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.prompts._schemas import response_format_for +from azure.cosmos.agent_memory.services import MemoryStoreProtocol +from azure.cosmos.agent_memory.services._pipeline_helpers import ( ID_SEED_SEP as _ID_SEED_SEP, ) -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.services._pipeline_helpers import ( VALID_VALENCES, PromptyLoader, build_topic_tags, @@ -53,15 +53,15 @@ coerce_valence, parse_llm_json, ) -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.services._pipeline_helpers import ( is_real_number as _is_real_number, ) -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.services._pipeline_helpers import ( max_or_none as _max_or_none, ) -from agent_memory_toolkit.store._search_helpers import top_literal +from azure.cosmos.agent_memory.store._search_helpers import top_literal -logger = get_logger("agent_memory_toolkit.pipeline") +logger = get_logger("azure.cosmos.agent_memory.pipeline") _coerce_valence = coerce_valence diff --git a/agent_memory_toolkit/store/__init__.py b/azure/cosmos/agent_memory/store/__init__.py similarity index 52% rename from agent_memory_toolkit/store/__init__.py rename to azure/cosmos/agent_memory/store/__init__.py index ef1d5ad..2e0da44 100644 --- a/agent_memory_toolkit/store/__init__.py +++ b/azure/cosmos/agent_memory/store/__init__.py @@ -1,5 +1,5 @@ """Cosmos DB memory store primitives (sync).""" -from agent_memory_toolkit.store.memory_store import MemoryStore +from azure.cosmos.agent_memory.store.memory_store import MemoryStore __all__ = ["MemoryStore"] diff --git a/agent_memory_toolkit/store/_search_helpers.py b/azure/cosmos/agent_memory/store/_search_helpers.py similarity index 91% rename from agent_memory_toolkit/store/_search_helpers.py rename to azure/cosmos/agent_memory/store/_search_helpers.py index 7af43d0..b0f6c5e 100644 --- a/agent_memory_toolkit/store/_search_helpers.py +++ b/azure/cosmos/agent_memory/store/_search_helpers.py @@ -1,7 +1,7 @@ """Shared helpers for memory search query construction. -Used by both :class:`agent_memory_toolkit.store.memory_store.MemoryStore` and -:class:`agent_memory_toolkit.aio.store.memory_store.AsyncMemoryStore` to keep +Used by both :class:`azure.cosmos.agent_memory.store.memory_store.MemoryStore` and +:class:`azure.cosmos.agent_memory.aio.store.memory_store.AsyncMemoryStore` to keep search SQL building and result formatting in one place. """ @@ -10,8 +10,8 @@ from collections.abc import Iterable from typing import Any, Optional -from agent_memory_toolkit._query_builder import _QueryBuilder -from agent_memory_toolkit.exceptions import ConfigurationError, ValidationError +from azure.cosmos.agent_memory._query_builder import _QueryBuilder +from azure.cosmos.agent_memory.exceptions import ConfigurationError, ValidationError MEMORY_PROJECTION = ( "c.id, c.user_id, c.thread_id, c.role, c.type, c.content, " diff --git a/agent_memory_toolkit/store/memory_store.py b/azure/cosmos/agent_memory/store/memory_store.py similarity index 98% rename from agent_memory_toolkit/store/memory_store.py rename to azure/cosmos/agent_memory/store/memory_store.py index 71e59f5..5cbe7cd 100644 --- a/agent_memory_toolkit/store/memory_store.py +++ b/azure/cosmos/agent_memory/store/memory_store.py @@ -5,29 +5,29 @@ from datetime import datetime, timezone from typing import Any, Optional -from agent_memory_toolkit._container_routing import ( +from azure.cosmos.agent_memory._container_routing import ( _CONTAINER_FOR_TYPE, ContainerKey, container_key_for_type, ) -from agent_memory_toolkit._query_builder import _QueryBuilder -from agent_memory_toolkit._utils import ( +from azure.cosmos.agent_memory._query_builder import _QueryBuilder +from azure.cosmos.agent_memory._utils import ( _build_memory_query_builder, _coerce_datetime_iso, _validate_hybrid_search, compute_content_hash, new_id, ) -from agent_memory_toolkit.exceptions import ( +from azure.cosmos.agent_memory.exceptions import ( ConfigurationError, CosmosOperationError, MemoryConflictError, MemoryNotFoundError, MemoryTypeMismatchError, ) -from agent_memory_toolkit.logging import get_logger -from agent_memory_toolkit.models import MemoryRecord -from agent_memory_toolkit.store._search_helpers import ( +from azure.cosmos.agent_memory.logging import get_logger +from azure.cosmos.agent_memory.models import MemoryRecord +from azure.cosmos.agent_memory.store._search_helpers import ( add_salience_filter, add_tag_filters, build_search_sql, @@ -37,7 +37,7 @@ require_search_terms, top_literal, ) -from agent_memory_toolkit.thresholds import default_ttl_for +from azure.cosmos.agent_memory.thresholds import default_ttl_for logger = get_logger(__name__) diff --git a/agent_memory_toolkit/thresholds.py b/azure/cosmos/agent_memory/thresholds.py similarity index 99% rename from agent_memory_toolkit/thresholds.py rename to azure/cosmos/agent_memory/thresholds.py index 8c1cc74..104e1e3 100644 --- a/agent_memory_toolkit/thresholds.py +++ b/azure/cosmos/agent_memory/thresholds.py @@ -11,7 +11,7 @@ import os from typing import Optional -from agent_memory_toolkit.logging import get_logger +from azure.cosmos.agent_memory.logging import get_logger logger = get_logger(__name__) diff --git a/function_app/_vendor/agent_memory_toolkit-0.1.0-py3-none-any.whl b/function_app/_vendor/agent_memory_toolkit-0.1.0-py3-none-any.whl deleted file mode 100644 index d84673e854e0d43b66409f87725b5236d32f7ff1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165132 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%0A4j+y)<4GDL}>L)1l)IZf(|~8ywx%e=VA$c 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νL=po(DDzd)ykPy=j1@h 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 diff --git a/function_app/orchestrators/__init__.py b/function_app/orchestrators/__init__.py index 64236cb..395c479 100644 --- a/function_app/orchestrators/__init__.py +++ b/function_app/orchestrators/__init__.py @@ -1,6 +1,6 @@ """Durable orchestrators + activity functions. Each orchestrator is a thin chain of activities that delegate to -:class:`agent_memory_toolkit.services.pipeline.PipelineService`. The pipeline owns +:class:`azure.cosmos.agent_memory.services.pipeline.PipelineService`. The pipeline owns all prompts and business logic; activities are deliberately small. """ diff --git a/function_app/orchestrators/extract_memories.py b/function_app/orchestrators/extract_memories.py index 055a440..1691318 100644 --- a/function_app/orchestrators/extract_memories.py +++ b/function_app/orchestrators/extract_memories.py @@ -123,6 +123,6 @@ def em_ReconcileMemories(payload: dict) -> dict: # operations are larger/more coupled than the extract→persist split handled here. user_id = payload["user_id"] pipeline = get_pipeline() - from agent_memory_toolkit.thresholds import get_dedup_pool_size + from azure.cosmos.agent_memory.thresholds import get_dedup_pool_size return pipeline.reconcile_memories(user_id=user_id, n=get_dedup_pool_size()) or {} diff --git a/function_app/requirements.txt b/function_app/requirements.txt index 3954e06..28d57e8 100644 --- a/function_app/requirements.txt +++ b/function_app/requirements.txt @@ -9,7 +9,7 @@ azure-functions-durable azure-cosmos>=4.16.0 azure-identity>=1.20 -# LLM + prompty (used transitively by agent_memory_toolkit.services.pipeline) +# LLM + prompty (used transitively by azure.cosmos.agent_memory.services.pipeline) openai>=1.60 prompty>=2.0.0a9 jinja2>=3.1.4 @@ -19,4 +19,4 @@ typing_extensions>=4.10 # truth for prompts + business logic). The SDK wheel is vendored under # ``_vendor/`` so the Function app builds without a private package feed. # Rebuild with: ``python -m build --wheel && cp dist/*.whl function_app/_vendor/`` -./_vendor/agent_memory_toolkit-0.1.0-py3-none-any.whl +./_vendor/azure_cosmos_agent_memory-0.1.0b1-py3-none-any.whl diff --git a/function_app/shared/config.py b/function_app/shared/config.py index 1826654..f15ed35 100644 --- a/function_app/shared/config.py +++ b/function_app/shared/config.py @@ -60,7 +60,7 @@ # Defaults documented in local.settings.json.template # --------------------------------------------------------------------------- -from agent_memory_toolkit.thresholds import ( # noqa: E402 +from azure.cosmos.agent_memory.thresholds import ( # noqa: E402 DEFAULT_DEDUP_EVERY_N, DEFAULT_FACT_EXTRACTION_EVERY_N, DEFAULT_PROCEDURAL_SYNTHESIS_AUTO, diff --git a/function_app/shared/counters.py b/function_app/shared/counters.py index 5844636..1ab09cc 100644 --- a/function_app/shared/counters.py +++ b/function_app/shared/counters.py @@ -29,7 +29,7 @@ MAX_RETRIES = 3 # One-shot mismatch-warn dedup. Mirrors the SDK-side pattern in -# ``agent_memory_toolkit/_counters.py`` so the FA also surfaces double-write +# ``azure/cosmos/agent_memory/_counters.py`` so the FA also surfaces double-write # misconfigurations without spamming logs (key = (counter_id, my_owner)). _warned_owner_mismatch: set[tuple[str, str]] = set() diff --git a/function_app/shared/pipeline_factory.py b/function_app/shared/pipeline_factory.py index 695bce5..bcf3a02 100644 --- a/function_app/shared/pipeline_factory.py +++ b/function_app/shared/pipeline_factory.py @@ -1,6 +1,6 @@ """Lazy PipelineService factory (MI auth, sync clients). -The activities reuse :class:`agent_memory_toolkit.services.pipeline.PipelineService` +The activities reuse :class:`azure.cosmos.agent_memory.services.pipeline.PipelineService` verbatim — no business logic is duplicated in the function app. """ @@ -26,12 +26,12 @@ def get_pipeline(): from azure.identity import DefaultAzureCredential - from agent_memory_toolkit._container_routing import ContainerKey - from agent_memory_toolkit._utils import _resolve_embedding_dimensions - from agent_memory_toolkit.chat import ChatClient - from agent_memory_toolkit.embeddings import EmbeddingsClient - from agent_memory_toolkit.services.pipeline import PipelineService - from agent_memory_toolkit.store import MemoryStore + from azure.cosmos.agent_memory._container_routing import ContainerKey + from azure.cosmos.agent_memory._utils import _resolve_embedding_dimensions + from azure.cosmos.agent_memory.chat import ChatClient + from azure.cosmos.agent_memory.embeddings import EmbeddingsClient + from azure.cosmos.agent_memory.services.pipeline import PipelineService + from azure.cosmos.agent_memory.store import MemoryStore credential = DefaultAzureCredential() memories_container = get_memories_container() diff --git a/function_app/triggers/change_feed.py b/function_app/triggers/change_feed.py index 482ad76..c7b2c8b 100644 --- a/function_app/triggers/change_feed.py +++ b/function_app/triggers/change_feed.py @@ -98,7 +98,7 @@ async def process_changefeed_batch( # explicitly to enable the change-feed trigger. Without that opt-in # both backends would double-extract / double-dedup against the same # writes — exactly the customer-day-one footgun this guard prevents. - from agent_memory_toolkit.thresholds import ( + from azure.cosmos.agent_memory.thresholds import ( PROCESSOR_OWNER_DURABLE, get_processor_owner, ) diff --git a/infra/main.bicep b/infra/main.bicep index 6016553..1f4b7e0 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -105,7 +105,7 @@ var planName = '${abbrs.appServicePlan}${resourceToken}' var commonTags = { 'azd-env-name': environmentName - workload: 'agent-memory-toolkit' + workload: 'azure-cosmos-agent-memory' } // --- Resource group ------------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index ee1b05f..ecf9659 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,18 +3,54 @@ requires = ["setuptools>=68.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] -include = ["agent_memory_toolkit*"] +where = ["."] +include = ["azure.cosmos.agent_memory*"] +namespaces = true [tool.setuptools.package-data] -"agent_memory_toolkit" = ["prompts/*.prompty"] +"azure.cosmos.agent_memory" = ["prompts/*.prompty"] [project] -name = "agent-memory-toolkit" -version = "0.1.0" -description = "A Python library for storing, retrieving, and transforming AI agent memories backed by Azure Cosmos DB" +name = "azure-cosmos-agent-memory" +version = "0.1.0b1" +description = "Store, retrieve, and transform AI agent memories backed by Azure Cosmos DB" readme = "README.md" license = {file = "LICENSE"} requires-python = ">=3.11" +authors = [ + {name = "Microsoft Corporation", email = "azurecosmosdb@microsoft.com"}, +] +maintainers = [ + {name = "Microsoft Corporation", email = "azurecosmosdb@microsoft.com"}, +] +keywords = [ + "azure", + "cosmos", + "cosmosdb", + "agent", + "memory", + "llm", + "ai", + "openai", + "rag", + "vector-search", + "semantic-memory", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Framework :: AsyncIO", + "Topic :: Database", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries :: Python Modules", +] dependencies = [ "azure-cosmos>=4.16.0", "azure-identity>=1.20", @@ -26,6 +62,13 @@ dependencies = [ "typing_extensions>=4.10", ] +[project.urls] +Homepage = "https://github.com/AzureCosmosDB/AgentMemoryToolkit" +Repository = "https://github.com/AzureCosmosDB/AgentMemoryToolkit" +Issues = "https://github.com/AzureCosmosDB/AgentMemoryToolkit/issues" +Documentation = "https://github.com/AzureCosmosDB/AgentMemoryToolkit/tree/main/Docs" +Changelog = "https://github.com/AzureCosmosDB/AgentMemoryToolkit/blob/main/CHANGELOG.md" + [project.optional-dependencies] dev = [ "pytest>=8.0", @@ -56,8 +99,8 @@ select = ["E", "F", "I", "W"] "Samples/Notebooks/*.ipynb" = ["E501"] [tool.ruff.lint.isort] -known-first-party = ["agent_memory_toolkit", "activities", "function_app"] +known-first-party = ["azure.cosmos.agent_memory", "activities", "function_app"] known-third-party = ["azure", "openai", "pydantic"] [tool.coverage.run] -source = ["agent_memory_toolkit"] +source = ["azure.cosmos.agent_memory"] diff --git a/tests/integration/test_full_pipeline.py b/tests/integration/test_full_pipeline.py index d9ad636..6e80bd9 100644 --- a/tests/integration/test_full_pipeline.py +++ b/tests/integration/test_full_pipeline.py @@ -25,7 +25,7 @@ import pytest -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient from tests.conftest import INTEGRATION_ENABLED pytestmark = [ diff --git a/tests/integration/test_processor_integration.py b/tests/integration/test_processor_integration.py index 431b08f..1995626 100644 --- a/tests/integration/test_processor_integration.py +++ b/tests/integration/test_processor_integration.py @@ -10,8 +10,8 @@ from unittest.mock import MagicMock -from agent_memory_toolkit.cosmos_memory_client import CosmosMemoryClient -from agent_memory_toolkit.processors import ( +from azure.cosmos.agent_memory.cosmos_memory_client import CosmosMemoryClient +from azure.cosmos.agent_memory.processors import ( DurableFunctionProcessor, InProcessProcessor, ProcessThreadResult, @@ -89,7 +89,7 @@ def test_process_now_is_a_noop(self, caplog): import logging - with caplog.at_level(logging.DEBUG, logger="agent_memory_toolkit.processors.durable"): + with caplog.at_level(logging.DEBUG, logger="azure.cosmos.agent_memory.processors.durable"): result = client.process_now(user_id="u-1", thread_id="th-1") assert isinstance(result, ProcessThreadResult) diff --git a/tests/integration/test_processor_integration_async.py b/tests/integration/test_processor_integration_async.py index d559c9a..f437715 100644 --- a/tests/integration/test_processor_integration_async.py +++ b/tests/integration/test_processor_integration_async.py @@ -6,8 +6,8 @@ import pytest -from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryClient -from agent_memory_toolkit.aio.processors import ( +from azure.cosmos.agent_memory.aio.cosmos_memory_client import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio.processors import ( AsyncDurableFunctionProcessor, AsyncInProcessProcessor, ProcessThreadResult, @@ -91,7 +91,7 @@ async def test_process_now_is_a_noop(self, caplog): import logging - with caplog.at_level(logging.DEBUG, logger="agent_memory_toolkit.aio.processors.durable"): + with caplog.at_level(logging.DEBUG, logger="azure.cosmos.agent_memory.aio.processors.durable"): result = await client.process_now(user_id="u-1", thread_id="th-1") assert isinstance(result, ProcessThreadResult) diff --git a/tests/integration/test_ttl_lifecycle.py b/tests/integration/test_ttl_lifecycle.py index ab67c42..1413d9b 100644 --- a/tests/integration/test_ttl_lifecycle.py +++ b/tests/integration/test_ttl_lifecycle.py @@ -8,7 +8,7 @@ import pytest -from agent_memory_toolkit import CosmosMemoryClient +from azure.cosmos.agent_memory import CosmosMemoryClient from tests.conftest import INTEGRATION_ENABLED pytestmark = [ diff --git a/tests/unit/_base/test_base_client.py b/tests/unit/_base/test_base_client.py index 4a5b9d5..ea44401 100644 --- a/tests/unit/_base/test_base_client.py +++ b/tests/unit/_base/test_base_client.py @@ -4,8 +4,8 @@ import pytest -from agent_memory_toolkit._base import _BaseMemoryClient -from agent_memory_toolkit.exceptions import ConfigurationError, CosmosNotConnectedError +from azure.cosmos.agent_memory._base import _BaseMemoryClient +from azure.cosmos.agent_memory.exceptions import ConfigurationError, CosmosNotConnectedError class DummyClient(_BaseMemoryClient): diff --git a/tests/unit/aio/processors/test_base.py b/tests/unit/aio/processors/test_base.py index 9afb46e..26dc70d 100644 --- a/tests/unit/aio/processors/test_base.py +++ b/tests/unit/aio/processors/test_base.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock -from agent_memory_toolkit.aio.processors import ( +from azure.cosmos.agent_memory.aio.processors import ( AsyncDurableFunctionProcessor, AsyncInProcessProcessor, AsyncMemoryProcessor, diff --git a/tests/unit/aio/processors/test_durable.py b/tests/unit/aio/processors/test_durable.py index 251a9f3..bf89abe 100644 --- a/tests/unit/aio/processors/test_durable.py +++ b/tests/unit/aio/processors/test_durable.py @@ -4,7 +4,7 @@ import pytest -from agent_memory_toolkit.aio.processors import ( +from azure.cosmos.agent_memory.aio.processors import ( AsyncDurableFunctionProcessor, ProcessThreadResult, UserSummaryResult, diff --git a/tests/unit/aio/processors/test_inprocess.py b/tests/unit/aio/processors/test_inprocess.py index ff2b7b3..2d0852b 100644 --- a/tests/unit/aio/processors/test_inprocess.py +++ b/tests/unit/aio/processors/test_inprocess.py @@ -6,7 +6,7 @@ import pytest -from agent_memory_toolkit.aio.processors import AsyncInProcessProcessor, ProcessThreadResult +from azure.cosmos.agent_memory.aio.processors import AsyncInProcessProcessor, ProcessThreadResult @pytest.mark.asyncio diff --git a/tests/unit/aio/processors/test_protocol_satisfaction.py b/tests/unit/aio/processors/test_protocol_satisfaction.py index 273c4da..04cda06 100644 --- a/tests/unit/aio/processors/test_protocol_satisfaction.py +++ b/tests/unit/aio/processors/test_protocol_satisfaction.py @@ -5,7 +5,7 @@ from typing import Any, Optional from unittest.mock import MagicMock -from agent_memory_toolkit.aio.processors import ( +from azure.cosmos.agent_memory.aio.processors import ( AsyncDurableFunctionProcessor, AsyncInProcessProcessor, AsyncMemoryProcessor, diff --git a/tests/unit/aio/store/test_memory_store.py b/tests/unit/aio/store/test_memory_store.py index daea8b8..7d3ba91 100644 --- a/tests/unit/aio/store/test_memory_store.py +++ b/tests/unit/aio/store/test_memory_store.py @@ -5,9 +5,9 @@ import pytest -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.aio.store import AsyncMemoryStore -from agent_memory_toolkit.exceptions import MemoryNotFoundError, MemoryTypeMismatchError +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.aio.store import AsyncMemoryStore +from azure.cosmos.agent_memory.exceptions import MemoryNotFoundError, MemoryTypeMismatchError class AsyncIterator: diff --git a/tests/unit/aio/test_auto_trigger.py b/tests/unit/aio/test_auto_trigger.py index f5e4e3d..edcf382 100644 --- a/tests/unit/aio/test_auto_trigger.py +++ b/tests/unit/aio/test_auto_trigger.py @@ -12,8 +12,8 @@ import pytest -from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryClient -from agent_memory_toolkit.aio.processors import AsyncInProcessProcessor +from azure.cosmos.agent_memory.aio.cosmos_memory_client import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio.processors import AsyncInProcessProcessor class TestAsyncAutoTriggerNonBlocking: @@ -45,7 +45,7 @@ async def fake_upsert(body): client._counter_container_client = MagicMock() with patch( - "agent_memory_toolkit._counters.increment_counter_async", + "azure.cosmos.agent_memory._counters.increment_counter_async", return_value=(0, 1), ): client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") diff --git a/tests/unit/aio/test_chat.py b/tests/unit/aio/test_chat.py index 1010961..1d79132 100644 --- a/tests/unit/aio/test_chat.py +++ b/tests/unit/aio/test_chat.py @@ -1,4 +1,4 @@ -"""Unit tests for agent_memory_toolkit.aio.chat.AsyncChatClient.""" +"""Unit tests for azure.cosmos.agent_memory.aio.chat.AsyncChatClient.""" from __future__ import annotations @@ -6,7 +6,7 @@ import pytest -from agent_memory_toolkit.aio.chat import ( +from azure.cosmos.agent_memory.aio.chat import ( AsyncChatClient, _is_async_credential, _make_sync_token_provider_for_async, diff --git a/tests/unit/aio/test_cosmos_memory_client.py b/tests/unit/aio/test_cosmos_memory_client.py index 16ad49e..3eec695 100644 --- a/tests/unit/aio/test_cosmos_memory_client.py +++ b/tests/unit/aio/test_cosmos_memory_client.py @@ -8,8 +8,8 @@ import pytest -from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryClient -from agent_memory_toolkit.exceptions import ( +from azure.cosmos.agent_memory.aio.cosmos_memory_client import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.exceptions import ( ConfigurationError, CosmosNotConnectedError, MemoryNotFoundError, diff --git a/tests/unit/aio/test_embeddings.py b/tests/unit/aio/test_embeddings.py index 9bea46c..165e6f6 100644 --- a/tests/unit/aio/test_embeddings.py +++ b/tests/unit/aio/test_embeddings.py @@ -4,8 +4,8 @@ import pytest -from agent_memory_toolkit.aio.embeddings import AOAI_EMBEDDING_BATCH_SIZE, AsyncEmbeddingsClient -from agent_memory_toolkit.exceptions import ConfigurationError +from azure.cosmos.agent_memory.aio.embeddings import AOAI_EMBEDDING_BATCH_SIZE, AsyncEmbeddingsClient +from azure.cosmos.agent_memory.exceptions import ConfigurationError # --------------------------------------------------------------------------- # Helpers diff --git a/tests/unit/aio/test_procedural_synthesis.py b/tests/unit/aio/test_procedural_synthesis.py index 5fc5ce2..af2307c 100644 --- a/tests/unit/aio/test_procedural_synthesis.py +++ b/tests/unit/aio/test_procedural_synthesis.py @@ -14,8 +14,8 @@ import pytest -from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryClient -from agent_memory_toolkit.aio.processors import AsyncDurableFunctionProcessor +from azure.cosmos.agent_memory.aio.cosmos_memory_client import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio.processors import AsyncDurableFunctionProcessor class AsyncIterator: diff --git a/tests/unit/aio/test_process_now.py b/tests/unit/aio/test_process_now.py index 7a223eb..6407b56 100644 --- a/tests/unit/aio/test_process_now.py +++ b/tests/unit/aio/test_process_now.py @@ -6,13 +6,13 @@ import pytest -from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryClient -from agent_memory_toolkit.aio.processors import ( +from azure.cosmos.agent_memory.aio.cosmos_memory_client import AsyncCosmosMemoryClient +from azure.cosmos.agent_memory.aio.processors import ( AsyncDurableFunctionProcessor, AsyncInProcessProcessor, ProcessThreadResult, ) -from agent_memory_toolkit.exceptions import CosmosNotConnectedError +from azure.cosmos.agent_memory.exceptions import CosmosNotConnectedError def _connected(processor=None) -> AsyncCosmosMemoryClient: diff --git a/tests/unit/aio/test_reconcile_telemetry.py b/tests/unit/aio/test_reconcile_telemetry.py index 5023b6f..858e328 100644 --- a/tests/unit/aio/test_reconcile_telemetry.py +++ b/tests/unit/aio/test_reconcile_telemetry.py @@ -12,9 +12,9 @@ import pytest -from agent_memory_toolkit.aio.services.pipeline import AsyncPipelineService +from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService -ASYNC_LOGGER_NAME = "agent_memory_toolkit.pipeline.aio" +ASYNC_LOGGER_NAME = "azure.cosmos.agent_memory.pipeline.aio" def _make_async_pipeline() -> AsyncPipelineService: diff --git a/tests/unit/function_app/conftest.py b/tests/unit/function_app/conftest.py index 3340b33..fb62332 100644 --- a/tests/unit/function_app/conftest.py +++ b/tests/unit/function_app/conftest.py @@ -1,7 +1,7 @@ """Path bootstrap for function-app unit tests. The function-app source lives in ``function_app/`` (a sibling of the SDK -``agent_memory_toolkit/``) and is *not* a package — Azure Functions discovers +``azure/cosmos/agent_memory/``) and is *not* a package — Azure Functions discovers modules by file name. We add the directory to ``sys.path`` here so tests can ``import shared.counters``, ``import triggers.change_feed`` etc. without each test file having to repeat the ``sys.path`` dance. diff --git a/tests/unit/function_app/test_pipeline_factory.py b/tests/unit/function_app/test_pipeline_factory.py index 2f1b1f9..c004962 100644 --- a/tests/unit/function_app/test_pipeline_factory.py +++ b/tests/unit/function_app/test_pipeline_factory.py @@ -56,10 +56,10 @@ def mocks(monkeypatch): patches = [ patch("azure.identity.DefaultAzureCredential", credential_ctor), - patch("agent_memory_toolkit.chat.ChatClient", chat_ctor), - patch("agent_memory_toolkit.embeddings.EmbeddingsClient", embed_ctor), - patch("agent_memory_toolkit.store.MemoryStore", store_ctor), - patch("agent_memory_toolkit.services.pipeline.PipelineService", pipeline_ctor), + patch("azure.cosmos.agent_memory.chat.ChatClient", chat_ctor), + patch("azure.cosmos.agent_memory.embeddings.EmbeddingsClient", embed_ctor), + patch("azure.cosmos.agent_memory.store.MemoryStore", store_ctor), + patch("azure.cosmos.agent_memory.services.pipeline.PipelineService", pipeline_ctor), ] for p in patches: p.start() @@ -91,7 +91,7 @@ def test_builds_pipeline_from_complete_env(mocks): result = pipeline_factory.get_pipeline() assert result is mocks.pipeline_instance - from agent_memory_toolkit._container_routing import ContainerKey + from azure.cosmos.agent_memory._container_routing import ContainerKey expected_containers = { ContainerKey.TURNS: mocks.turns_container, diff --git a/tests/unit/processors/test_base.py b/tests/unit/processors/test_base.py index be0be16..b7e8f10 100644 --- a/tests/unit/processors/test_base.py +++ b/tests/unit/processors/test_base.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock -from agent_memory_toolkit.processors import ( +from azure.cosmos.agent_memory.processors import ( DurableFunctionProcessor, InProcessProcessor, MemoryProcessor, diff --git a/tests/unit/processors/test_durable.py b/tests/unit/processors/test_durable.py index 3a7d6cb..6625b57 100644 --- a/tests/unit/processors/test_durable.py +++ b/tests/unit/processors/test_durable.py @@ -2,7 +2,7 @@ from __future__ import annotations -from agent_memory_toolkit.processors import ( +from azure.cosmos.agent_memory.processors import ( DurableFunctionProcessor, ProcessThreadResult, UserSummaryResult, diff --git a/tests/unit/processors/test_inprocess.py b/tests/unit/processors/test_inprocess.py index 635b11a..be8d841 100644 --- a/tests/unit/processors/test_inprocess.py +++ b/tests/unit/processors/test_inprocess.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock -from agent_memory_toolkit.processors import InProcessProcessor, ProcessThreadResult +from azure.cosmos.agent_memory.processors import InProcessProcessor, ProcessThreadResult def test_process_thread_calls_summarize_extract_reconcile_in_order(): diff --git a/tests/unit/processors/test_protocol_satisfaction.py b/tests/unit/processors/test_protocol_satisfaction.py index 852b14a..dc63c98 100644 --- a/tests/unit/processors/test_protocol_satisfaction.py +++ b/tests/unit/processors/test_protocol_satisfaction.py @@ -5,7 +5,7 @@ from typing import Any, Optional from unittest.mock import MagicMock -from agent_memory_toolkit.processors import ( +from azure.cosmos.agent_memory.processors import ( DurableFunctionProcessor, InProcessProcessor, MemoryProcessor, diff --git a/tests/unit/prompts/test_schema_prompty_conformance.py b/tests/unit/prompts/test_schema_prompty_conformance.py index a8137cf..6d8093c 100644 --- a/tests/unit/prompts/test_schema_prompty_conformance.py +++ b/tests/unit/prompts/test_schema_prompty_conformance.py @@ -1,6 +1,6 @@ """Conformance tests: every strict JSON schema must match its prompty body. -The strict ``response_format`` schemas in ``agent_memory_toolkit.prompts._schemas`` +The strict ``response_format`` schemas in ``azure.cosmos.agent_memory.prompts._schemas`` are paired with prompty files that describe the expected output shape in markdown JSON code-fences. If the two drift apart: @@ -25,10 +25,10 @@ import pytest -from agent_memory_toolkit.prompts._schemas import PROMPTY_SCHEMAS +from azure.cosmos.agent_memory.prompts._schemas import PROMPTY_SCHEMAS _REPO_ROOT = Path(__file__).resolve().parents[3] -_PROMPTS_DIR = _REPO_ROOT / "agent_memory_toolkit" / "prompts" +_PROMPTS_DIR = _REPO_ROOT / "azure" / "cosmos" / "agent_memory" / "prompts" _JSON_BLOCK_RE = re.compile(r"```json\s*\n(.*?)```", re.DOTALL) @@ -131,7 +131,7 @@ def test_prompty_describes_schema_shape(filename: str, schema_entry: tuple[str, f" prompty top-level keys per block: {diagnostic_blocks}\n" f"\nFix one of:\n" f" - Add or update an example so required keys appear at the top\n" - f" - Update the schema in agent_memory_toolkit/prompts/_schemas.py\n" + f" - Update the schema in azure/cosmos/agent_memory/prompts/_schemas.py\n" f" if the prompty's intent has changed." ) diff --git a/tests/unit/services/test_chaos_extract_persist.py b/tests/unit/services/test_chaos_extract_persist.py index e6c2a40..d12e61b 100644 --- a/tests/unit/services/test_chaos_extract_persist.py +++ b/tests/unit/services/test_chaos_extract_persist.py @@ -6,9 +6,9 @@ import pytest from azure.cosmos.exceptions import CosmosResourceExistsError -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.aio.services.pipeline import AsyncPipelineService, _AsyncStoreContainerAdapter -from agent_memory_toolkit.services.pipeline import PipelineService, _StoreContainerAdapter +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService, _AsyncStoreContainerAdapter +from azure.cosmos.agent_memory.services.pipeline import PipelineService, _StoreContainerAdapter class _FlakyContainer: diff --git a/tests/unit/services/test_extract_dry.py b/tests/unit/services/test_extract_dry.py index 61fafd9..c7686e6 100644 --- a/tests/unit/services/test_extract_dry.py +++ b/tests/unit/services/test_extract_dry.py @@ -5,9 +5,9 @@ import pytest -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.aio.services.pipeline import AsyncPipelineService, _AsyncStoreContainerAdapter -from agent_memory_toolkit.services.pipeline import PipelineService, _StoreContainerAdapter +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService, _AsyncStoreContainerAdapter +from azure.cosmos.agent_memory.services.pipeline import PipelineService, _StoreContainerAdapter class _SyncChat: diff --git a/tests/unit/services/test_persist_extracted.py b/tests/unit/services/test_persist_extracted.py index bccf002..01907f7 100644 --- a/tests/unit/services/test_persist_extracted.py +++ b/tests/unit/services/test_persist_extracted.py @@ -7,11 +7,11 @@ import pytest from azure.cosmos.exceptions import CosmosResourceExistsError -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit._utils import compute_content_hash -from agent_memory_toolkit.aio.services.pipeline import AsyncPipelineService, _AsyncStoreContainerAdapter -from agent_memory_toolkit.services._pipeline_helpers import ID_SEED_SEP -from agent_memory_toolkit.services.pipeline import PipelineService, _StoreContainerAdapter +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory._utils import compute_content_hash +from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService, _AsyncStoreContainerAdapter +from azure.cosmos.agent_memory.services._pipeline_helpers import ID_SEED_SEP +from azure.cosmos.agent_memory.services.pipeline import PipelineService, _StoreContainerAdapter class _Container: diff --git a/tests/unit/services/test_pipeline_service.py b/tests/unit/services/test_pipeline_service.py index 41070a6..b9fa81c 100644 --- a/tests/unit/services/test_pipeline_service.py +++ b/tests/unit/services/test_pipeline_service.py @@ -6,8 +6,8 @@ import pytest -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.services.pipeline import PipelineService, _StoreContainerAdapter +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.services.pipeline import PipelineService, _StoreContainerAdapter class FakeLLMService: diff --git a/tests/unit/services/test_prompty_loader.py b/tests/unit/services/test_prompty_loader.py index 80b327e..daa1ee6 100644 --- a/tests/unit/services/test_prompty_loader.py +++ b/tests/unit/services/test_prompty_loader.py @@ -4,7 +4,7 @@ from pathlib import Path -from agent_memory_toolkit.services._pipeline_helpers import ( +from azure.cosmos.agent_memory.services._pipeline_helpers import ( DEFAULT_PROMPT_VERSION, PromptyLoader, _read_prompty_version, diff --git a/tests/unit/services/test_user_summary_topic_tags.py b/tests/unit/services/test_user_summary_topic_tags.py index 03318b7..f766a32 100644 --- a/tests/unit/services/test_user_summary_topic_tags.py +++ b/tests/unit/services/test_user_summary_topic_tags.py @@ -2,8 +2,8 @@ from unittest.mock import AsyncMock, MagicMock -from agent_memory_toolkit.aio.services.pipeline import AsyncPipelineService -from agent_memory_toolkit.services.pipeline import PipelineService +from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService +from azure.cosmos.agent_memory.services.pipeline import PipelineService def _user_summary_doc() -> dict: diff --git a/tests/unit/store/test_list_tags.py b/tests/unit/store/test_list_tags.py index e80b0c3..be0c653 100644 --- a/tests/unit/store/test_list_tags.py +++ b/tests/unit/store/test_list_tags.py @@ -2,9 +2,9 @@ from unittest.mock import MagicMock -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.aio.store import AsyncMemoryStore -from agent_memory_toolkit.store import MemoryStore +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.aio.store import AsyncMemoryStore +from azure.cosmos.agent_memory.store import MemoryStore class AsyncIterator: diff --git a/tests/unit/store/test_memory_store.py b/tests/unit/store/test_memory_store.py index 5f3908b..b9b06a6 100644 --- a/tests/unit/store/test_memory_store.py +++ b/tests/unit/store/test_memory_store.py @@ -5,9 +5,9 @@ import pytest -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.exceptions import MemoryNotFoundError, MemoryTypeMismatchError -from agent_memory_toolkit.store import MemoryStore +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.exceptions import MemoryNotFoundError, MemoryTypeMismatchError +from azure.cosmos.agent_memory.store import MemoryStore def _doc(**overrides): diff --git a/tests/unit/store/test_tag_mutation.py b/tests/unit/store/test_tag_mutation.py index f19c101..725f6de 100644 --- a/tests/unit/store/test_tag_mutation.py +++ b/tests/unit/store/test_tag_mutation.py @@ -6,10 +6,10 @@ from azure.core import MatchConditions from azure.cosmos.exceptions import CosmosAccessConditionFailedError -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.aio.store import AsyncMemoryStore -from agent_memory_toolkit.exceptions import MemoryConflictError -from agent_memory_toolkit.store import MemoryStore +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.aio.store import AsyncMemoryStore +from azure.cosmos.agent_memory.exceptions import MemoryConflictError +from azure.cosmos.agent_memory.store import MemoryStore def _doc(etag: str, tags: list[str]) -> dict: diff --git a/tests/unit/test_async_hygiene.py b/tests/unit/test_async_hygiene.py index e714076..7debcf3 100644 --- a/tests/unit/test_async_hygiene.py +++ b/tests/unit/test_async_hygiene.py @@ -1,4 +1,4 @@ -"""Async hygiene regression guards for ``agent_memory_toolkit/aio/``. +"""Async hygiene regression guards for ``azure/cosmos/agent_memory/aio/``. These tests programmatically inspect the async subpackage to catch regressions where contributors might inadvertently re-introduce sync I/O on @@ -17,7 +17,7 @@ import ast from pathlib import Path -import agent_memory_toolkit.aio as _aio_pkg +import azure.cosmos.agent_memory.aio as _aio_pkg # Each entry is ``(relative_path_under_aio, name_of_an_enclosing_function)``. # A call site matches if *any* function in its enclosing chain has the @@ -102,7 +102,7 @@ def test_asyncio_to_thread_only_in_allowed_call_sites() -> None: unexpected = [(rel, lineno, chain) for (rel, lineno, chain) in hits if not _is_allowed(rel, chain)] assert not unexpected, ( "Unexpected asyncio.to_thread call site(s) found in " - "agent_memory_toolkit/aio/. Each new to_thread reintroduces sync " + "azure/cosmos/agent_memory/aio/. Each new to_thread reintroduces sync " "I/O onto the event loop and must be either removed or explicitly " "added to ALLOWED_CALL_SITES in this test with justification.\n" f" Unexpected sites: {unexpected}\n" @@ -111,7 +111,7 @@ def test_asyncio_to_thread_only_in_allowed_call_sites() -> None: assert len(hits) == EXPECTED_TOTAL_CALL_SITES, ( f"Expected exactly {EXPECTED_TOTAL_CALL_SITES} asyncio.to_thread call " - f"sites in agent_memory_toolkit/aio/; found {len(hits)}.\n" + f"sites in azure/cosmos/agent_memory/aio/; found {len(hits)}.\n" f" All sites: {hits}\n" "If an allowed site was intentionally removed, drop the corresponding " "entry from ALLOWED_CALL_SITES and update EXPECTED_TOTAL_CALL_SITES." diff --git a/tests/unit/test_auto_trigger.py b/tests/unit/test_auto_trigger.py index d634121..03c5420 100644 --- a/tests/unit/test_auto_trigger.py +++ b/tests/unit/test_auto_trigger.py @@ -10,8 +10,8 @@ from unittest.mock import MagicMock, patch -from agent_memory_toolkit.cosmos_memory_client import CosmosMemoryClient -from agent_memory_toolkit.processors import DurableFunctionProcessor, InProcessProcessor +from azure.cosmos.agent_memory.cosmos_memory_client import CosmosMemoryClient +from azure.cosmos.agent_memory.processors import DurableFunctionProcessor, InProcessProcessor def _connected(processor=None) -> CosmosMemoryClient: @@ -37,7 +37,7 @@ def test_push_to_cosmos_fires_inprocess_trigger_per_turn(monkeypatch): client._processor._pipeline = pipeline with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", return_value=(0, 1), ): client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") @@ -52,7 +52,7 @@ def test_push_to_cosmos_durable_does_not_fire_trigger(monkeypatch): client._counter_container_client = MagicMock() with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", return_value=(0, 1), ) as inc: client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") @@ -70,7 +70,7 @@ def test_push_to_cosmos_skips_trigger_when_thresholds_zero(monkeypatch): client._counter_container_client = MagicMock() with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", return_value=(0, 1), ) as inc: client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") @@ -89,7 +89,7 @@ def test_push_to_cosmos_swallows_trigger_failures(monkeypatch): client._counter_container_client = MagicMock() with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", return_value=(0, 1), ): client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") @@ -137,7 +137,7 @@ def test_extract_fires_independently_of_summary(self, monkeypatch): client._counter_container_client = MagicMock() with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", return_value=(0, 1), # crosses 1 only ): client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") @@ -161,7 +161,7 @@ def test_summary_fires_independently_when_threshold_crossed(self, monkeypatch): client._counter_container_client = MagicMock() with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", return_value=(9, 10), # crosses 10 only ): client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") @@ -184,7 +184,7 @@ def test_user_summary_fires_at_user_threshold(self, monkeypatch): # Thread counter: (0,1) then (1,2); user counter: (1,2) crosses 2. with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", side_effect=[(0, 1), (1, 2), (1, 2)], ): client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") @@ -213,7 +213,7 @@ def test_durable_owner_suppresses_sdk_trigger(self, monkeypatch): client._counter_container_client = MagicMock() with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", return_value=(0, 1), ) as inc: client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") @@ -235,7 +235,7 @@ def test_inprocess_owner_allows_sdk_trigger(self, monkeypatch): client._counter_container_client = MagicMock() with patch( - "agent_memory_toolkit._counters.increment_counter_sync", + "azure.cosmos.agent_memory._counters.increment_counter_sync", return_value=(0, 1), ): client.add_local(user_id="u1", role="user", thread_id="t1", content="hi") diff --git a/tests/unit/test_chat.py b/tests/unit/test_chat.py index 5e0cbd0..a5ad5d9 100644 --- a/tests/unit/test_chat.py +++ b/tests/unit/test_chat.py @@ -1,4 +1,4 @@ -"""Unit tests for agent_memory_toolkit.chat.ChatClient (sync).""" +"""Unit tests for azure.cosmos.agent_memory.chat.ChatClient (sync).""" from __future__ import annotations @@ -6,8 +6,8 @@ import pytest -from agent_memory_toolkit.chat import ChatClient -from agent_memory_toolkit.exceptions import ConfigurationError +from azure.cosmos.agent_memory.chat import ChatClient +from azure.cosmos.agent_memory.exceptions import ConfigurationError # --------------------------------------------------------------------------- # Initialization diff --git a/tests/unit/test_container_routing.py b/tests/unit/test_container_routing.py index 63977b0..3508b2c 100644 --- a/tests/unit/test_container_routing.py +++ b/tests/unit/test_container_routing.py @@ -10,13 +10,13 @@ import pytest -from agent_memory_toolkit._container_routing import ( +from azure.cosmos.agent_memory._container_routing import ( _CONTAINER_FOR_TYPE, ContainerKey, container_key_for_type, container_keys_for_types, ) -from agent_memory_toolkit._utils import VALID_TYPES +from azure.cosmos.agent_memory._utils import VALID_TYPES class TestContainerKey: diff --git a/tests/unit/test_cosmos_memory_client.py b/tests/unit/test_cosmos_memory_client.py index 56d62d3..4c79eab 100644 --- a/tests/unit/test_cosmos_memory_client.py +++ b/tests/unit/test_cosmos_memory_client.py @@ -8,8 +8,8 @@ import pytest -from agent_memory_toolkit.cosmos_memory_client import CosmosMemoryClient -from agent_memory_toolkit.exceptions import ( +from azure.cosmos.agent_memory.cosmos_memory_client import CosmosMemoryClient +from azure.cosmos.agent_memory.exceptions import ( ConfigurationError, CosmosNotConnectedError, MemoryNotFoundError, diff --git a/tests/unit/test_counters.py b/tests/unit/test_counters.py index 8cd16e8..5ebd996 100644 --- a/tests/unit/test_counters.py +++ b/tests/unit/test_counters.py @@ -1,4 +1,4 @@ -"""Tests for ``agent_memory_toolkit._counters``. +"""Tests for ``azure.cosmos.agent_memory._counters``. Covers counter-doc construction (LSN preservation, failure breadcrumbs) and ``stamp_failure_sync`` — the helpers that let SDK and FA share a @@ -9,7 +9,7 @@ from unittest.mock import MagicMock -from agent_memory_toolkit._counters import _build_counter_doc, stamp_failure_sync +from azure.cosmos.agent_memory._counters import _build_counter_doc, stamp_failure_sync class TestBuildCounterDoc: diff --git a/tests/unit/test_embeddings.py b/tests/unit/test_embeddings.py index c04e328..9616138 100644 --- a/tests/unit/test_embeddings.py +++ b/tests/unit/test_embeddings.py @@ -6,8 +6,8 @@ import pytest -from agent_memory_toolkit.embeddings import AOAI_EMBEDDING_BATCH_SIZE, EmbeddingsClient -from agent_memory_toolkit.exceptions import ConfigurationError +from azure.cosmos.agent_memory.embeddings import AOAI_EMBEDDING_BATCH_SIZE, EmbeddingsClient +from azure.cosmos.agent_memory.exceptions import ConfigurationError # --------------------------------------------------------------------------- # Helpers diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index bf520c9..7b36baf 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -1,8 +1,8 @@ -"""Unit tests for agent_memory_toolkit.exceptions.""" +"""Unit tests for azure.cosmos.agent_memory.exceptions.""" import pytest -from agent_memory_toolkit.exceptions import ( +from azure.cosmos.agent_memory.exceptions import ( AgentMemoryError, ConfigurationError, CosmosNotConnectedError, diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index 00db5c8..693d555 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -8,8 +8,8 @@ import pytest -from agent_memory_toolkit.exceptions import MemoryNotFoundError, ValidationError -from agent_memory_toolkit.logging import ( +from azure.cosmos.agent_memory.exceptions import MemoryNotFoundError, ValidationError +from azure.cosmos.agent_memory.logging import ( JsonFormatter, configure_logging, get_logger, @@ -18,7 +18,7 @@ @pytest.fixture(autouse=True) def _reset_root_logger(): - root = logging.getLogger("agent_memory_toolkit") + root = logging.getLogger("azure.cosmos.agent_memory") saved_handlers = list(root.handlers) saved_level = root.level for h in list(root.handlers): @@ -39,7 +39,7 @@ def _render(record: logging.LogRecord) -> dict: class TestJsonFormatter: def _make_record(self, **extra) -> logging.LogRecord: record = logging.LogRecord( - name="agent_memory_toolkit.test", + name="azure.cosmos.agent_memory.test", level=logging.INFO, pathname=__file__, lineno=1, @@ -54,7 +54,7 @@ def _make_record(self, **extra) -> logging.LogRecord: def test_minimal_payload_shape(self): payload = _render(self._make_record()) assert payload["level"] == "INFO" - assert payload["logger"] == "agent_memory_toolkit.test" + assert payload["logger"] == "azure.cosmos.agent_memory.test" assert payload["msg"] == "hello world" assert "ts" in payload # No correlation_id in the payload at all. @@ -92,7 +92,7 @@ def test_exception_info_serialised(self): import sys record = logging.LogRecord( - name="agent_memory_toolkit.test", + name="azure.cosmos.agent_memory.test", level=logging.ERROR, pathname=__file__, lineno=1, @@ -112,7 +112,7 @@ def test_error_code_from_agent_memory_error_subclass(self): import sys record = logging.LogRecord( - name="agent_memory_toolkit.test", + name="azure.cosmos.agent_memory.test", level=logging.ERROR, pathname=__file__, lineno=1, @@ -132,7 +132,7 @@ def test_single_line_output(self): class TestConfigureLogging: def test_attaches_single_handler(self): configure_logging(force_json=True) - root = logging.getLogger("agent_memory_toolkit") + root = logging.getLogger("azure.cosmos.agent_memory") managed = [h for h in root.handlers if getattr(h, "_amt_managed", False)] assert len(managed) == 1 @@ -140,23 +140,23 @@ def test_is_idempotent(self): configure_logging(force_json=True) configure_logging(force_json=True) configure_logging(force_json=True) - root = logging.getLogger("agent_memory_toolkit") + root = logging.getLogger("azure.cosmos.agent_memory") managed = [h for h in root.handlers if getattr(h, "_amt_managed", False)] assert len(managed) == 1 def test_force_json_uses_json_formatter(self): configure_logging(force_json=True) - root = logging.getLogger("agent_memory_toolkit") + root = logging.getLogger("azure.cosmos.agent_memory") managed = [h for h in root.handlers if getattr(h, "_amt_managed", False)] assert isinstance(managed[0].formatter, JsonFormatter) def test_get_logger_participates_in_root(self): - logger = get_logger("agent_memory_toolkit.some.submodule") + logger = get_logger("azure.cosmos.agent_memory.some.submodule") buf = io.StringIO() capture = logging.StreamHandler(buf) capture.setFormatter(JsonFormatter()) capture._amt_managed = True # type: ignore[attr-defined] - root = logging.getLogger("agent_memory_toolkit") + root = logging.getLogger("azure.cosmos.agent_memory") for h in list(root.handlers): root.removeHandler(h) root.addHandler(capture) @@ -166,4 +166,4 @@ def test_get_logger_participates_in_root(self): assert payload["msg"] == "ping" assert payload["operation"] == "test_op" assert payload["user_id"] == "u9" - assert payload["logger"] == "agent_memory_toolkit.some.submodule" + assert payload["logger"] == "azure.cosmos.agent_memory.some.submodule" diff --git a/tests/unit/test_memory_type_multi.py b/tests/unit/test_memory_type_multi.py index cd6449c..28642ec 100644 --- a/tests/unit/test_memory_type_multi.py +++ b/tests/unit/test_memory_type_multi.py @@ -12,8 +12,8 @@ from unittest.mock import MagicMock -from agent_memory_toolkit._utils import _build_memory_query_builder -from agent_memory_toolkit.cosmos_memory_client import CosmosMemoryClient +from azure.cosmos.agent_memory._utils import _build_memory_query_builder +from azure.cosmos.agent_memory.cosmos_memory_client import CosmosMemoryClient # --------------------------------------------------------------------------- # Helpers — a small replica of the patterns used in test_cosmos_memory_client. diff --git a/tests/unit/test_min_confidence.py b/tests/unit/test_min_confidence.py index e690dd8..487f30e 100644 --- a/tests/unit/test_min_confidence.py +++ b/tests/unit/test_min_confidence.py @@ -2,7 +2,7 @@ from __future__ import annotations -from agent_memory_toolkit._utils import _build_memory_query_builder +from azure.cosmos.agent_memory._utils import _build_memory_query_builder def test_min_confidence_adds_predicate_when_set(): diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 0c254d3..f89c9d5 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -1,4 +1,4 @@ -"""Unit tests for agent_memory_toolkit.models. +"""Unit tests for azure.cosmos.agent_memory.models. Each typed subclass (TurnRecord / ThreadSummaryRecord / UserSummaryRecord / FactRecord / EpisodicRecord / ProceduralRecord) gets its own focused class @@ -16,7 +16,7 @@ import pydantic import pytest -from agent_memory_toolkit.models import ( +from azure.cosmos.agent_memory.models import ( EpisodicRecord, FactRecord, MemoryRecord, diff --git a/tests/unit/test_pipeline_confidence.py b/tests/unit/test_pipeline_confidence.py index 0096a77..84eb6aa 100644 --- a/tests/unit/test_pipeline_confidence.py +++ b/tests/unit/test_pipeline_confidence.py @@ -8,9 +8,9 @@ import pytest from azure.cosmos.exceptions import CosmosResourceNotFoundError -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.services.pipeline import PipelineService -from agent_memory_toolkit.store import MemoryStore +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.services.pipeline import PipelineService +from azure.cosmos.agent_memory.store import MemoryStore def _make_pipeline(llm_response: dict): @@ -164,7 +164,7 @@ class TestMarkSupersededDoesNotMutate: def test_input_dict_unchanged_on_success(self): from azure.core import MatchConditions - from agent_memory_toolkit.services.pipeline import PipelineService + from azure.cosmos.agent_memory.services.pipeline import PipelineService pipeline = PipelineService.__new__(PipelineService) pipeline._store = None @@ -189,7 +189,7 @@ def test_input_dict_unchanged_on_success(self): def test_input_dict_unchanged_on_failure(self): from azure.cosmos.exceptions import CosmosAccessConditionFailedError - from agent_memory_toolkit.services.pipeline import PipelineService + from azure.cosmos.agent_memory.services.pipeline import PipelineService pipeline = PipelineService.__new__(PipelineService) pipeline._store = None @@ -221,7 +221,7 @@ class TestGenerateUserSummaryThreadIdsObservabilityOnly: """ def _build_pipeline(self): - from agent_memory_toolkit.services.pipeline import PipelineService + from azure.cosmos.agent_memory.services.pipeline import PipelineService pipeline = PipelineService.__new__(PipelineService) pipeline._embeddings = MagicMock() @@ -304,7 +304,7 @@ def test_extract_scoped_intent_without_outcome_stores_correctly(caplog): } ) - with caplog.at_level("WARNING", logger="agent_memory_toolkit.pipeline"): + with caplog.at_level("WARNING", logger="azure.cosmos.agent_memory.pipeline"): pipeline.extract_memories("u1", "t1") eps = [d for d in upserted if d["type"] == "episodic"] @@ -403,7 +403,7 @@ def test_extract_drops_episodic_missing_scope_type(caplog): } ) - with caplog.at_level("WARNING", logger="agent_memory_toolkit.pipeline"): + with caplog.at_level("WARNING", logger="azure.cosmos.agent_memory.pipeline"): pipeline.extract_memories("u1", "t1") assert not any(d["type"] == "episodic" for d in upserted) @@ -424,7 +424,7 @@ def test_extract_drops_episodic_missing_scope_value(caplog): } ) - with caplog.at_level("WARNING", logger="agent_memory_toolkit.pipeline"): + with caplog.at_level("WARNING", logger="azure.cosmos.agent_memory.pipeline"): pipeline.extract_memories("u1", "t1") assert not any(d["type"] == "episodic" for d in upserted) @@ -456,7 +456,7 @@ def test_extract_drops_episodic_with_blank_or_invalid_scope(scope_type, scope_va } ) - with caplog.at_level("WARNING", logger="agent_memory_toolkit.pipeline"): + with caplog.at_level("WARNING", logger="azure.cosmos.agent_memory.pipeline"): pipeline.extract_memories("u1", "t1") assert not any(d["type"] == "episodic" for d in upserted) diff --git a/tests/unit/test_procedural_synthesis.py b/tests/unit/test_procedural_synthesis.py index 14078c2..c92d5b5 100644 --- a/tests/unit/test_procedural_synthesis.py +++ b/tests/unit/test_procedural_synthesis.py @@ -8,11 +8,11 @@ import pytest -from agent_memory_toolkit._container_routing import ContainerKey -from agent_memory_toolkit.cosmos_memory_client import CosmosMemoryClient -from agent_memory_toolkit.processors import DurableFunctionProcessor -from agent_memory_toolkit.services.pipeline import PipelineService -from agent_memory_toolkit.store import MemoryStore +from azure.cosmos.agent_memory._container_routing import ContainerKey +from azure.cosmos.agent_memory.cosmos_memory_client import CosmosMemoryClient +from azure.cosmos.agent_memory.processors import DurableFunctionProcessor +from azure.cosmos.agent_memory.services.pipeline import PipelineService +from azure.cosmos.agent_memory.store import MemoryStore def _assert_iso8601(text: str) -> None: diff --git a/tests/unit/test_process_now.py b/tests/unit/test_process_now.py index cc503e6..4b60138 100644 --- a/tests/unit/test_process_now.py +++ b/tests/unit/test_process_now.py @@ -6,9 +6,9 @@ import pytest -from agent_memory_toolkit.cosmos_memory_client import CosmosMemoryClient -from agent_memory_toolkit.exceptions import CosmosNotConnectedError -from agent_memory_toolkit.processors import ( +from azure.cosmos.agent_memory.cosmos_memory_client import CosmosMemoryClient +from azure.cosmos.agent_memory.exceptions import CosmosNotConnectedError +from azure.cosmos.agent_memory.processors import ( DurableFunctionProcessor, InProcessProcessor, ProcessThreadResult, diff --git a/tests/unit/test_query_builder.py b/tests/unit/test_query_builder.py index 9c77219..4eaf660 100644 --- a/tests/unit/test_query_builder.py +++ b/tests/unit/test_query_builder.py @@ -1,6 +1,6 @@ -"""Unit tests for agent_memory_toolkit._query_builder._QueryBuilder.""" +"""Unit tests for azure.cosmos.agent_memory._query_builder._QueryBuilder.""" -from agent_memory_toolkit._query_builder import _QueryBuilder +from azure.cosmos.agent_memory._query_builder import _QueryBuilder # --------------------------------------------------------------------------- # build_where diff --git a/tests/unit/test_reconcile.py b/tests/unit/test_reconcile.py index f8b41a1..88b5a6b 100644 --- a/tests/unit/test_reconcile.py +++ b/tests/unit/test_reconcile.py @@ -24,9 +24,9 @@ import pytest -from agent_memory_toolkit._utils import _normalize_for_hash, compute_content_hash -from agent_memory_toolkit.exceptions import ValidationError -from agent_memory_toolkit.services.pipeline import PipelineService +from azure.cosmos.agent_memory._utils import _normalize_for_hash, compute_content_hash +from azure.cosmos.agent_memory.exceptions import ValidationError +from azure.cosmos.agent_memory.services.pipeline import PipelineService def _make_pipeline() -> PipelineService: @@ -489,7 +489,7 @@ def _build(self) -> PipelineService: return p def test_extract_skips_when_content_hash_matches_existing(self): - from agent_memory_toolkit._utils import compute_content_hash + from azure.cosmos.agent_memory._utils import compute_content_hash p = self._build() existing_text = "User likes coffee" @@ -542,7 +542,7 @@ def test_extract_skips_when_content_hash_matches_existing(self): assert all(call.args[0].get("type") != "fact" for call in p._upsert_memory.call_args_list) def test_extract_writes_content_hash_on_new_facts(self): - from agent_memory_toolkit._utils import compute_content_hash + from azure.cosmos.agent_memory._utils import compute_content_hash p = self._build() p._load_existing_memories = MagicMock(return_value=[]) @@ -923,25 +923,25 @@ def _capture(name, inputs): class TestDedupPoolSizeThreshold: def test_pool_size_default(self, monkeypatch): - from agent_memory_toolkit.thresholds import DEFAULT_DEDUP_POOL_SIZE, get_dedup_pool_size + from azure.cosmos.agent_memory.thresholds import DEFAULT_DEDUP_POOL_SIZE, get_dedup_pool_size monkeypatch.delenv("DEDUP_POOL_SIZE", raising=False) assert get_dedup_pool_size() == DEFAULT_DEDUP_POOL_SIZE def test_pool_size_override(self, monkeypatch): - from agent_memory_toolkit.thresholds import get_dedup_pool_size + from azure.cosmos.agent_memory.thresholds import get_dedup_pool_size monkeypatch.setenv("DEDUP_POOL_SIZE", "100") assert get_dedup_pool_size() == 100 def test_pool_size_clamped_to_500(self, monkeypatch): - from agent_memory_toolkit.thresholds import get_dedup_pool_size + from azure.cosmos.agent_memory.thresholds import get_dedup_pool_size monkeypatch.setenv("DEDUP_POOL_SIZE", "9999") assert get_dedup_pool_size() == 500 def test_pool_size_zero_falls_back_to_default(self, monkeypatch): - from agent_memory_toolkit.thresholds import DEFAULT_DEDUP_POOL_SIZE, get_dedup_pool_size + from azure.cosmos.agent_memory.thresholds import DEFAULT_DEDUP_POOL_SIZE, get_dedup_pool_size monkeypatch.setenv("DEDUP_POOL_SIZE", "0") assert get_dedup_pool_size() == DEFAULT_DEDUP_POOL_SIZE @@ -978,7 +978,7 @@ def test_same_merged_content_yields_same_id_across_runs(self): first_id = upserts[0]["id"] # Predict id from public formula: ch = compute_content_hash("User likes coffee") - from agent_memory_toolkit.services.pipeline import _ID_SEED_SEP + from azure.cosmos.agent_memory.services.pipeline import _ID_SEED_SEP seed = _ID_SEED_SEP.join(("u1", "merged", ch)) expected = "fact_" + hashlib.sha256(seed.encode()).hexdigest()[:32] @@ -1140,7 +1140,7 @@ def test_clean_contradiction_does_not_warn_about_kept_mismatch(self, caplog): } ) ) - with caplog.at_level(logging.WARNING, logger="agent_memory_toolkit.pipeline"): + with caplog.at_level(logging.WARNING, logger="azure.cosmos.agent_memory.pipeline"): result = p.reconcile_memories("u1") assert result["contradicted"] == 1 # No "kept_ids mismatch" warnings on a clean LLM response. @@ -1413,8 +1413,8 @@ def _build(self) -> PipelineService: return p def test_fact_update_with_self_referential_id_is_skipped(self): - from agent_memory_toolkit._utils import compute_content_hash - from agent_memory_toolkit.services.pipeline import _ID_SEED_SEP + from azure.cosmos.agent_memory._utils import compute_content_hash + from azure.cosmos.agent_memory.services.pipeline import _ID_SEED_SEP p = self._build() text = "User likes tea" @@ -1459,8 +1459,8 @@ def test_fact_update_with_self_referential_id_is_skipped(self): assert out["fact_count"] == 0 def test_procedural_update_with_self_referential_id_is_skipped(self): - from agent_memory_toolkit._utils import compute_content_hash - from agent_memory_toolkit.services.pipeline import _ID_SEED_SEP + from azure.cosmos.agent_memory._utils import compute_content_hash + from azure.cosmos.agent_memory.services.pipeline import _ID_SEED_SEP p = self._build() text = "Greet the user casually" @@ -1514,7 +1514,7 @@ def _outcome_records(caplog) -> list: return [ r for r in caplog.records - if r.name == "agent_memory_toolkit.pipeline" and r.getMessage() == "reconcile.outcome" + if r.name == "azure.cosmos.agent_memory.pipeline" and r.getMessage() == "reconcile.outcome" ] def test_reconcile_emits_outcome_log_line_on_success(self, caplog): @@ -1543,7 +1543,7 @@ def test_reconcile_emits_outcome_log_line_on_success(self, caplog): p._upsert_memory = MagicMock(side_effect=lambda doc: doc) p._mark_superseded = MagicMock(return_value=True) - with caplog.at_level(logging.INFO, logger="agent_memory_toolkit.pipeline"): + with caplog.at_level(logging.INFO, logger="azure.cosmos.agent_memory.pipeline"): result = p.reconcile_memories("u1") records = self._outcome_records(caplog) @@ -1568,7 +1568,7 @@ def test_reconcile_emits_outcome_log_line_on_zero_candidates(self, caplog): p._container.query_items.return_value = iter([]) p._run_prompty = MagicMock() - with caplog.at_level(logging.INFO, logger="agent_memory_toolkit.pipeline"): + with caplog.at_level(logging.INFO, logger="azure.cosmos.agent_memory.pipeline"): result = p.reconcile_memories("u1") p._run_prompty.assert_not_called() @@ -1594,7 +1594,7 @@ def test_reconcile_duration_ms_is_positive_float(self, caplog): p._container.query_items.return_value = iter(facts) p._run_prompty = MagicMock() - with caplog.at_level(logging.INFO, logger="agent_memory_toolkit.pipeline"): + with caplog.at_level(logging.INFO, logger="azure.cosmos.agent_memory.pipeline"): p.reconcile_memories("u1") records = self._outcome_records(caplog) diff --git a/tests/unit/test_thresholds.py b/tests/unit/test_thresholds.py index 1f32eea..77671b9 100644 --- a/tests/unit/test_thresholds.py +++ b/tests/unit/test_thresholds.py @@ -1,6 +1,6 @@ from __future__ import annotations -from agent_memory_toolkit.thresholds import DEFAULT_TTL_BY_TYPE, default_ttl_for +from azure.cosmos.agent_memory.thresholds import DEFAULT_TTL_BY_TYPE, default_ttl_for def test_default_ttl_table_values() -> None: diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 394da5e..826179a 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,8 +1,8 @@ -"""Unit tests for shared helpers in agent_memory_toolkit._utils.""" +"""Unit tests for shared helpers in azure.cosmos.agent_memory._utils.""" import pytest -from agent_memory_toolkit._utils import ( +from azure.cosmos.agent_memory._utils import ( DEFAULT_TTL_BY_TYPE, _build_container_kwargs, _make_memory, @@ -11,7 +11,7 @@ _resolve_full_text_language, compute_content_hash, ) -from agent_memory_toolkit.exceptions import ConfigurationError, ValidationError +from azure.cosmos.agent_memory.exceptions import ConfigurationError, ValidationError def test_build_container_kwargs_includes_required_fields_and_extras(): From 082128263719c14193b8fb2cb806ba46837d9b63 Mon Sep 17 00:00:00 2001 From: Aayush Kataria Date: Mon, 1 Jun 2026 13:41:26 -0700 Subject: [PATCH 2/5] Adding a allowlist for metadata --- CHANGELOG.md | 3 +- .../agent_memory/aio/cosmos_memory_client.py | 7 +- .../agent_memory/aio/services/pipeline.py | 12 +- .../agent_memory/cosmos_memory_client.py | 7 +- .../services/_pipeline_helpers.py | 41 ++++- .../cosmos/agent_memory/services/pipeline.py | 12 +- .../unit/services/test_transcript_metadata.py | 152 ++++++++++++++++++ 7 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 tests/unit/services/test_transcript_metadata.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c0902..f86ec86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,6 @@ Pin a specific version when integrating. ### Package layout - Distribution name: **`azure-cosmos-agent-memory`** (PyPI) -- Import path: **`azure.cosmos.agent_memory`** (Azure SDK namespace - convention, coexists side-by-side with `azure-cosmos`) +- Import path: **`azure.cosmos.agent_memory`** [0.1.0b1]: https://github.com/AzureCosmosDB/AgentMemoryToolkit/releases/tag/v0.1.0b1 diff --git a/azure/cosmos/agent_memory/aio/cosmos_memory_client.py b/azure/cosmos/agent_memory/aio/cosmos_memory_client.py index befb775..eb30f2a 100644 --- a/azure/cosmos/agent_memory/aio/cosmos_memory_client.py +++ b/azure/cosmos/agent_memory/aio/cosmos_memory_client.py @@ -4,7 +4,7 @@ import asyncio from datetime import datetime -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Iterable, Optional from azure.cosmos.agent_memory._base import _BaseMemoryClient from azure.cosmos.agent_memory._container_routing import container_key_for_type @@ -95,6 +95,7 @@ def __init__( chat_deployment_name: str = "gpt-4o-mini", use_default_credential: bool = True, processor: Optional[AsyncMemoryProcessor] = None, + transcript_metadata_keys: Optional[Iterable[str]] = None, ) -> None: self._init_base_config( cosmos_endpoint=cosmos_endpoint, @@ -135,6 +136,9 @@ def __init__( self._pipeline: Optional[AsyncPipelineService] = None self._processor: Optional[AsyncMemoryProcessor] = processor self._processor_explicit = processor is not None + self._transcript_metadata_keys: Optional[tuple[str, ...]] = ( + tuple(transcript_metadata_keys) if transcript_metadata_keys else None + ) logger.info("AsyncCosmosMemoryClient initialized") async def __aenter__(self) -> "AsyncCosmosMemoryClient": @@ -401,6 +405,7 @@ def _build_pipeline(self, store: AsyncMemoryStore) -> AsyncPipelineService: self._chat_client, self._embeddings_client, containers=self._containers, + transcript_metadata_keys=self._transcript_metadata_keys, ) def _store_uses_current_clients(self) -> bool: diff --git a/azure/cosmos/agent_memory/aio/services/pipeline.py b/azure/cosmos/agent_memory/aio/services/pipeline.py index 1becf6f..11e3930 100644 --- a/azure/cosmos/agent_memory/aio/services/pipeline.py +++ b/azure/cosmos/agent_memory/aio/services/pipeline.py @@ -176,6 +176,7 @@ def __init__( prompts_dir: str | None = None, *, containers: dict[ContainerKey, Any], + transcript_metadata_keys: Optional[tuple[str, ...]] = None, ) -> None: self._store = store self._containers = containers @@ -186,6 +187,9 @@ def __init__( self._chat_client = chat_client self._embeddings = embeddings_client self._prompty = PromptyLoader(prompts_dir) + self._transcript_metadata_keys: Optional[tuple[str, ...]] = ( + tuple(transcript_metadata_keys) if transcript_metadata_keys else None + ) async def _query_items(self, container: Any, **kwargs: Any) -> list[dict[str, Any]]: result = container.query_items(**kwargs) @@ -261,13 +265,17 @@ def _validate_extracted_doc(self, doc: dict[str, Any]) -> dict[str, Any]: def _chat_text(response: Any) -> str: return chat_text(response) - @staticmethod def _build_transcript( + self, items: list[dict[str, Any]], *, group_by_thread: bool = False, ) -> str: - return build_transcript(items, group_by_thread=group_by_thread) + return build_transcript( + items, + group_by_thread=group_by_thread, + metadata_keys=getattr(self, "_transcript_metadata_keys", None), + ) async def _load_existing_memories( self, diff --git a/azure/cosmos/agent_memory/cosmos_memory_client.py b/azure/cosmos/agent_memory/cosmos_memory_client.py index ec41032..c33581a 100644 --- a/azure/cosmos/agent_memory/cosmos_memory_client.py +++ b/azure/cosmos/agent_memory/cosmos_memory_client.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Iterable, Optional from azure.cosmos.agent_memory.logging import get_logger @@ -90,6 +90,7 @@ def __init__( chat_deployment_name: str = "gpt-4o-mini", use_default_credential: bool = True, processor: Optional[MemoryProcessor] = None, + transcript_metadata_keys: Optional[Iterable[str]] = None, ) -> None: self._init_base_config( cosmos_endpoint=cosmos_endpoint, @@ -127,6 +128,9 @@ def __init__( self._pipeline: Optional[PipelineService] = None self._processor: Optional[MemoryProcessor] = processor self._processor_explicit = processor is not None + self._transcript_metadata_keys: Optional[tuple[str, ...]] = ( + tuple(transcript_metadata_keys) if transcript_metadata_keys else None + ) if self._cosmos_endpoint: self.create_memory_store() logger.info("CosmosMemoryClient initialized") @@ -372,6 +376,7 @@ def _build_pipeline(self, store: MemoryStore) -> PipelineService: self._chat_client, self._embeddings_client, containers=self._containers, + transcript_metadata_keys=self._transcript_metadata_keys, ) def _store_uses_current_clients(self) -> bool: diff --git a/azure/cosmos/agent_memory/services/_pipeline_helpers.py b/azure/cosmos/agent_memory/services/_pipeline_helpers.py index 8ec86f6..13d4b60 100644 --- a/azure/cosmos/agent_memory/services/_pipeline_helpers.py +++ b/azure/cosmos/agent_memory/services/_pipeline_helpers.py @@ -14,7 +14,7 @@ import re from collections import defaultdict from pathlib import Path -from typing import Any, Optional +from typing import Any, Iterable, Optional from azure.cosmos.agent_memory.exceptions import LLMError @@ -181,10 +181,30 @@ def extract_prompty_params(p: Any) -> dict[str, Any]: return params +def _format_metadata_segment( + metadata: Any, + metadata_keys: Optional[Iterable[str]], +) -> str: + """Render the trailing ``[metadata: {...}]`` segment for a transcript line. + + Returns an empty string unless ``metadata_keys`` is a non-empty iterable + AND at least one of those keys is present in ``metadata``. Only the + explicitly allow-listed keys are serialized, in the iteration order of + ``metadata_keys``. + """ + if not metadata_keys or not isinstance(metadata, dict): + return "" + filtered = {k: metadata[k] for k in metadata_keys if k in metadata} + if not filtered: + return "" + return f" [metadata: {json.dumps(filtered)}]" + + def build_transcript( items: list[dict[str, Any]], *, group_by_thread: bool = False, + metadata_keys: Optional[Iterable[str]] = None, ) -> str: """Build a formatted transcript from memory documents. @@ -194,14 +214,26 @@ def build_transcript( Memory dicts with ``role``, ``content``, and optional ``metadata``. group_by_thread: If *True*, group messages under ``=== Thread ===`` headers. + metadata_keys: + Allow-list of metadata keys to surface in each transcript line. + Defaults to ``None`` (no metadata serialized — only ``[role]: + content`` lines). When provided, only the listed keys are emitted, + in iteration order. Keys absent from a given turn's metadata are + silently skipped. + + Set this when callers stash semantically useful breadcrumbs in + ``TurnRecord.metadata`` that the extraction LLM should see + (e.g. ``["agent_id", "timestamp"]``). Leaving it unset keeps free-form + metadata blobs (raw tool calls, IDE schema, etc.) out of every + prompt — they're often 10-100x larger than the dialog itself and + dilute extraction quality. """ if not group_by_thread: lines: list[str] = [] for m in items: role = m.get("role", "unknown") content = m.get("content", "") - metadata = m.get("metadata", {}) - meta_str = f" [metadata: {json.dumps(metadata)}]" if metadata else "" + meta_str = _format_metadata_segment(m.get("metadata", {}), metadata_keys) lines.append(f"[{role}]: {content}{meta_str}") return "\n".join(lines) @@ -215,8 +247,7 @@ def build_transcript( for m in thread_items: role = m.get("role", "unknown") content = m.get("content", "") - metadata = m.get("metadata", {}) - meta_str = f" [metadata: {json.dumps(metadata)}]" if metadata else "" + meta_str = _format_metadata_segment(m.get("metadata", {}), metadata_keys) parts.append(f"[{role}]: {content}{meta_str}") parts.append("") return "\n".join(parts) diff --git a/azure/cosmos/agent_memory/services/pipeline.py b/azure/cosmos/agent_memory/services/pipeline.py index effaebf..a723b06 100644 --- a/azure/cosmos/agent_memory/services/pipeline.py +++ b/azure/cosmos/agent_memory/services/pipeline.py @@ -161,6 +161,7 @@ def __init__( prompts_dir: str | None = None, *, containers: dict[ContainerKey, Any], + transcript_metadata_keys: Optional[tuple[str, ...]] = None, ) -> None: self._store = store self._containers = containers @@ -171,6 +172,9 @@ def __init__( self._chat_client = chat_client self._embeddings = embeddings_client self._prompty = PromptyLoader(prompts_dir) + self._transcript_metadata_keys: Optional[tuple[str, ...]] = ( + tuple(transcript_metadata_keys) if transcript_metadata_keys else None + ) def _run_prompty( self, @@ -214,13 +218,17 @@ def _validate_extracted_doc(self, doc: dict[str, Any]) -> dict[str, Any]: def _chat_text(response: Any) -> str: return chat_text(response) - @staticmethod def _build_transcript( + self, items: list[dict[str, Any]], *, group_by_thread: bool = False, ) -> str: - return build_transcript(items, group_by_thread=group_by_thread) + return build_transcript( + items, + group_by_thread=group_by_thread, + metadata_keys=getattr(self, "_transcript_metadata_keys", None), + ) def _load_existing_memories( self, diff --git a/tests/unit/services/test_transcript_metadata.py b/tests/unit/services/test_transcript_metadata.py new file mode 100644 index 0000000..e6bbbc4 --- /dev/null +++ b/tests/unit/services/test_transcript_metadata.py @@ -0,0 +1,152 @@ +"""Transcript-building behavior: default + metadata allow-list.""" + +from __future__ import annotations + +import json + +import pytest + +from azure.cosmos.agent_memory.services._pipeline_helpers import build_transcript + + +def _turn(role: str, content: str, **meta: object) -> dict[str, object]: + return {"role": role, "content": content, "metadata": dict(meta)} + + +class TestDefaultExcludesMetadata: + """Default behavior must NEVER serialize metadata into the transcript. + + Coding-agent frameworks routinely stash full tool-call payloads, IDE + schema fragments, and raw model responses under ``TurnRecord.metadata``. + Dumping that blob into every extraction/summarization prompt was + drowning out the actual dialogue (issue: user reported 24-turn session + where metadata was 20-25x the size of the conversation content). + """ + + def test_metadata_omitted_by_default_flat(self) -> None: + items = [ + _turn("user", "Hello", raw_response={"huge": "blob"}, tool_calls=[1, 2, 3]), + _turn("assistant", "Hi", model_id="gpt-4o", token_count=42), + ] + out = build_transcript(items) + assert out == "[user]: Hello\n[assistant]: Hi" + assert "metadata" not in out + assert "raw_response" not in out + assert "tool_calls" not in out + + def test_metadata_omitted_by_default_grouped(self) -> None: + items = [ + {"role": "user", "content": "Q", "metadata": {"raw_response": "..."}, "thread_id": "t1"}, + {"role": "assistant", "content": "A", "metadata": {"raw_response": "..."}, "thread_id": "t1"}, + ] + out = build_transcript(items, group_by_thread=True) + assert "raw_response" not in out + assert "metadata" not in out + assert "=== Thread t1 ===" in out + assert "[user]: Q" in out + assert "[assistant]: A" in out + + def test_empty_allowlist_is_same_as_default(self) -> None: + items = [_turn("user", "x", a=1, b=2)] + assert build_transcript(items, metadata_keys=[]) == "[user]: x" + assert build_transcript(items, metadata_keys=None) == "[user]: x" + + def test_no_metadata_field_does_not_crash(self) -> None: + items = [{"role": "user", "content": "no meta key here"}] + assert build_transcript(items) == "[user]: no meta key here" + + +class TestAllowlist: + """``metadata_keys`` is an opt-in allow-list, not a denylist.""" + + def test_only_allowed_keys_surface(self) -> None: + items = [_turn("user", "Hi", agent_id="copilot", raw_response={"huge": "blob"}, timestamp="2026-01-01")] + out = build_transcript(items, metadata_keys=["agent_id", "timestamp"]) + # The line still includes the metadata segment, but only the + # allow-listed keys made it through. + assert "[user]: Hi" in out + assert "agent_id" in out + assert "timestamp" in out + assert "raw_response" not in out + assert "huge" not in out + + def test_allowlist_ordering_is_iteration_order(self) -> None: + items = [_turn("user", "Hi", agent_id="copilot", timestamp="2026-01-01")] + out_a = build_transcript(items, metadata_keys=["agent_id", "timestamp"]) + out_b = build_transcript(items, metadata_keys=["timestamp", "agent_id"]) + # JSON serialization must reflect the iteration order of the + # allow-list so callers can pin ordering for deterministic prompts. + assert out_a.index("agent_id") < out_a.index("timestamp") + assert out_b.index("timestamp") < out_b.index("agent_id") + + def test_missing_allowed_keys_silently_skipped(self) -> None: + items = [_turn("user", "Hi", agent_id="copilot")] + out = build_transcript(items, metadata_keys=["agent_id", "model_id", "not_present"]) + assert "agent_id" in out + # Absent keys do not appear as ``null`` or empty entries. + assert "model_id" not in out + assert "not_present" not in out + assert "null" not in out + + def test_segment_dropped_when_no_allowed_key_present(self) -> None: + items = [_turn("user", "Hi", raw_response="big")] + out = build_transcript(items, metadata_keys=["agent_id"]) + # None of the allow-listed keys exist on this turn's metadata, + # so the ``[metadata: ...]`` segment is suppressed entirely. + assert out == "[user]: Hi" + + def test_works_with_grouped_layout(self) -> None: + items = [ + {"role": "user", "content": "Q", "metadata": {"agent_id": "x", "raw": "big"}, "thread_id": "t1"}, + {"role": "assistant", "content": "A", "metadata": {"agent_id": "x", "raw": "big"}, "thread_id": "t1"}, + ] + out = build_transcript(items, group_by_thread=True, metadata_keys=["agent_id"]) + assert "agent_id" in out + assert "raw" not in out + + def test_non_dict_metadata_is_safe(self) -> None: + items = [{"role": "user", "content": "x", "metadata": "not a dict"}] + # Should not crash and should not invent metadata content. + assert build_transcript(items, metadata_keys=["agent_id"]) == "[user]: x" + + def test_serialized_json_is_valid(self) -> None: + items = [_turn("user", "Hi", agent_id="copilot", count=7)] + out = build_transcript(items, metadata_keys=["agent_id", "count"]) + start = out.index("{") + end = out.rindex("}") + 1 + parsed = json.loads(out[start:end]) + assert parsed == {"agent_id": "copilot", "count": 7} + + +class TestPipelineServicePlumbing: + """``PipelineService.__init__`` must accept the allow-list and thread it + through ``_build_transcript``.""" + + def test_sync_pipeline_propagates_allowlist(self) -> None: + from azure.cosmos.agent_memory.services.pipeline import PipelineService + + service = PipelineService.__new__(PipelineService) + service._transcript_metadata_keys = ("agent_id",) + items = [_turn("user", "Hi", agent_id="copilot", raw="big")] + out = service._build_transcript(items) + assert "agent_id" in out + assert "raw" not in out + + def test_sync_pipeline_default_empty(self) -> None: + from azure.cosmos.agent_memory.services.pipeline import PipelineService + + service = PipelineService.__new__(PipelineService) + service._transcript_metadata_keys = None + items = [_turn("user", "Hi", agent_id="copilot")] + assert service._build_transcript(items) == "[user]: Hi" + + @pytest.mark.asyncio + async def test_async_pipeline_propagates_allowlist(self) -> None: + from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService + + service = AsyncPipelineService.__new__(AsyncPipelineService) + service._transcript_metadata_keys = ("agent_id",) + items = [_turn("user", "Hi", agent_id="copilot", raw="big")] + out = service._build_transcript(items) + assert "agent_id" in out + assert "raw" not in out From 500fe825874f3247e93565b05c8c438037494a22 Mon Sep 17 00:00:00 2001 From: Aayush Kataria Date: Mon, 1 Jun 2026 16:48:08 -0700 Subject: [PATCH 3/5] Resolving comments --- .../agent_memory/aio/cosmos_memory_client.py | 5 +- .../agent_memory/aio/services/pipeline.py | 11 +- .../agent_memory/cosmos_memory_client.py | 5 +- .../services/_pipeline_helpers.py | 37 +++- .../cosmos/agent_memory/services/pipeline.py | 11 +- .../orchestrators/extract_memories.py | 4 +- function_app/shared/pipeline_factory.py | 23 ++- .../function_app/test_pipeline_factory.py | 26 +++ .../unit/services/test_transcript_metadata.py | 177 ++++++++++++++++++ 9 files changed, 274 insertions(+), 25 deletions(-) diff --git a/azure/cosmos/agent_memory/aio/cosmos_memory_client.py b/azure/cosmos/agent_memory/aio/cosmos_memory_client.py index eb30f2a..6f5098d 100644 --- a/azure/cosmos/agent_memory/aio/cosmos_memory_client.py +++ b/azure/cosmos/agent_memory/aio/cosmos_memory_client.py @@ -27,6 +27,7 @@ from azure.cosmos.agent_memory.aio.store import AsyncMemoryStore from azure.cosmos.agent_memory.exceptions import CosmosNotConnectedError, CosmosOperationError from azure.cosmos.agent_memory.logging import get_logger +from azure.cosmos.agent_memory.services._pipeline_helpers import _normalize_metadata_keys from azure.cosmos.agent_memory.thresholds import DEFAULT_TTL_BY_TYPE if TYPE_CHECKING: # pragma: no cover - typing-only import @@ -136,9 +137,7 @@ def __init__( self._pipeline: Optional[AsyncPipelineService] = None self._processor: Optional[AsyncMemoryProcessor] = processor self._processor_explicit = processor is not None - self._transcript_metadata_keys: Optional[tuple[str, ...]] = ( - tuple(transcript_metadata_keys) if transcript_metadata_keys else None - ) + self._transcript_metadata_keys: Optional[tuple[str, ...]] = _normalize_metadata_keys(transcript_metadata_keys) logger.info("AsyncCosmosMemoryClient initialized") async def __aenter__(self) -> "AsyncCosmosMemoryClient": diff --git a/azure/cosmos/agent_memory/aio/services/pipeline.py b/azure/cosmos/agent_memory/aio/services/pipeline.py index 11e3930..f4823e3 100644 --- a/azure/cosmos/agent_memory/aio/services/pipeline.py +++ b/azure/cosmos/agent_memory/aio/services/pipeline.py @@ -16,7 +16,7 @@ import time from collections import defaultdict from datetime import datetime, timezone -from typing import Any, Literal, Optional +from typing import Any, Iterable, Literal, Optional from azure.cosmos.exceptions import ( CosmosHttpResponseError, @@ -48,6 +48,7 @@ from azure.cosmos.agent_memory.services._pipeline_helpers import ( VALID_VALENCES, PromptyLoader, + _normalize_metadata_keys, build_topic_tags, build_transcript, cap_structured_summary, @@ -176,7 +177,7 @@ def __init__( prompts_dir: str | None = None, *, containers: dict[ContainerKey, Any], - transcript_metadata_keys: Optional[tuple[str, ...]] = None, + transcript_metadata_keys: Optional[Iterable[str]] = None, ) -> None: self._store = store self._containers = containers @@ -187,9 +188,7 @@ def __init__( self._chat_client = chat_client self._embeddings = embeddings_client self._prompty = PromptyLoader(prompts_dir) - self._transcript_metadata_keys: Optional[tuple[str, ...]] = ( - tuple(transcript_metadata_keys) if transcript_metadata_keys else None - ) + self._transcript_metadata_keys: Optional[tuple[str, ...]] = _normalize_metadata_keys(transcript_metadata_keys) async def _query_items(self, container: Any, **kwargs: Any) -> list[dict[str, Any]]: result = container.query_items(**kwargs) @@ -271,6 +270,8 @@ def _build_transcript( *, group_by_thread: bool = False, ) -> str: + # getattr fallback covers unit tests that build AsyncPipelineService + # via __new__ to bypass __init__ (and therefore the metadata-keys stash). return build_transcript( items, group_by_thread=group_by_thread, diff --git a/azure/cosmos/agent_memory/cosmos_memory_client.py b/azure/cosmos/agent_memory/cosmos_memory_client.py index c33581a..e3bbbf2 100644 --- a/azure/cosmos/agent_memory/cosmos_memory_client.py +++ b/azure/cosmos/agent_memory/cosmos_memory_client.py @@ -25,6 +25,7 @@ from .embeddings import EmbeddingsClient from .exceptions import CosmosOperationError from .processors import InProcessProcessor, MemoryProcessor +from .services._pipeline_helpers import _normalize_metadata_keys from .services.pipeline import PipelineService from .store import MemoryStore from .thresholds import DEFAULT_TTL_BY_TYPE @@ -128,9 +129,7 @@ def __init__( self._pipeline: Optional[PipelineService] = None self._processor: Optional[MemoryProcessor] = processor self._processor_explicit = processor is not None - self._transcript_metadata_keys: Optional[tuple[str, ...]] = ( - tuple(transcript_metadata_keys) if transcript_metadata_keys else None - ) + self._transcript_metadata_keys: Optional[tuple[str, ...]] = _normalize_metadata_keys(transcript_metadata_keys) if self._cosmos_endpoint: self.create_memory_store() logger.info("CosmosMemoryClient initialized") diff --git a/azure/cosmos/agent_memory/services/_pipeline_helpers.py b/azure/cosmos/agent_memory/services/_pipeline_helpers.py index 13d4b60..715b453 100644 --- a/azure/cosmos/agent_memory/services/_pipeline_helpers.py +++ b/azure/cosmos/agent_memory/services/_pipeline_helpers.py @@ -181,13 +181,34 @@ def extract_prompty_params(p: Any) -> dict[str, Any]: return params +def _normalize_metadata_keys( + value: Optional[Iterable[str]], +) -> Optional[tuple[str, ...]]: + """Validate + coerce a ``transcript_metadata_keys`` argument to a tuple. + + Rejects ``str`` outright (a bare string is iterable char-by-char, which + would silently produce a one-letter allow-list). Returns ``None`` for + empty or missing input. + """ + if value is None: + return None + if isinstance(value, str): + raise TypeError( + "transcript_metadata_keys must be a sequence of keys " + "(list/tuple/set), not a single str. " + f"Got: {value!r}. Did you mean [{value!r}]?" + ) + keys = tuple(str(k) for k in value if str(k)) + return keys or None + + def _format_metadata_segment( metadata: Any, - metadata_keys: Optional[Iterable[str]], + metadata_keys: Optional[tuple[str, ...]], ) -> str: """Render the trailing ``[metadata: {...}]`` segment for a transcript line. - Returns an empty string unless ``metadata_keys`` is a non-empty iterable + Returns an empty string unless ``metadata_keys`` is a non-empty tuple AND at least one of those keys is present in ``metadata``. Only the explicitly allow-listed keys are serialized, in the iteration order of ``metadata_keys``. @@ -197,7 +218,8 @@ def _format_metadata_segment( filtered = {k: metadata[k] for k in metadata_keys if k in metadata} if not filtered: return "" - return f" [metadata: {json.dumps(filtered)}]" + payload = json.dumps(filtered, separators=(",", ":"), ensure_ascii=False, default=str) + return f" [metadata: {payload}]" def build_transcript( @@ -227,13 +249,18 @@ def build_transcript( metadata blobs (raw tool calls, IDE schema, etc.) out of every prompt — they're often 10-100x larger than the dialog itself and dilute extraction quality. + + Accepts any iterable of strings except ``str`` itself (which would + be interpreted char-by-char). Generators are coerced to a tuple so + the allow-list is reusable across turns. """ + keys = _normalize_metadata_keys(metadata_keys) if not group_by_thread: lines: list[str] = [] for m in items: role = m.get("role", "unknown") content = m.get("content", "") - meta_str = _format_metadata_segment(m.get("metadata", {}), metadata_keys) + meta_str = _format_metadata_segment(m.get("metadata", {}), keys) lines.append(f"[{role}]: {content}{meta_str}") return "\n".join(lines) @@ -247,7 +274,7 @@ def build_transcript( for m in thread_items: role = m.get("role", "unknown") content = m.get("content", "") - meta_str = _format_metadata_segment(m.get("metadata", {}), metadata_keys) + meta_str = _format_metadata_segment(m.get("metadata", {}), keys) parts.append(f"[{role}]: {content}{meta_str}") parts.append("") return "\n".join(parts) diff --git a/azure/cosmos/agent_memory/services/pipeline.py b/azure/cosmos/agent_memory/services/pipeline.py index a723b06..315fd05 100644 --- a/azure/cosmos/agent_memory/services/pipeline.py +++ b/azure/cosmos/agent_memory/services/pipeline.py @@ -14,7 +14,7 @@ import time from collections import defaultdict from datetime import datetime, timezone -from typing import Any, Literal, Optional +from typing import Any, Iterable, Literal, Optional from azure.cosmos.exceptions import ( CosmosHttpResponseError, @@ -46,6 +46,7 @@ from azure.cosmos.agent_memory.services._pipeline_helpers import ( VALID_VALENCES, PromptyLoader, + _normalize_metadata_keys, build_topic_tags, build_transcript, cap_structured_summary, @@ -161,7 +162,7 @@ def __init__( prompts_dir: str | None = None, *, containers: dict[ContainerKey, Any], - transcript_metadata_keys: Optional[tuple[str, ...]] = None, + transcript_metadata_keys: Optional[Iterable[str]] = None, ) -> None: self._store = store self._containers = containers @@ -172,9 +173,7 @@ def __init__( self._chat_client = chat_client self._embeddings = embeddings_client self._prompty = PromptyLoader(prompts_dir) - self._transcript_metadata_keys: Optional[tuple[str, ...]] = ( - tuple(transcript_metadata_keys) if transcript_metadata_keys else None - ) + self._transcript_metadata_keys: Optional[tuple[str, ...]] = _normalize_metadata_keys(transcript_metadata_keys) def _run_prompty( self, @@ -224,6 +223,8 @@ def _build_transcript( *, group_by_thread: bool = False, ) -> str: + # getattr fallback covers unit tests that build PipelineService via + # __new__ to bypass __init__ (and therefore the metadata-keys stash). return build_transcript( items, group_by_thread=group_by_thread, diff --git a/function_app/orchestrators/extract_memories.py b/function_app/orchestrators/extract_memories.py index 1691318..a911f20 100644 --- a/function_app/orchestrators/extract_memories.py +++ b/function_app/orchestrators/extract_memories.py @@ -56,9 +56,7 @@ def ExtractMemoriesOrchestrator(context: df.DurableOrchestrationContext): ) if config.get_procedural_synthesis_auto(): count = payload.get("count") - instance_id = ( - f"procedural:{user_id}:{thread_id}:{count}" if count is not None else None - ) + instance_id = f"procedural:{user_id}:{thread_id}:{count}" if count is not None else None try: procedural = yield context.call_sub_orchestrator_with_retry( "SynthesizeProceduralOrchestrator", diff --git a/function_app/shared/pipeline_factory.py b/function_app/shared/pipeline_factory.py index bcf3a02..dc43fd0 100644 --- a/function_app/shared/pipeline_factory.py +++ b/function_app/shared/pipeline_factory.py @@ -6,6 +6,7 @@ from __future__ import annotations +import os from typing import Any from . import config @@ -18,6 +19,20 @@ _pipeline: Any | None = None +def _read_transcript_metadata_keys() -> tuple[str, ...] | None: + """Parse ``AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS`` (comma-separated allow-list). + + Mirrors the ``transcript_metadata_keys`` ctor kwarg on + :class:`CosmosMemoryClient` so the Durable-Functions backend produces + the same prompt content as the in-process backend. + """ + raw = os.environ.get("AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS", "").strip() + if not raw: + return None + keys = tuple(part.strip() for part in raw.split(",") if part.strip()) + return keys or None + + def get_pipeline(): """Return the cached :class:`PipelineService` for this worker.""" global _pipeline @@ -59,5 +74,11 @@ def get_pipeline(): ContainerKey.SUMMARIES: summaries_container, } store = MemoryStore(containers=containers, embeddings_client=embeddings) - _pipeline = PipelineService(store, chat, embeddings, containers=containers) + _pipeline = PipelineService( + store, + chat, + embeddings, + containers=containers, + transcript_metadata_keys=_read_transcript_metadata_keys(), + ) return _pipeline diff --git a/tests/unit/function_app/test_pipeline_factory.py b/tests/unit/function_app/test_pipeline_factory.py index c004962..37b7cce 100644 --- a/tests/unit/function_app/test_pipeline_factory.py +++ b/tests/unit/function_app/test_pipeline_factory.py @@ -107,6 +107,7 @@ def test_builds_pipeline_from_complete_env(mocks): mocks.chat_instance, mocks.embed_instance, containers=expected_containers, + transcript_metadata_keys=None, ) @@ -144,6 +145,31 @@ def test_passes_credential_to_both_clients(mocks): assert mocks.embed_ctor.call_args.kwargs["credential"] is mocks.credential +def test_transcript_metadata_keys_env_threads_into_pipeline(monkeypatch, mocks): + monkeypatch.setenv( + "AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS", + " agent_id , timestamp , , model_id", + ) + pipeline_factory.get_pipeline() + + kwargs = mocks.pipeline_ctor.call_args.kwargs + assert kwargs["transcript_metadata_keys"] == ("agent_id", "timestamp", "model_id") + + +def test_transcript_metadata_keys_env_unset_yields_none(monkeypatch, mocks): + monkeypatch.delenv("AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS", raising=False) + pipeline_factory.get_pipeline() + + assert mocks.pipeline_ctor.call_args.kwargs["transcript_metadata_keys"] is None + + +def test_transcript_metadata_keys_env_empty_string_yields_none(monkeypatch, mocks): + monkeypatch.setenv("AGENT_MEMORY_TRANSCRIPT_METADATA_KEYS", " ") + pipeline_factory.get_pipeline() + + assert mocks.pipeline_ctor.call_args.kwargs["transcript_metadata_keys"] is None + + # --------------------------------------------------------------------------- # Caching / idempotence # --------------------------------------------------------------------------- diff --git a/tests/unit/services/test_transcript_metadata.py b/tests/unit/services/test_transcript_metadata.py index e6bbbc4..fa67bb8 100644 --- a/tests/unit/services/test_transcript_metadata.py +++ b/tests/unit/services/test_transcript_metadata.py @@ -150,3 +150,180 @@ async def test_async_pipeline_propagates_allowlist(self) -> None: out = service._build_transcript(items) assert "agent_id" in out assert "raw" not in out + + +class TestStringInputRejected: + """A bare ``str`` is iterable char-by-char — passing it would silently + produce a per-character allow-list. Reject with a clear ``TypeError``.""" + + def test_build_transcript_rejects_str(self) -> None: + with pytest.raises(TypeError, match="not a single str"): + build_transcript([_turn("user", "Hi", agent_id="x")], metadata_keys="agent_id") + + def test_pipeline_service_ctor_rejects_str(self) -> None: + from unittest.mock import MagicMock + + from azure.cosmos.agent_memory._container_routing import ContainerKey + from azure.cosmos.agent_memory.services.pipeline import PipelineService + + containers = { + ContainerKey.MEMORIES: MagicMock(), + ContainerKey.TURNS: MagicMock(), + ContainerKey.SUMMARIES: MagicMock(), + } + with pytest.raises(TypeError, match="not a single str"): + PipelineService( + MagicMock(), + MagicMock(), + MagicMock(), + containers=containers, + transcript_metadata_keys="agent_id", + ) + + def test_async_pipeline_service_ctor_rejects_str(self) -> None: + from unittest.mock import MagicMock + + from azure.cosmos.agent_memory._container_routing import ContainerKey + from azure.cosmos.agent_memory.aio.services.pipeline import AsyncPipelineService + + containers = { + ContainerKey.MEMORIES: MagicMock(), + ContainerKey.TURNS: MagicMock(), + ContainerKey.SUMMARIES: MagicMock(), + } + with pytest.raises(TypeError, match="not a single str"): + AsyncPipelineService( + MagicMock(), + MagicMock(), + MagicMock(), + containers=containers, + transcript_metadata_keys="agent_id", + ) + + def test_client_ctor_rejects_str(self) -> None: + from azure.cosmos.agent_memory import CosmosMemoryClient + + with pytest.raises(TypeError, match="not a single str"): + CosmosMemoryClient( + cosmos_endpoint="", + transcript_metadata_keys="agent_id", + ) + + +class TestCompactJsonSerialization: + """Token-reduction is the whole point — emit compact JSON, not pretty.""" + + def test_no_whitespace_between_separators(self) -> None: + items = [_turn("user", "Hi", a=1, b=2)] + out = build_transcript(items, metadata_keys=["a", "b"]) + # Default json.dumps would produce ``{"a": 1, "b": 2}`` (spaces + # after `:` and `,`). Compact form drops both. + assert '{"a":1,"b":2}' in out + assert '"a": 1' not in out + assert '"b": 2' not in out + + def test_non_ascii_preserved(self) -> None: + items = [_turn("user", "Hi", note="café")] + out = build_transcript(items, metadata_keys=["note"]) + # ensure_ascii=False keeps human-readable utf-8 in the prompt. + assert "café" in out + assert "\\u00e9" not in out + + +class TestNonSerializableMetadataCoerced: + """Real-world metadata carries datetimes, UUIDs, etc. — ``default=str`` + keeps a single bad value from blowing up the entire extraction.""" + + def test_datetime_value_does_not_crash(self) -> None: + from datetime import datetime, timezone + + items = [_turn("user", "Hi", timestamp=datetime(2026, 1, 1, tzinfo=timezone.utc))] + out = build_transcript(items, metadata_keys=["timestamp"]) + assert "timestamp" in out + assert "2026-01-01" in out + + def test_uuid_value_does_not_crash(self) -> None: + import uuid + + u = uuid.uuid4() + items = [_turn("user", "Hi", agent_id=u)] + out = build_transcript(items, metadata_keys=["agent_id"]) + assert str(u) in out + + +class TestGeneratorCoercion: + """A generator passed for ``metadata_keys`` would be exhausted after the + first turn; ``build_transcript`` must coerce it to a sequence up front.""" + + def test_generator_works_across_multiple_turns(self) -> None: + items = [ + _turn("user", "T1", agent_id="x"), + _turn("user", "T2", agent_id="y"), + _turn("user", "T3", agent_id="z"), + ] + out = build_transcript(items, metadata_keys=(k for k in ["agent_id"])) + assert out.count("agent_id") == 3 + + +class TestClientToPipelineWiring: + """A typo in ``_build_pipeline`` (e.g. ``_transcript_metadata_key`` vs + ``_keys``) must fail a test, not silently disarm the kwarg.""" + + def test_sync_client_threads_kwarg_into_pipeline(self) -> None: + from unittest.mock import MagicMock + + from azure.cosmos.agent_memory import CosmosMemoryClient + + client = CosmosMemoryClient.__new__(CosmosMemoryClient) + client._transcript_metadata_keys = ("agent_id", "timestamp") + client._turns_container_client = MagicMock() + client._memories_container_client = MagicMock() + client._summaries_container_client = MagicMock() + client._chat_client = MagicMock() + client._embeddings_client = MagicMock() + captured: dict[str, object] = {} + + from azure.cosmos.agent_memory import cosmos_memory_client as mod + + original = mod.PipelineService + + def fake_pipeline(*args: object, **kwargs: object) -> object: + captured.update(kwargs) + return MagicMock() + + mod.PipelineService = fake_pipeline # type: ignore[assignment] + try: + client._build_pipeline(MagicMock()) + finally: + mod.PipelineService = original + + assert captured.get("transcript_metadata_keys") == ("agent_id", "timestamp") + + def test_async_client_threads_kwarg_into_pipeline(self) -> None: + from unittest.mock import MagicMock + + from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient + from azure.cosmos.agent_memory.aio import cosmos_memory_client as mod + + client = AsyncCosmosMemoryClient.__new__(AsyncCosmosMemoryClient) + client._transcript_metadata_keys = ("agent_id",) + client._turns_container_client = MagicMock() + client._memories_container_client = MagicMock() + client._summaries_container_client = MagicMock() + client._chat_client = MagicMock() + client._embeddings_client = MagicMock() + captured: dict[str, object] = {} + + original = mod.AsyncPipelineService + + def fake_pipeline(*args: object, **kwargs: object) -> object: + captured.update(kwargs) + return MagicMock() + + mod.AsyncPipelineService = fake_pipeline # type: ignore[assignment] + try: + client._build_pipeline(MagicMock()) + finally: + mod.AsyncPipelineService = original + + assert captured.get("transcript_metadata_keys") == ("agent_id",) From 595f7ec93c07de482e75b1edfc394dde5b09bf35 Mon Sep 17 00:00:00 2001 From: Aayush Kataria Date: Mon, 1 Jun 2026 17:18:15 -0700 Subject: [PATCH 4/5] Updating the release workflows --- .github/workflows/_lint.yml | 80 ++++++++++++ .github/workflows/_test.yml | 56 ++++++++ .github/workflows/_test_release.yml | 93 +++++++++++++ .github/workflows/ci.yml | 62 +++++---- .github/workflows/release.yml | 196 ++++++++++++++++++++++++++++ Docs/RELEASING.md | 96 +++++++------- function_app/requirements.txt | 11 +- 7 files changed, 502 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/_lint.yml create mode 100644 .github/workflows/_test.yml create mode 100644 .github/workflows/_test_release.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml new file mode 100644 index 0000000..6d47350 --- /dev/null +++ b/.github/workflows/_lint.yml @@ -0,0 +1,80 @@ +name: lint + +on: + workflow_call: + +permissions: + contents: read + +env: + # Inline annotations on PR diffs when ruff complains. + RUFF_OUTPUT_FORMAT: github + +jobs: + ruff: + name: "ruff #${{ matrix.python-version }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Min + max supported Pythons only; intermediates almost never surface + # a lint issue that doesn't show up on the boundaries. + python-version: ["3.11", "3.13"] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install ruff + run: pip install 'ruff>=0.15,<0.16' + + - name: ruff check + run: ruff check azure/ tests/ function_app/ + + - name: ruff format --check + run: ruff format --check azure/ tests/ function_app/ + + wheel-namespace-guard: + name: "wheel namespace guard" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install build + run: pip install build + + - name: Build wheel + run: python -m build --wheel + + - name: Assert wheel does NOT ship azure/ or azure/cosmos/ __init__.py + # azure.cosmos.* is a PEP 420 implicit namespace package owned by the + # official azure-cosmos SDK. Shipping azure/__init__.py or + # azure/cosmos/__init__.py from this wheel would shadow the SDK and + # break every consumer of azure-cosmos at import time. + run: | + set -eo pipefail + BAD=$(python -m zipfile -l dist/*.whl | awk '{print $1}' | \ + grep -E '^(azure/__init__\.py|azure/cosmos/__init__\.py)$' || true) + if [ -n "$BAD" ]; then + echo "::error::Wheel contains forbidden namespace __init__.py files:" + echo "$BAD" + exit 1 + fi + echo "✓ Wheel does not ship azure/ or azure/cosmos/ __init__.py" + + - name: Assert wheel DOES ship azure/cosmos/agent_memory/__init__.py + run: | + set -eo pipefail + python -m zipfile -l dist/*.whl | awk '{print $1}' | \ + grep -qE '^azure/cosmos/agent_memory/__init__\.py$' + echo "✓ Wheel ships azure/cosmos/agent_memory/__init__.py" diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml new file mode 100644 index 0000000..7f67c9a --- /dev/null +++ b/.github/workflows/_test.yml @@ -0,0 +1,56 @@ +name: test + +on: + workflow_call: + +permissions: + contents: read + +jobs: + unit: + name: "pytest #${{ matrix.python-version }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install package with dev extras + run: pip install -e ".[dev]" + + - name: Run unit tests with coverage + run: | + pytest tests/unit/ \ + --cov=azure.cosmos.agent_memory \ + --cov-report=xml \ + --cov-report=term-missing \ + -v + + - name: Upload coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report-${{ matrix.python-version }} + path: coverage.xml + if-no-files-found: ignore + + - name: Ensure tests did not write to the working tree + # Catches tests that accidentally leave files behind (e.g. .cache + # directories, downloaded fixtures, or local Cosmos emulator state). + run: | + set -eu + STATUS="$(git status --porcelain)" + if [ -n "$STATUS" ]; then + echo "::error::Tests modified the working tree:" + echo "$STATUS" + exit 1 + fi + echo "✓ Working tree clean" diff --git a/.github/workflows/_test_release.yml b/.github/workflows/_test_release.yml new file mode 100644 index 0000000..e712c7d --- /dev/null +++ b/.github/workflows/_test_release.yml @@ -0,0 +1,93 @@ +name: test-release + +# Builds the wheel + sdist, uploads to TestPyPI (sanity check only). +# Called by release.yml before the real PyPI publish. + +on: + workflow_call: + outputs: + pkg-name: + description: "Distribution name from pyproject.toml" + value: ${{ jobs.build.outputs.pkg-name }} + version: + description: "Version from pyproject.toml" + value: ${{ jobs.build.outputs.version }} + +permissions: {} + +env: + PYTHON_VERSION: "3.11" + +jobs: + build: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + + outputs: + pkg-name: ${{ steps.check-version.outputs.pkg-name }} + version: ${{ steps.check-version.outputs.version }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install build tooling + run: pip install build + + # We keep the build job *separate* from the publish job so a compromised + # build-time dependency cannot reach the trusted-publishing OIDC token. + # https://github.com/pypa/gh-action-pypi-publish#non-goals + - name: Build wheel + sdist + run: python -m build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: test-dist + path: dist/ + + - name: Extract pkg-name + version + id: check-version + run: | + python -m pip install --quiet tomli + PKG=$(python -c "import tomli; print(tomli.load(open('pyproject.toml','rb'))['project']['name'])") + VER=$(python -c "import tomli; print(tomli.load(open('pyproject.toml','rb'))['project']['version'])") + echo "pkg-name=$PKG" >> "$GITHUB_OUTPUT" + echo "version=$VER" >> "$GITHUB_OUTPUT" + + publish: + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + # Required for PyPI trusted publishing (OIDC token). + # Configure the trusted publisher at: + # https://test.pypi.org/manage/account/publishing/ + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + name: test-dist + path: dist/ + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + verbose: true + print-hash: true + repository-url: https://test.pypi.org/legacy/ + # CI-only — overwrites a same-version file if a re-run is needed. + # https://github.com/pypa/gh-action-pypi-publish#tolerating-release-package-file-duplicates + skip-existing: true + # Attestations default-on in v1.11.0+ and require additional + # trusted-publisher config; disable until we opt in deliberately. + attestations: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7852c3..c826cbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,44 +6,42 @@ on: pull_request: branches: [main] +# Cancel in-progress runs on the same PR / branch when a new push arrives. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read jobs: lint: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.11", "3.12", "3.13"] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: pip install 'ruff>=0.15,<0.16' - - name: Ruff check - run: ruff check azure/cosmos/agent_memory/ tests/ - - name: Ruff format check - run: ruff format --check azure/cosmos/agent_memory/ tests/ + uses: ./.github/workflows/_lint.yml + permissions: + contents: read test: + uses: ./.github/workflows/_test.yml + permissions: + contents: read + + ci-success: + # Single required-status-check target. Branch protection should require + # only this job; it summarizes lint + test matrix results so a failure + # in any matrix cell blocks merge. + name: "CI Success" + needs: [lint, test] + if: always() runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.11", "3.12", "3.13"] + env: + JOBS_JSON: ${{ toJSON(needs) }} + RESULTS_JSON: ${{ toJSON(needs.*.result) }} + EXIT_CODE: ${{!contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && '0' || '1'}} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install package with dev dependencies - run: pip install -e ".[dev]" - - name: Run unit tests with coverage - run: pytest tests/unit/ --cov=azure.cosmos.agent_memory --cov-report=xml --cov-report=term-missing -v - - name: Upload coverage - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage-report-${{ matrix.python-version }} - path: coverage.xml + - name: Aggregate matrix results + run: | + echo "$JOBS_JSON" + echo "$RESULTS_JSON" + echo "Exiting with $EXIT_CODE" + exit $EXIT_CODE + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..2f773de --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,196 @@ +name: release +run-name: Release azure-cosmos-agent-memory by @${{ github.actor }} + +# Triggered manually from the Actions tab after `pyproject.toml` version +# has been bumped and committed to `main`. Pipeline: +# +# 1. build wheel + sdist +# 2. TestPyPI publish (sanity check, isolated permissions) +# 3. install from TestPyPI in a clean env, run unit + integration tests +# 4. publish to real PyPI via trusted publishing +# 5. create GitHub Release tag v with artifacts attached +# +# Trusted publishing must be configured for THIS workflow on both: +# - https://test.pypi.org/manage/account/publishing/ +# - https://pypi.org/manage/account/publishing/ + +on: + workflow_dispatch: {} + +permissions: {} + +env: + PYTHON_VERSION: "3.11" + +jobs: + build: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + + outputs: + pkg-name: ${{ steps.check-version.outputs.pkg-name }} + version: ${{ steps.check-version.outputs.version }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install build tooling + run: pip install build tomli + + # See _test_release.yml for the security rationale on splitting build + # from publish. + - name: Build wheel + sdist + run: python -m build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + - name: Extract pkg-name + version + id: check-version + run: | + PKG=$(python -c "import tomli; print(tomli.load(open('pyproject.toml','rb'))['project']['name'])") + VER=$(python -c "import tomli; print(tomli.load(open('pyproject.toml','rb'))['project']['version'])") + echo "pkg-name=$PKG" >> "$GITHUB_OUTPUT" + echo "version=$VER" >> "$GITHUB_OUTPUT" + + - name: Assert wheel does NOT ship namespace __init__.py shadows + run: | + set -eo pipefail + BAD=$(python -m zipfile -l dist/*.whl | awk '{print $1}' | \ + grep -E '^(azure/__init__\.py|azure/cosmos/__init__\.py)$' || true) + if [ -n "$BAD" ]; then + echo "::error::Wheel contains forbidden namespace __init__.py files:" + echo "$BAD" + exit 1 + fi + + test-pypi-publish: + needs: build + uses: ./.github/workflows/_test_release.yml + permissions: + contents: read + id-token: write + secrets: inherit + + pre-release-checks: + needs: + - build + - test-pypi-publish + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + # Deliberately NO pip cache here. A cache hit would mask the case + # where a transitive dependency was dropped from pyproject.toml but + # tests still pass because the cached venv carries the old dep. + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install the just-published TestPyPI wheel + env: + PKG_NAME: ${{ needs.build.outputs.pkg-name }} + VERSION: ${{ needs.build.outputs.version }} + # Two-pass retry — TestPyPI mirror propagation can lag by a few seconds. + # Primary index is real PyPI so all the dependencies resolve normally; + # the just-published preview only lives on TestPyPI. + run: | + set -e + install_cmd() { + pip install \ + --extra-index-url https://test.pypi.org/simple/ \ + "${PKG_NAME}==${VERSION}" + } + install_cmd || ( sleep 10 && install_cmd ) || ( sleep 20 && install_cmd ) + + - name: Smoke-test import path + run: | + python -c "from azure.cosmos.agent_memory import CosmosMemoryClient; print(CosmosMemoryClient)" + python -c "from azure.cosmos.agent_memory.aio import AsyncCosmosMemoryClient; print(AsyncCosmosMemoryClient)" + + - name: Install test extras + # The published wheel does NOT carry the [dev] extra's pytest, ruff, + # etc. Install them separately so the unit suite can run against the + # installed wheel. + run: pip install -e ".[dev]" + + - name: Re-install the TestPyPI wheel to overwrite the editable install + env: + PKG_NAME: ${{ needs.build.outputs.pkg-name }} + VERSION: ${{ needs.build.outputs.version }} + run: | + pip install --force-reinstall --no-deps \ + --extra-index-url https://test.pypi.org/simple/ \ + "${PKG_NAME}==${VERSION}" + + - name: Run unit tests against the installed wheel + run: pytest tests/unit -q + + publish: + needs: + - build + - test-pypi-publish + - pre-release-checks + runs-on: ubuntu-latest + permissions: + contents: read + # Required for PyPI trusted publishing (OIDC token). + # Configure at https://pypi.org/manage/account/publishing/ + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + verbose: true + print-hash: true + # Attestations default-on in v1.11.0+ — opt out until configured. + attestations: false + + mark-release: + needs: + - build + - publish + runs-on: ubuntu-latest + permissions: + # ncipollo/release-action needs contents:write to create the tag + release. + contents: write + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Create GitHub Release + uses: ncipollo/release-action@v1 + with: + artifacts: "dist/*" + token: ${{ secrets.GITHUB_TOKEN }} + draft: false + generateReleaseNotes: true + tag: v${{ needs.build.outputs.version }} + commit: ${{ github.sha }} + name: v${{ needs.build.outputs.version }} diff --git a/Docs/RELEASING.md b/Docs/RELEASING.md index 2521af4..646ea3e 100644 --- a/Docs/RELEASING.md +++ b/Docs/RELEASING.md @@ -46,56 +46,52 @@ Before cutting a release: 2. **Update `CHANGELOG.md`** — add a new section with the version, the date, and a summary of changes. Move entries from the unreleased section if you keep one. -3. **Rebuild the vendored wheel** for the Function app (the FA installs - the SDK from `function_app/_vendor/` rather than PyPI): - ```bash - rm -rf dist/ build/ - python -m build --wheel - rm function_app/_vendor/azure_cosmos_agent_memory-*.whl - cp dist/azure_cosmos_agent_memory--py3-none-any.whl function_app/_vendor/ - ``` - And update the wheel filename in `function_app/requirements.txt`. -4. **Commit** the version bump + CHANGELOG + rebuilt vendor wheel + - updated `requirements.txt` together. Suggested message: - `Release v`. -5. **Tag** the commit: - ```bash - git tag -a v -m "Release v" - git push origin main v - ``` -6. **Build the release artifacts**: - ```bash - rm -rf dist/ build/ - python -m build - twine check dist/* - ``` -7. **Smoke-test the wheel in a fresh venv**: - ```bash - python -m venv /tmp/release-smoke - source /tmp/release-smoke/bin/activate - pip install dist/azure_cosmos_agent_memory--py3-none-any.whl - python -c "from azure.cosmos.agent_memory import CosmosMemoryClient; print(CosmosMemoryClient)" - python -c "from azure.cosmos import CosmosClient; print(CosmosClient)" # side-by-side check - ``` -8. **Upload to TestPyPI** (sanity check before pushing to the real index): - ```bash - twine upload --repository testpypi dist/* - ``` - Verify the package page renders correctly at - . -9. **Upload to PyPI**: - ```bash - twine upload dist/* - ``` -10. **Create a GitHub Release** from the tag with the changelog entry as - the body. - -## Trusted Publishing (recommended) - -Configure [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) -so the GitHub Actions workflow uploads with a short-lived OIDC token -instead of a long-lived API token. See `.github/workflows/release.yml` -(once added) for the tag-triggered build → TestPyPI → PyPI flow. +3. **Bump the Function app's SDK pin** in `function_app/requirements.txt` + to match — `azure-cosmos-agent-memory==`. The FA installs + the SDK from PyPI, so the pin must move in lockstep with the SDK + release. (If the release workflow fails after merge, the FA will be + pinned to a non-existent version until you cut a follow-up patch — + coordinate the merge + release-workflow run together.) +4. **Open a PR** with the version bump + CHANGELOG + updated FA pin. + Suggested title: `Release v`. Get it reviewed and merged + to `main`. +5. **Trigger the release workflow**: + - Navigate to **Actions → release → Run workflow** + - Pick the `main` branch + - Click **Run workflow** +6. **The workflow does the rest** — see `.github/workflows/release.yml`: + - Builds `dist/*.whl` + `dist/*.tar.gz` + - Asserts no namespace `__init__.py` shadows are in the wheel + - Publishes to **TestPyPI** via trusted publishing + - Installs from TestPyPI into a clean env and runs the unit suite + - Publishes to **PyPI** via trusted publishing + - Creates a **GitHub Release** with tag `v` and the + wheel + sdist attached +7. **Verify** on + that the new version landed and the README + classifiers render. + +## Trusted Publishing setup (one-time, per environment) + +Trusted publishing replaces long-lived PyPI API tokens with short-lived +OIDC tokens minted by GitHub Actions on each run. Configure once on both +TestPyPI and PyPI: + +- **TestPyPI** — +- **PyPI** — + +For each, register a pending publisher with: + +| Field | Value | +|------|-------| +| PyPI project name | `azure-cosmos-agent-memory` | +| Owner | `AzureCosmosDB` | +| Repository | `AgentMemoryToolkit` | +| Workflow filename | `release.yml` (for **both** TestPyPI and PyPI — the test-release job is invoked via `workflow_call` so PyPI sees the calling workflow's filename, not `_test_release.yml`) | +| Environment name | _(leave blank)_ | + +Until the first release is published, both registrations sit in the +"pending publishers" state. They activate automatically on the first +successful upload. ## Namespace package note diff --git a/function_app/requirements.txt b/function_app/requirements.txt index 28d57e8..69450cc 100644 --- a/function_app/requirements.txt +++ b/function_app/requirements.txt @@ -2,10 +2,7 @@ azure-functions azure-functions-durable -# Cosmos DB + auth (sync + async clients used). -# Pin >=4.16.0 — 4.15.x async ContainerProxy.query_items had a kwarg-leak bug -# where enable_cross_partition_query was forwarded into aiohttp. We dropped -# the kwarg from our async paths in 4.16.0; older 4.15.x is fine for sync. +azure-cosmos-agent-memory==0.1.0b1 azure-cosmos>=4.16.0 azure-identity>=1.20 @@ -14,9 +11,3 @@ openai>=1.60 prompty>=2.0.0a9 jinja2>=3.1.4 typing_extensions>=4.10 - -# Agent Memory Toolkit SDK — provides PipelineService (single source of -# truth for prompts + business logic). The SDK wheel is vendored under -# ``_vendor/`` so the Function app builds without a private package feed. -# Rebuild with: ``python -m build --wheel && cp dist/*.whl function_app/_vendor/`` -./_vendor/azure_cosmos_agent_memory-0.1.0b1-py3-none-any.whl From 90f8bc4e85b11696ee0bc30d014c646dc6035822 Mon Sep 17 00:00:00 2001 From: Aayush Kataria Date: Mon, 1 Jun 2026 17:22:58 -0700 Subject: [PATCH 5/5] Updating the release workflows --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 230fc9c..f7f1a73 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,13 @@ build/ venv/ .pytest_cache/ +# Test / coverage artifacts +.coverage +.coverage.* +coverage.xml +htmlcov/ +.ruff_cache/ + # Jupyter Notebook .ipynb_checkpoints/