* python: replace pre-commit with prek, add PEP 723 script deps, clean up dev dependencies - Replace pre-commit with prek (Rust-native, faster pre-commit alternative) - Move supported hooks to repo: builtin for zero-clone speed - Add new builtin hooks: trailing-whitespace, check-merge-conflict, detect-private-key, check-added-large-files - Update all hook versions to latest (pre-commit-hooks v6, pyupgrade v3.21.2, bandit 1.9.3, uv-pre-commit 0.10.0) - Add PEP 723 inline script metadata to 34 samples with external deps - Remove autogen-agentchat/autogen-ext from dev deps (now declared per-sample) - Remove unused dev deps: pytest-env, tomli-w - Add agent-framework-core>=1.0.0b260130 lower bound to all 21 packages - Update CI workflow to use j178/prek-action - Update docs: DEV_SETUP.md, AGENTS.md, CODING_STANDARD.md, SAMPLE_GUIDELINES.md * updated lock * python: fix prek config paths for local execution and CI workflow Remove global 'files: ^python/' filter and strip python/ prefix from all path patterns in .pre-commit-config.yaml so prek finds files when run from the python/ directory. Update CI workflow to use --cd python instead of --config path. Include trailing whitespace fixes and dev dependency cleanup. * python: move helper scripts to scripts/ folder and exclude from checks * python: exclude AGENTS.md from prek markdown code lint * python: exclude AGENTS.md and azure_ai_search sample from markdown lint * fix m365 sample * python: ignore CPY rule for samples with PEP 723 headers * fix in dev_setup * python: replace aiofiles with regular open in samples * python: suppress reportUnusedImport in markdown code block checker * python: use samples pyright config for markdown code block checker Write a temp pyrightconfig.json matching pyrightconfig.samples.json rules (typeCheckingMode=off, only reportMissingImports and reportAttributeAccessIssue). Filter output to only fail on these rules since syntax-level errors (top-level await, undefined vars) are expected in README documentation snippets. * python: use markdown-code-lint with fixed globs instead of prek file list The prek-markdown-code-lint task received all changed files including non-README markdown and files with pre-existing broken imports. Replace with the standard markdown-code-lint task which uses the correct glob patterns (README.md, packages/**/README.md, samples/**/*.md). * python: exclude READMEs with pre-existing broken imports from markdown lint * python: fix broken README code snippets instead of excluding them - ag-ui: replace TextContent (removed) with content.type == 'text' - durabletask: fix import path to durabletask.worker.TaskHubGrpcWorker - orchestrations: use constructor params instead of .participants() method - observability: mark deprecated code blocks as plain text, filter reportMissingImports to agent_framework modules only - remove README excludes from markdown-code-lint task * add revision to gaia download * feat(python): parallelize checks across packages Run (package × task) cross-product in parallel using ThreadPoolExecutor and subprocesses. Key changes: - Add scripts/task_runner.py with shared parallel execution engine - Update run_tasks_in_packages_if_exists.py to accept multiple tasks - Update run_tasks_in_changed_packages.py with --files flag and parallel support - Add check-packages poe task (fmt+lint+pyright+mypy in parallel) - Add prek-markdown-code-lint and prek-samples-check with change detection - Split CI code quality workflow into parallel prek and mypy jobs - Update DEV_SETUP.md to document new parallel behavior Core package changes still trigger checks on all packages. * feat(ci): split code quality into 4 parallel jobs Split the single prek job into parallel jobs: - pre-commit-hooks: lightweight hooks (SKIP=poe-check) - package-checks: fmt/lint/pyright/mypy via check-packages - samples-markdown: samples-lint, samples-syntax, markdown-code-lint - mypy: change-detected mypy checks All 4 jobs run concurrently (×2 Python versions = 8 runners). * feat(ci): use only Python 3.10 for code quality checks * refactor(python): add future annotations and remove quoted types Add `from __future__ import annotations` to 93 package files that used quoted string annotations, then run pyupgrade --py310-plus to remove the now-unnecessary quotes. Fixes https://github.com/microsoft/agent-framework/issues/3578
Microsoft Agent Framework – Purview Integration (Python)
agent-framework-purview adds Microsoft Purview (Microsoft Graph dataSecurityAndGovernance) policy evaluation to the Microsoft Agent Framework. It lets you enforce data security / governance policies on both the prompt (user input + conversation history) and the model response before they proceed further in your workflow.
Status: Preview
Key Features
- Middleware-based policy enforcement (agent-level and chat-client level)
- Blocks or allows content at both ingress (prompt) and egress (response)
- Works with any
ChatAgent/ agent orchestration using the standard Agent Framework middleware pipeline - Supports both synchronous
TokenCredentialandAsyncTokenCredentialfromazure-identity - Configuration via
PurviewSettings/PurviewAppLocation - Built-in caching with configurable TTL and size limits for protection scopes in
PurviewSettings - Background processing for content activities and offline policy evaluation
When to Use
Add Purview when you need to:
- Prevent sensitive data leaks: Inline blocking of sensitive content based on Data Loss Prevention (DLP) policies.
- Enable governance: Log AI interactions in Purview for Audit, Communication Compliance, Insider Risk Management, eDiscovery, and Data Lifecycle Management.
- Prevent sensitive or disallowed content from being sent to an LLM
- Prevent model output containing disallowed data from leaving the system
- Apply centrally managed policies without rewriting agent logic
Prerequisites
- Microsoft Azure subscription with Microsoft Purview configured.
- Microsoft 365 subscription with an E5 license and pay-as-you-go billing setup.
- For testing, you can use a Microsoft 365 Developer Program tenant. For more information, see Join the Microsoft 365 Developer Program.
Authentication
PurviewClient uses the azure-identity library for token acquisition. You can use any TokenCredential or AsyncTokenCredential implementation.
-
Entra registration: Register your agent and add the required Microsoft Graph permissions (
dataSecurityAndGovernance) to the Service Principal. For more information, see Register an application in Microsoft Entra ID and dataSecurityAndGovernance resource type. You'll need the Microsoft Entra app ID in the next step. -
Graph Permissions:
-
ProtectionScopes.Compute.All : userProtectionScopeContainer
-
Content.Process.All : processContent
-
ContentActivity.Write : contentActivity
-
Purview policies: Configure Purview policies using the Microsoft Entra app ID to enable agent communications data to flow into Purview. For more information, see Configure Microsoft Purview.
Scopes
PurviewSettings.get_scopes() derives the Graph scope list (currently https://graph.microsoft.com/.default style).
Quick Start
import asyncio
from agent_framework import ChatAgent, ChatMessage, Role
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.microsoft import PurviewPolicyMiddleware, PurviewSettings
from azure.identity import InteractiveBrowserCredential
async def main():
chat_client = AzureOpenAIChatClient() # uses environment for endpoint + deployment
purview_middleware = PurviewPolicyMiddleware(
credential=InteractiveBrowserCredential(),
settings=PurviewSettings(app_name="My Sample App")
)
agent = ChatAgent(
chat_client=chat_client,
instructions="You are a helpful assistant.",
middleware=[purview_middleware]
)
response = await agent.run(ChatMessage("user", ["Summarize zero trust in one sentence."]))
print(response)
asyncio.run(main())
If a policy violation is detected on the prompt, the middleware terminates the run and substitutes a system message: "Prompt blocked by policy". If on the response, the result becomes "Response blocked by policy".
Configuration
PurviewSettings
PurviewSettings(
app_name="My App", # Required: Display / logical name
app_version=None, # Optional: Version string of the application
tenant_id=None, # Optional: Tenant id (guid), used mainly for auth context
purview_app_location=None, # Optional: PurviewAppLocation for scoping
graph_base_uri="https://graph.microsoft.com/v1.0/",
blocked_prompt_message="Prompt blocked by policy", # Custom message for blocked prompts
blocked_response_message="Response blocked by policy", # Custom message for blocked responses
ignore_exceptions=False, # If True, non-payment exceptions are logged but not thrown
ignore_payment_required=False, # If True, 402 payment required errors are logged but not thrown
cache_ttl_seconds=14400, # Cache TTL in seconds (default 4 hours)
max_cache_size_bytes=200 * 1024 * 1024 # Max cache size in bytes (default 200MB)
)
Caching
The Purview integration includes built-in caching for protection scopes responses to improve performance and reduce API calls:
- Default TTL: 4 hours (14400 seconds)
- Default Cache Size: 200MB
- Cache Provider:
InMemoryCacheProvideris used by default, but you can provide a custom implementation via theCacheProviderprotocol - Cache Invalidation: Cache is automatically invalidated when protection scope state is modified
- Exception Caching: 402 Payment Required errors are cached to avoid repeated failed API calls
You can customize caching behavior in PurviewSettings:
from agent_framework.microsoft import PurviewSettings
settings = PurviewSettings(
app_name="My App",
cache_ttl_seconds=14400, # 4 hours
max_cache_size_bytes=200 * 1024 * 1024 # 200MB
)
Or provide your own cache provider:
from typing import Any
from agent_framework.microsoft import PurviewPolicyMiddleware, PurviewSettings, CacheProvider
from azure.identity import DefaultAzureCredential
class MyCustomCache(CacheProvider):
async def get(self, key: str) -> Any | None:
# Your implementation
pass
async def set(self, key: str, value: Any, ttl_seconds: int | None = None) -> None:
# Your implementation
pass
async def remove(self, key: str) -> None:
# Your implementation
pass
credential = DefaultAzureCredential()
settings = PurviewSettings(app_name="MyApp")
middleware = PurviewPolicyMiddleware(
credential=credential,
settings=settings,
cache_provider=MyCustomCache()
)
To scope evaluation by location (application, URL, or domain):
from agent_framework.microsoft import (
PurviewAppLocation,
PurviewLocationType,
PurviewSettings,
)
settings = PurviewSettings(
app_name="Contoso Support",
purview_app_location=PurviewAppLocation(
location_type=PurviewLocationType.APPLICATION,
location_value="<app-client-id>"
)
)
Customizing Blocked Messages
By default, when Purview blocks a prompt or response, the middleware returns a generic system message. You can customize these messages by providing your own text in the PurviewSettings:
from agent_framework.microsoft import PurviewSettings
settings = PurviewSettings(
app_name="My App",
blocked_prompt_message="Your request contains content that violates our policies. Please rephrase and try again.",
blocked_response_message="The response was blocked due to policy restrictions. Please contact support if you need assistance."
)
Exception Handling Controls
The Purview integration provides fine-grained control over exception handling to support graceful degradation scenarios:
from agent_framework.microsoft import PurviewSettings
# Ignore all non-payment exceptions (continue execution even if policy check fails)
settings = PurviewSettings(
app_name="My App",
ignore_exceptions=True # Log errors but don't throw
)
# Ignore only 402 Payment Required errors (useful for tenants without proper licensing)
settings = PurviewSettings(
app_name="My App",
ignore_payment_required=True # Continue even without Purview Consumptive Billing Setup
)
# Both can be combined
settings = PurviewSettings(
app_name="My App",
ignore_exceptions=True,
ignore_payment_required=True
)
Selecting Agent vs Chat Middleware
Use the agent middleware when you already have / want the full agent pipeline:
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.microsoft import PurviewPolicyMiddleware, PurviewSettings
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
client = AzureOpenAIChatClient()
agent = ChatAgent(
chat_client=client,
instructions="You are helpful.",
middleware=[PurviewPolicyMiddleware(credential, PurviewSettings(app_name="My App"))]
)
Use the chat middleware when you attach directly to a chat client (e.g. minimal agent shell or custom orchestration):
import os
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.microsoft import PurviewChatPolicyMiddleware, PurviewSettings
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
chat_client = AzureOpenAIChatClient(
deployment_name=os.environ["AZURE_OPENAI_DEPLOYMENT_NAME"],
endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
credential=credential,
middleware=[
PurviewChatPolicyMiddleware(credential, PurviewSettings(app_name="My App (Chat)"))
],
)
agent = ChatAgent(chat_client=chat_client, instructions="You are helpful.")
The policy logic is identical; the difference is only the hook point in the pipeline.
Middleware Lifecycle
- Before agent execution (
prompt phase): allcontext.messagesare evaluated.- If no valid user_id is found, processing is skipped (no policy evaluation)
- Protection scopes are retrieved (with caching)
- Applicable scopes are checked to determine execution mode
- In inline mode: content is evaluated immediately
- In offline mode: evaluation is queued in background
- If blocked:
context.resultis replaced with a system message andcontext.terminate = True. - After successful agent execution (
response phase): the produced messages are evaluated using the same user_id from the prompt phase. - If blocked: result messages are replaced with a blocking notice.
The user identifier is discovered from ChatMessage.additional_properties['user_id'] during the prompt phase and reused for the response phase, ensuring both evaluations map consistently to the same user. If no user_id is present, policy evaluation is skipped entirely.
You can customize the blocking messages using the blocked_prompt_message and blocked_response_message fields in PurviewSettings. For more advanced scenarios, you can wrap the middleware or post-process context.result in later middleware.
Exceptions
| Exception | Scenario |
|---|---|
PurviewPaymentRequiredError |
402 Payment Required - tenant lacks proper Purview licensing or consumptive billing setup |
PurviewAuthenticationError |
Token acquisition / validation issues |
PurviewRateLimitError |
429 responses from service |
PurviewRequestError |
4xx client errors (bad input, unauthorized, forbidden) |
PurviewServiceError |
5xx or unexpected service errors |
Exception Handling
All exceptions inherit from PurviewServiceError. You can catch specific exceptions or use the base class:
from agent_framework.microsoft import (
PurviewPaymentRequiredError,
PurviewAuthenticationError,
PurviewRateLimitError,
PurviewRequestError,
PurviewServiceError
)
try:
# Your code here
pass
except PurviewPaymentRequiredError as ex:
# Handle licensing issues specifically
print(f"Purview licensing required: {ex}")
except (PurviewAuthenticationError, PurviewRateLimitError, PurviewRequestError, PurviewServiceError) as ex:
# Handle other errors
print(f"Purview enforcement skipped: {ex}")
Notes
- User Identification: Provide a
user_idper request (e.g. inChatMessage(..., additional_properties={"user_id": "<guid>"})) for per-user policy scoping. If no user_id is provided, policy evaluation is skipped entirely. - Blocking Messages: Can be customized via
blocked_prompt_messageandblocked_response_messageinPurviewSettings. By default, they are "Prompt blocked by policy" and "Response blocked by policy" respectively. - Streaming Responses: Post-response policy evaluation presently applies only to non-streaming chat responses.
- Error Handling: Use
ignore_exceptionsandignore_payment_requiredsettings for graceful degradation. When enabled, errors are logged but don't fail the request. - Caching: Protection scopes responses and 402 errors are cached by default with a 4-hour TTL. Cache is automatically invalidated when protection scope state changes.
- Background Processing: Content Activities and offline Process Content requests are handled asynchronously using background tasks to avoid blocking the main execution flow.