Skip to content

Add content safety guardrail sample for the invocations protocol - #1019

Merged
Ankit Sinha (ankitbko) merged 4 commits into
mainfrom
amitbhave/invocations-content-safety-guardrail
Sep 24, 2026
Merged

Ankit Sinha (ankitbko) merged 4 commits into
mainfrom
amitbhave/invocations-content-safety-guardrail

Conversation

@amitbhave10

@amitbhave10 Amit Bhave (amitbhave10) commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

What this adds

samples/python/hosted-agents/agent-framework/invocations/02-content-safety-guardrail — the invocations counterpart to the existing responses guardrail sample.

Why

On the responses protocol the platform knows the payload shape, so attaching an RAI policy is enough. On invocations the request and response bodies are agent-defined, so a policy on its own screens nothing — it is attached and inert.

The sample shows the accompanying invocationsModeration block that tells the platform where the prompt and response text live:

policies:
  - type: rai_policy
    raiPolicyName: <ARM resource ID>
    invocationsModeration:
      responseMode: both
      inputContentType: json
      outputContentType: json
      inputPaths: [$.message]
      outputPaths: [$.response]
      streamSelectors:
        - eventType: message.delta
          textField: text

Note textField: text is a field name, not a path expression. A $-prefixed value silently matches nothing and leaves streamed output unscreened; that footgun is fixed separately in Azure/azure-dev#9791 / Azure/azure-dev#10130, and this sample follows the corrected form.

responseMode: both declares a capability, not "input and output" — the agent answers buffered and streamed requests, and the platform picks exactly one gate at runtime from the response Content-Type.

Conventions followed

  • uv, not requirements.txt — ships pyproject.toml + uv.lock + uv.toml per DEPENDENCY_POLICY.md, matching the sibling samples migrated in Migrate some Python Agent Framework samples to uv #992. Lock generated with the CI-pinned uv==0.11.7 (revision = 3).
  • Dockerfile is the uv template used by the sibling guardrail sample, so codeConfiguration.runtime: python_3_13 and the image base agree.
  • test-spec.yml, the preferred behavior contract, rather than the legacy test-payload.txt.

The contract asserts that benign traffic still flows through both gates untouched. It deliberately does not assert blocking: that needs a real RAI policy, and the sample ships a placeholder ID. Benign pass-through is the deterministic signal that regresses if the moderation wiring is wrong.

Validation run locally

Check Result
check_hosted_agent_contracts.py --base <merge-base> --head HEAD Validated behavior contracts for 1 new hosted-agent sample(s).
check-hosted-agent-python-requirements.py --resolve Python Hosted Agent dependency-policy check passed.
hosted_agent_test_spec.py validate Valid hosted-agent test spec
hosted_agent_test_spec.py plan --protocol invocations plan generated, test applicable
uv lock --check (uv 0.11.7) resolved, lock current
ruff check / black --check clean

The end-to-end guardrail behavior this sample documents was separately verified against a hosted agent in canadacentral prod: with textField set to the frame's field name a violating response is blocked with content_filter, and a control run without invocationsModeration was not blocked.

Note on venue

I originally raised this against the private staging repo (foundry-samples-pr#1042) before re-reading CONTRIBUTING.md, which states publishable changes should come through a public same-repository pull request rather than an internal publication workflow. This PR supersedes it; I'll close that one.

Adds samples/python/hosted-agents/agent-framework/invocations/02-content-safety-guardrail,
the invocations counterpart to the existing responses guardrail sample.

On the invocations protocol the request and response bodies are agent-defined, so
attaching an RAI policy alone screens nothing. The sample shows the accompanying
invocationsModeration block that tells the platform where the prompt and response
text live, including the stream selector used for streamed frames.

Uses uv (pyproject.toml + uv.lock + uv.toml) per the Python hosted-agent dependency
policy, and registers a test-spec.yml behavior contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d91d7b94-da15-4dac-b0d4-195fad8fa01b
@amitbhave10

Copy link
Copy Markdown
Contributor Author

Re-verified the sample end to end after converting it from requirements.txt to uv, since that change swapped both the resolver and the base image (python:3.12-slim → python:3.13-slim).

Functional

Ran the locked environment against a real Foundry project (agents-model-validation-e2e-tests-swedencentral, gpt-4.1-mini):

  • Buffered: {"response":"Understood. INVOCATIONS-GUARDRAIL-OK"} — matches the declared outputPaths: [$.response], and satisfies the test-spec.yml assertion.
  • Streamed: frames are exactly what streamSelectors declares —
    data: {"type": "message.delta", "text": "Hello"}
    data: {"type": "message.delta", "text": "!"}
    data: {"type": "message.done"}
    
    eventType: message.delta matches the frame's type value and textField: text is the bare field name, so every text-bearing frame is screened and the terminal message.done is correctly ignored.
  • Container: builds and serves; the credential-free path ({"stream":false} with no message) returns 400, confirming the handler is reached through the uv-built venv.

All imports resolve from the lock — python-dotenv, starlette, and azure-ai-agentserver-invocations come in transitively (108 packages).

Latency

Three cold starts per variant, docker run → first accepted request:

Variant Image size Cold start
Original pip / py3.12 497 MB 1.94 / 2.10 / 3.09 s
This PR (uv / py3.13) 508 MB 1.92 / 1.97 / 2.18 s

No regression — the uv conversion is latency-neutral.

While measuring I found that the shared uv template ships the 56 MB uv binary in the runtime image even though CMD never uses it; confining uv to a build stage takes this sample from 508 MB to 418 MB with identical startup. That affects 20 samples, so I raised it as #1020 rather than diverging this one — the Dockerfile here intentionally stays identical to its siblings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 3 Low severity

Open (4)
What changed in this PR

Adds a Python Agent Framework Invocations sample demonstrating RAI content safety configuration through invocationsModeration.

Changes:

  • Adds structured buffered and streaming invocation handling with moderation paths/selectors.
  • Adds uv-based packaging, Docker configuration, and Azure deployment manifest.
  • Adds documentation, repository indexes, and a benign behavior contract.
File Description
samples/​python/​hosted-agents/​README.md Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​README.md Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​src/​agent-framework-content-safety-guardrail-invocations/​uv.toml Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​src/​agent-framework-content-safety-guardrail-invocations/​pyproject.toml Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​src/​agent-framework-content-safety-guardrail-invocations/​main.py Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​src/​agent-framework-content-safety-guardrail-invocations/​Dockerfile Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​src/​agent-framework-content-safety-guardrail-invocations/​.env.example Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​src/​agent-framework-content-safety-guardrail-invocations/​.dockerignore Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​src/​agent-framework-content-safety-guardrail-invocations/​.azdignore Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​README.md Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​CLAUDE.md Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​azure.yaml Updated as part of this pull request.
samples/​python/​hosted-agents/​agent-framework/​invocations/​02-content-safety-guardrail/​AGENTS.md Updated as part of this pull request.
.azure-pipelines/​hosted-agent-tests/​python/​agent-framework/​invocations/​02-content-safety-guardrail/​test-spec.yml Updated as part of this pull request.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

- Add the repository's established azd: '>=1.27.1' floor to requiredVersions;
  this manifest was the only Python Agent Framework sample missing it (25/26).
- Describe inputPaths/outputPaths as selector expressions rather than JSONPath,
  and document the supported subset plus the textField field-name contract.
- Use the {"message": "..."} payload form for azd ai agent invoke in README.md
  and AGENTS.md; the handler parses JSON and requires a message property, so the
  bare-string form fails.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d91d7b94-da15-4dac-b0d4-195fad8fa01b

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (4)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Add a streaming turn to exercise streamSelectors

.azure-pipelines/​hosted-agent-tests/​python/​agent-framework/​invocations/​02-content-safety-guardrail/​test-spec.yml:19

The only turn leaves stream false, so this contract exercises the buffered outputPaths gate but never the streaming branch or its streamSelectors, which are the defining additions of this sample. A benign buffered response would still pass if the streaming wiring were broken or absent; add a second benign turn with stream: true and assert its streamed output as well.

The inline environmentVariables list is the deprecated shape; azd still reads
it and still expands ${...} in it, but it is retained only for mid-migration
compatibility. This sample was the only one of 87 hosted-agent manifests still
using it. Switch to the service-level env mapping so the manifest matches the
rest of the repository.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d91d7b94-da15-4dac-b0d4-195fad8fa01b

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

One or more issues must be addressed before approval.

Review effort: Lite
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Add streaming test coverage for streamSelectors

.azure-pipelines/​hosted-agent-tests/​python/​agent-framework/​invocations/​02-content-safety-guardrail/​test-spec.yml:19

This contract only sends the default buffered request because stream is omitted, so it never exercises the structured SSE path or the new streamSelectors configuration. Add a second turn (or separate test) with stream: true and the same marker so the claimed streaming gate is covered as well.

@ankitbko
Ankit Sinha (ankitbko) merged commit 5642c45 into main Sep 24, 2026
28 checks passed
@ankitbko
Ankit Sinha (ankitbko) deleted the amitbhave/invocations-content-safety-guardrail branch September 24, 2026 07:46
Rohan Deshpande (appwiz) pushed a commit to appwiz/foundry-samples that referenced this pull request Sep 27, 2026
* feat(langchain): add custom store sample

* chore: update sdk version

* add test spec

* chore: refine readme and remove unused files

* align agents and README structure with the template

* fix previous_respond_id vs conversation id in the document

* fix: add the limitations section

* simplify wording

This branch was successfully deployed

1 active deployment
L4-validation — 7574b04f Deployed Sep 23, 2026 by amitbhave10 via trusted #333
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants