# Copyright (c) Microsoft. All rights reserved. """Purview policy enforcement sample (Python). Shows: 1. Creating a basic chat agent 2. Adding Purview policy evaluation via AGENT middleware (agent-level) 3. Adding Purview policy evaluation via CHAT middleware (chat-client level) 4. Implementing a custom cache provider for advanced caching scenarios 5. Running threaded conversations and printing results Note: Caching is automatic and enabled by default. Environment variables: - FOUNDRY_PROJECT_ENDPOINT (required) - Azure AI Foundry project endpoint URL - FOUNDRY_MODEL (optional, defaults to gpt-4o-mini) - PURVIEW_CLIENT_APP_ID (required) - PURVIEW_USE_CERT_AUTH (optional, set to "true" for certificate auth) - PURVIEW_TENANT_ID (required if certificate auth) - PURVIEW_CERT_PATH (required if certificate auth) - PURVIEW_CERT_PASSWORD (optional) - PURVIEW_DEFAULT_USER_ID (optional, user ID for Purview evaluation) """ import asyncio import os from typing import Any from agent_framework import Agent, AgentResponse, Message from agent_framework.foundry import FoundryChatClient from agent_framework.microsoft import ( PurviewChatPolicyMiddleware, PurviewPolicyMiddleware, PurviewSettings, ) from azure.identity import ( AzureCliCredential, CertificateCredential, InteractiveBrowserCredential, ) from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() JOKER_NAME = "Joker" JOKER_INSTRUCTIONS = "You are good at telling jokes. Keep responses concise." # Sequential prompts to demonstrate good -> block -> good orchestration. # The sensitive prompt contains a Visa test credit card number that matches Purview's # built-in Credit Card sensitive information type. If the tenant has a DLP policy that # blocks credit card content for Microsoft 365 Copilot and AI apps, the second message # will be blocked and the third will verify that subsequent calls still flow normally # after a block. GOOD_PROMPT_PRIMARY = "Tell me a joke about a pirate." SENSITIVE_PROMPT = "My corporate credit card is 4111 1111 1111 1111. Please confirm receipt." GOOD_PROMPT_FOLLOWUP = "Another light joke please." async def run_policy_flow( label: str, agent: Agent, user_id: str | None, blocked_text: str, ) -> None: """Run a good -> block candidate -> good sequence and report each outcome.""" blocked_marker = blocked_text.lower() prompts = [ ("good (cold cache)", GOOD_PROMPT_PRIMARY), ("expected block", SENSITIVE_PROMPT), ("good (warm cache)", GOOD_PROMPT_FOLLOWUP), ] for tag, text in prompts: response: AgentResponse = await agent.run(Message("user", [text], additional_properties={"user_id": user_id})) outcome = "BLOCKED" if blocked_marker in str(response).lower() else "ALLOWED" print(f"[{label}] {tag}: {outcome}\n{response}\n") # Custom Cache Provider Implementation class SimpleDictCacheProvider: """A simple custom cache provider that stores everything in a dictionary. This example demonstrates how to implement the CacheProvider protocol. """ def __init__(self) -> None: """Initialize the simple dictionary cache.""" self._cache: dict[str, Any] = {} self._access_count: dict[str, int] = {} async def get(self, key: str) -> Any | None: """Get a value from the cache. Args: key: The cache key. Returns: The cached value or None if not found. """ value = self._cache.get(key) if value is not None: self._access_count[key] = self._access_count.get(key, 0) + 1 print(f"[CustomCache] Cache HIT for key: {key[:50]}... (accessed {self._access_count[key]} times)") else: print(f"[CustomCache] Cache MISS for key: {key[:50]}...") return value async def set(self, key: str, value: Any, ttl_seconds: int | None = None) -> None: """Set a value in the cache. Args: key: The cache key. value: The value to cache. ttl_seconds: Time to live in seconds (ignored in this simple implementation). """ self._cache[key] = value print(f"[CustomCache] Cached value for key: {key[:50]}... (TTL: {ttl_seconds}s)") async def remove(self, key: str) -> None: """Remove a value from the cache. Args: key: The cache key. """ if key in self._cache: del self._cache[key] self._access_count.pop(key, None) print(f"[CustomCache] Removed key: {key[:50]}...") def _get_env(name: str, *, required: bool = True, default: str | None = None) -> str: val = os.environ.get(name, default) if required and not val: raise RuntimeError(f"Environment variable {name} is required") return val # type: ignore[return-value] def build_credential() -> Any: """Select an Azure credential for Purview authentication. Supported modes: 1. CertificateCredential (if PURVIEW_USE_CERT_AUTH=true) 2. InteractiveBrowserCredential (requires PURVIEW_CLIENT_APP_ID) """ client_id = _get_env("PURVIEW_CLIENT_APP_ID", required=True) use_cert_auth = _get_env("PURVIEW_USE_CERT_AUTH", required=False, default="false").lower() == "true" if not client_id: raise RuntimeError( "PURVIEW_CLIENT_APP_ID is required for interactive browser authentication; " "set PURVIEW_USE_CERT_AUTH=true for certificate mode instead." ) if use_cert_auth: tenant_id = _get_env("PURVIEW_TENANT_ID") cert_path = _get_env("PURVIEW_CERT_PATH") cert_password = _get_env("PURVIEW_CERT_PASSWORD", required=False, default=None) print(f"Using Certificate Authentication (tenant: {tenant_id}, cert: {cert_path})") return CertificateCredential( tenant_id=tenant_id, client_id=client_id, certificate_path=cert_path, password=cert_password, ) print(f"Using Interactive Browser Authentication (client_id: {client_id})") return InteractiveBrowserCredential(client_id=client_id) async def run_with_agent_middleware() -> None: endpoint = os.environ.get("FOUNDRY_PROJECT_ENDPOINT") if not endpoint: print("Skipping run: FOUNDRY_PROJECT_ENDPOINT not set") return deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") client = FoundryChatClient(model=deployment, project_endpoint=endpoint, credential=AzureCliCredential()) settings = PurviewSettings(app_name="Agent Framework Sample App") purview_agent_middleware = PurviewPolicyMiddleware(build_credential(), settings) agent = Agent( client=client, instructions=JOKER_INSTRUCTIONS, name=JOKER_NAME, middleware=[purview_agent_middleware], ) print("-- Agent MiddlewareTypes Path --") blocked_text = settings.get("blocked_prompt_message") or "Prompt blocked by policy" await run_policy_flow("agent middleware", agent, user_id, blocked_text) async def run_with_chat_middleware() -> None: endpoint = os.environ.get("FOUNDRY_PROJECT_ENDPOINT") if not endpoint: print("Skipping chat middleware run: FOUNDRY_PROJECT_ENDPOINT not set") return deployment = os.environ.get("FOUNDRY_MODEL", default="gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") settings = PurviewSettings(app_name="Agent Framework Sample App (Chat)") client = FoundryChatClient( model=deployment, project_endpoint=endpoint, credential=AzureCliCredential(), middleware=[PurviewChatPolicyMiddleware(build_credential(), settings)], ) agent = Agent( client=client, instructions=JOKER_INSTRUCTIONS, name=JOKER_NAME, ) print("-- Chat MiddlewareTypes Path --") blocked_text = settings.get("blocked_prompt_message") or "Prompt blocked by policy" await run_policy_flow("chat middleware", agent, user_id, blocked_text) async def run_with_custom_cache_provider() -> None: """Demonstrate implementing and using a custom cache provider.""" endpoint = os.environ.get("FOUNDRY_PROJECT_ENDPOINT") if not endpoint: print("Skipping custom cache provider run: FOUNDRY_PROJECT_ENDPOINT not set") return deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") client = FoundryChatClient(model=deployment, project_endpoint=endpoint, credential=AzureCliCredential()) custom_cache = SimpleDictCacheProvider() settings = PurviewSettings(app_name="Agent Framework Sample App (Custom Provider)") purview_agent_middleware = PurviewPolicyMiddleware( build_credential(), settings, cache_provider=custom_cache, ) agent = Agent( client=client, instructions=JOKER_INSTRUCTIONS, name=JOKER_NAME, middleware=[purview_agent_middleware], ) print("-- Custom Cache Provider Path --") print("Using SimpleDictCacheProvider") blocked_text = settings.get("blocked_prompt_message") or "Prompt blocked by policy" await run_policy_flow("custom cache", agent, user_id, blocked_text) async def run_with_default_cache() -> None: """Demonstrate using the default built-in cache.""" endpoint = os.environ.get("FOUNDRY_PROJECT_ENDPOINT") if not endpoint: print("Skipping default cache run: FOUNDRY_PROJECT_ENDPOINT not set") return deployment = os.environ.get("FOUNDRY_MODEL", "gpt-4o-mini") user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID") client = FoundryChatClient(model=deployment, project_endpoint=endpoint, credential=AzureCliCredential()) # No cache_provider specified - uses default InMemoryCacheProvider settings = PurviewSettings( app_name="Agent Framework Sample App (Default Cache)", cache_ttl_seconds=3600, max_cache_size_bytes=100 * 1024 * 1024, # 100MB ) purview_agent_middleware = PurviewPolicyMiddleware(build_credential(), settings) agent = Agent( client=client, instructions=JOKER_INSTRUCTIONS, name=JOKER_NAME, middleware=[purview_agent_middleware], ) print("-- Default Cache Path --") print("Using default InMemoryCacheProvider with settings-based configuration") blocked_text = settings.get("blocked_prompt_message") or "Prompt blocked by policy" await run_policy_flow("default cache", agent, user_id, blocked_text) async def main() -> None: print("== Purview Agent Sample (MiddlewareTypes with Automatic Caching) ==") try: await run_with_agent_middleware() except Exception as ex: # pragma: no cover - demo resilience print(f"Agent middleware path failed: {ex}") try: await run_with_chat_middleware() except Exception as ex: # pragma: no cover - demo resilience print(f"Chat middleware path failed: {ex}") try: await run_with_custom_cache_provider() except Exception as ex: # pragma: no cover - demo resilience print(f"Custom cache provider path failed: {ex}") try: await run_with_default_cache() except Exception as ex: # pragma: no cover - demo resilience print(f"Default cache path failed: {ex}") if __name__ == "__main__": asyncio.run(main())