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 05e80a9..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 agent_memory_toolkit/ tests/ - - name: Ruff format check - run: ruff format --check agent_memory_toolkit/ 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=agent_memory_toolkit --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/.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/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f86ec86 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +# 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`** + +[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..646ea3e --- /dev/null +++ b/Docs/RELEASING.md @@ -0,0 +1,124 @@ +# 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. **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 + +`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 95% rename from agent_memory_toolkit/aio/cosmos_memory_client.py rename to azure/cosmos/agent_memory/aio/cosmos_memory_client.py index b18c07c..6f5098d 100644 --- a/agent_memory_toolkit/aio/cosmos_memory_client.py +++ b/azure/cosmos/agent_memory/aio/cosmos_memory_client.py @@ -4,11 +4,11 @@ import asyncio from datetime import datetime -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Iterable, 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,19 @@ _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.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 - 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 +73,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, @@ -95,6 +96,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 +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, ...]] = _normalize_metadata_keys(transcript_metadata_keys) logger.info("AsyncCosmosMemoryClient initialized") async def __aenter__(self) -> "AsyncCosmosMemoryClient": @@ -401,6 +404,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: @@ -806,7 +810,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 97% rename from agent_memory_toolkit/aio/services/pipeline.py rename to azure/cosmos/agent_memory/aio/services/pipeline.py index bf2f1f1..f4823e3 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. """ @@ -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, @@ -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,13 +41,14 @@ 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, + _normalize_metadata_keys, build_topic_tags, build_transcript, cap_structured_summary, @@ -55,15 +56,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 @@ -176,6 +177,7 @@ def __init__( prompts_dir: str | None = None, *, containers: dict[ContainerKey, Any], + transcript_metadata_keys: Optional[Iterable[str]] = None, ) -> None: self._store = store self._containers = containers @@ -186,6 +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, ...]] = _normalize_metadata_keys(transcript_metadata_keys) async def _query_items(self, container: Any, **kwargs: Any) -> list[dict[str, Any]]: result = container.query_items(**kwargs) @@ -261,13 +264,19 @@ 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) + # 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, + metadata_keys=getattr(self, "_transcript_metadata_keys", None), + ) async def _load_existing_memories( self, 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 98% rename from agent_memory_toolkit/cosmos_memory_client.py rename to azure/cosmos/agent_memory/cosmos_memory_client.py index 44ffa93..e3bbbf2 100644 --- a/agent_memory_toolkit/cosmos_memory_client.py +++ b/azure/cosmos/agent_memory/cosmos_memory_client.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Iterable, 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 @@ -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 @@ -90,6 +91,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 +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, ...]] = _normalize_metadata_keys(transcript_metadata_keys) if self._cosmos_endpoint: self.create_memory_store() logger.info("CosmosMemoryClient initialized") @@ -372,6 +375,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/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 81% rename from agent_memory_toolkit/services/_pipeline_helpers.py rename to azure/cosmos/agent_memory/services/_pipeline_helpers.py index 041c3eb..715b453 100644 --- a/agent_memory_toolkit/services/_pipeline_helpers.py +++ b/azure/cosmos/agent_memory/services/_pipeline_helpers.py @@ -14,9 +14,9 @@ import re from collections import defaultdict from pathlib import Path -from typing import Any, Optional +from typing import Any, Iterable, 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 @@ -181,10 +181,52 @@ 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[tuple[str, ...]], +) -> str: + """Render the trailing ``[metadata: {...}]`` segment for a transcript line. + + 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``. + """ + 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 "" + payload = json.dumps(filtered, separators=(",", ":"), ensure_ascii=False, default=str) + return f" [metadata: {payload}]" + + 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 +236,31 @@ 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. + + 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", "") - metadata = m.get("metadata", {}) - meta_str = f" [metadata: {json.dumps(metadata)}]" if metadata else "" + meta_str = _format_metadata_segment(m.get("metadata", {}), keys) lines.append(f"[{role}]: {content}{meta_str}") return "\n".join(lines) @@ -215,8 +274,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", {}), keys) parts.append(f"[{role}]: {content}{meta_str}") parts.append("") return "\n".join(parts) @@ -243,7 +301,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 +367,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 97% rename from agent_memory_toolkit/services/pipeline.py rename to azure/cosmos/agent_memory/services/pipeline.py index 051b24d..315fd05 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 @@ -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, @@ -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,14 +38,15 @@ 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, + _normalize_metadata_keys, build_topic_tags, build_transcript, cap_structured_summary, @@ -53,15 +54,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 @@ -161,6 +162,7 @@ def __init__( prompts_dir: str | None = None, *, containers: dict[ContainerKey, Any], + transcript_metadata_keys: Optional[Iterable[str]] = None, ) -> None: self._store = store self._containers = containers @@ -171,6 +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, ...]] = _normalize_metadata_keys(transcript_metadata_keys) def _run_prompty( self, @@ -214,13 +217,19 @@ 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) + # 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, + metadata_keys=getattr(self, "_transcript_metadata_keys", None), + ) def _load_existing_memories( self, 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 d84673e..0000000 Binary files a/function_app/_vendor/agent_memory_toolkit-0.1.0-py3-none-any.whl and /dev/null differ 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..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", @@ -123,6 +121,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..69450cc 100644 --- a/function_app/requirements.txt +++ b/function_app/requirements.txt @@ -2,21 +2,12 @@ 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 -# 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 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/agent_memory_toolkit-0.1.0-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..dc43fd0 100644 --- a/function_app/shared/pipeline_factory.py +++ b/function_app/shared/pipeline_factory.py @@ -1,11 +1,12 @@ """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. """ 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 @@ -26,12 +41,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() @@ -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/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..37b7cce 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, @@ -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/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_transcript_metadata.py b/tests/unit/services/test_transcript_metadata.py new file mode 100644 index 0000000..fa67bb8 --- /dev/null +++ b/tests/unit/services/test_transcript_metadata.py @@ -0,0 +1,329 @@ +"""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 + + +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",) 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():