Python: Add Purview Middleware (#1142)

* [Py Purview] Purview Python Initial Commit

* [Py Purview] Purview Python Minor Fixes

* [Py Purview] Purview Python Comment Fixesish

* [Py Purview] Purview Python Agent Middleware Done

* [Py Purview] Purview Python Agent Middleware Done

* [Py Purview] Purview Python Lint Errors

* [Py Purview] Purview Python Final Hopefully

* [Py Purview] Purview Python Final Hopefully

* [Py Purview] Purview Python Fix ReadMe

* [Py Purview] Purview Python Fix MyPy

* [Py Purview] Purview Python Minor Updates on comments

* [Py Purview] Purview Python Fix Build Error

---------

Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
This commit is contained in:
Rishabh Chawla
2025-10-16 14:46:04 -07:00
committed by GitHub
Unverified
parent 76ae0a62ac
commit 59da578902
24 changed files with 3707 additions and 5 deletions
@@ -0,0 +1,88 @@
## Purview Policy Enforcement Sample (Python)
This getting-started sample shows how to attach Microsoft Purview policy evaluation to an Agent Framework `ChatAgent` using the **middleware** approach.
1. Configure an Azure OpenAI chat client
2. Add Purview policy enforcement middleware (`PurviewPolicyMiddleware`)
3. Run a short conversation and observe prompt / response blocking behavior
---
## 1. Setup
### Required Environment Variables
| Variable | Required | Purpose |
|----------|----------|---------|
| `AZURE_OPENAI_ENDPOINT` | Yes | Azure OpenAI endpoint (https://<name>.openai.azure.com) |
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Optional | Model deployment name (defaults inside SDK if omitted) |
| `PURVIEW_CLIENT_APP_ID` | Yes* | Client (application) ID used for Purview authentication |
| `PURVIEW_USE_CERT_AUTH` | Optional (`true`/`false`) | Switch between certificate and interactive auth |
| `PURVIEW_TENANT_ID` | Yes (when cert auth on) | Tenant ID for certificate authentication |
| `PURVIEW_CERT_PATH` | Yes (when cert auth on) | Path to your .pfx certificate |
| `PURVIEW_CERT_PASSWORD` | Optional | Password for encrypted certs |
*A demo default exists in code for illustration only—always set your own value.
### 2. Auth Modes Supported
#### A. Interactive Browser Authentication (default)
Opens a browser on first run to sign in.
```powershell
$env:AZURE_OPENAI_ENDPOINT = "https://your-openai-instance.openai.azure.com"
$env:PURVIEW_CLIENT_APP_ID = "00000000-0000-0000-0000-000000000000"
```
#### B. Certificate Authentication
For headless / CI scenarios.
```powershell
$env:PURVIEW_USE_CERT_AUTH = "true"
$env:PURVIEW_TENANT_ID = "<tenant-guid>"
$env:PURVIEW_CERT_PATH = "C:\path\to\cert.pfx"
$env:PURVIEW_CERT_PASSWORD = "optional-password"
```
Certificate steps (summary): create / register app, generate certificate, upload public key, export .pfx with private key, grant required Graph / Purview permissions.
---
## 3. Run the Sample
From repo root:
```powershell
cd python/samples/getting_started/purview_agent
python sample_purview_agent.py
```
If interactive auth is used, a browser window will appear the first time.
---
## 4. How It Works
1. Builds an Azure OpenAI chat client (using the environment endpoint / deployment)
2. Chooses credential mode (certificate vs interactive)
3. Creates `PurviewPolicyMiddleware` with `PurviewSettings`
4. Injects middleware into the agent at construction
5. Sends two user messages sequentially
6. Prints results (or policy block messages)
Prompt blocks set a system-level message: `Prompt blocked by policy` and terminate the run early. Response blocks rewrite the output to `Response blocked by policy`.
---
## 5. Code Snippet (Middleware Injection)
```python
agent = ChatAgent(
chat_client=chat_client,
instructions="You are good at telling jokes.",
name="Joker",
middleware=[
PurviewPolicyMiddleware(credential, PurviewSettings(app_name="Sample App", default_user_id="<guid>"))
],
)
```
---
@@ -0,0 +1,179 @@
# 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. Running a threaded conversation and printing results
Environment variables:
- AZURE_OPENAI_ENDPOINT (required)
- AZURE_OPENAI_DEPLOYMENT_NAME (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)
"""
from __future__ import annotations
import asyncio
import os
from typing import Any
from agent_framework import AgentRunResponse, ChatAgent, ChatMessage, Role
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import (
AzureCliCredential,
CertificateCredential,
InteractiveBrowserCredential,
)
# Purview integration pieces
from agent_framework.microsoft import (
PurviewPolicyMiddleware,
PurviewChatPolicyMiddleware,
PurviewSettings,
)
JOKER_NAME = "Joker"
JOKER_INSTRUCTIONS = "You are good at telling jokes. Keep responses concise."
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("AZURE_OPENAI_ENDPOINT")
if not endpoint:
print("Skipping run: AZURE_OPENAI_ENDPOINT not set")
return
deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini")
user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID")
chat_client = AzureOpenAIChatClient(deployment_name=deployment, endpoint=endpoint, credential=AzureCliCredential())
purview_agent_middleware = PurviewPolicyMiddleware(
build_credential(),
PurviewSettings(
app_name="Agent Framework Sample App",
),
)
agent = ChatAgent(
chat_client=chat_client,
instructions=JOKER_INSTRUCTIONS,
name=JOKER_NAME,
middleware=purview_agent_middleware,
)
print("-- Agent Middleware Path --")
first: AgentRunResponse = await agent.run(ChatMessage(role=Role.USER, text="Tell me a joke about a pirate.", additional_properties={"user_id": user_id}))
print("First response (agent middleware):\n", first)
second: AgentRunResponse = await agent.run(ChatMessage(role=Role.USER, text="That was funny. Tell me another one.", additional_properties={"user_id": user_id}))
print("Second response (agent middleware):\n", second)
async def run_with_chat_middleware() -> None:
endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
if not endpoint:
print("Skipping chat middleware run: AZURE_OPENAI_ENDPOINT not set")
return
deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", default="gpt-4o-mini")
user_id = os.environ.get("PURVIEW_DEFAULT_USER_ID")
chat_client = AzureOpenAIChatClient(
deployment_name=deployment,
endpoint=endpoint,
credential=AzureCliCredential(),
middleware=[
PurviewChatPolicyMiddleware(
build_credential(),
PurviewSettings(
app_name="Agent Framework Sample App (Chat)",
),
)
],
)
agent = ChatAgent(
chat_client=chat_client,
instructions=JOKER_INSTRUCTIONS,
name=JOKER_NAME,
)
print("-- Chat Middleware Path --")
first: AgentRunResponse = await agent.run(
ChatMessage(
role=Role.USER,
text="Give me a short clean joke.",
additional_properties={"user_id": user_id},
)
)
print("First response (chat middleware):\n", first)
second: AgentRunResponse = await agent.run(
ChatMessage(
role=Role.USER,
text="One more please.",
additional_properties={"user_id": user_id},
)
)
print("Second response (chat middleware):\n", second)
async def main() -> None:
print("== Purview Agent Sample (Agent & Chat Middleware) ==")
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}")
if __name__ == "__main__":
asyncio.run(main())