Skip to content

Add static prompt evaluation and red-team framework - #1398

Open
Cedric Vidal (cedricvidal) wants to merge 23 commits into
microsoft:mainfrom
cedricvidal:cedricvidal-plan-static-prompt-evals
Open

Cedric Vidal (cedricvidal) wants to merge 23 commits into
microsoft:mainfrom
cedricvidal:cedricvidal-plan-static-prompt-evals

Conversation

@cedricvidal

@cedricvidal Cedric Vidal (cedricvidal) commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add production-backed quality evaluations for 10 static prompt families and cloud red-team orchestration for 8 user-controlled instruction surfaces.
  • Commit 222 curated input cases; keep generated responses and evaluation artifacts ignored.
  • Add rubric-driven grading, gate-based reports, historical rubric snapshots, immutable selective regrading, and case-level evidence.
  • Correct native grader parsing, task context, SDK tool-call schemas, incomplete-sample handling, and replay source protection.
  • Document evaluation maintenance and case-context review. No GitHub Actions workflows are changed.

Scope and evidence

Quality orchestration runs locally using Azure AI Evaluation SDK graders.
The latest retained 222-case assessment reuses recorded responses: 221 successful generation rows and one error. Execution completed, acceptance failed, and assessment integrity remains incomplete. Blocking gates: 65 passed, 14 failed, 8 unresolved; 2 advisory violations.

The focused Python report, CLI, and quality suite passed 91 tests. Prior workspace validation covered dataset/target validation, TypeScript typechecking and tests, focused production prompt tests, and offline smoke execution.

Aggregate pass-rate floors above 80% were lowered in the rubric only. The engine continues to honor configured rates through 100%; mean-score requirements and lower floors are unchanged.

Generated artifacts and unrelated local editor settings are not included. The interactive review extension remains session-scoped, outside the repository.

Known limitations and follow-up

  • Suggested-feature novelty receives existing-feature detection output instead of generated suggestions in 28 of 30 current cases; these scores are not reliable novelty assessments.
  • Feature-authoring candidate checking can omit child references because it selects the first configured field instead of combining parent and child IDs.
  • Variation duplicate checking can omit the original existingPrompt and does not combine all relevant prompt collections.
  • Dependency references need validation and explicit authority/completeness rules before adopting deterministic set-based grading.
  • Feature-extraction mean requirements of 1.0 still require perfection despite separate 80% pass-rate floors. Custom 1-5 rubrics need behavioral score anchors.
  • Missing original judge tool traces, one report-generation error, and incomplete generator provenance remain visible limitations; no evidence was fabricated.
  • The cloud red-team baseline is blocked by missing Foundry Microsoft.CognitiveServices/accounts/AIServices/evaluations/write permission. Quality results are not a completed safety/red-team assessment or a clean pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Retain lower floors and exact mean/case requirements; preserve hash-verified historical policy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Validate nested labels and finite scores; normalize SDK conversations and recorded tool history without fabricating evidence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Share versioned decisions between Markdown and review clients, retain policy snapshots and hashes, and explicitly select affected graders while reusing source responses. Document integrity limits and regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Emit decision-summary.json for new runs and provide report --decision-output --decision-only outside the source run, retaining hash-verified historical policy identities.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove runtime and baseline caps, accept configured pass-rate floors through 100%, and retain exact historical snapshot semantics and policy-change provenance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Strip standalone forwarded separators before argparse and cover decision-only exports through both direct and pnpm-style arguments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… early

Require expected sample coverage before case verdicts or means, retaining case-level diversity semantics. Resolve source/results paths before creating artifacts and reject equal, nested, or symlink-aliased source destinations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ostics

Use flat name/arguments/tool_call_id fields and validate actual SDK converter inputs before evaluation. Capture SDK run-summary errors for input-only native failures and preserve diagnostic sidecars through replay.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@cedricvidal
Cedric Vidal (cedricvidal) marked this pull request as ready for review September 10, 2026 03:03
Resolve prompt composition conflicts by retaining shared builders and routing production calls through adaptive chat completion. Preserve both evaluation guidance and upstream contribution instructions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Declare shared and telemetry workspace dependencies for recursive build ordering. Allow only the verified OpenAPI content fingerprint in the dataset manifest under the generic API key rule.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

Test Results (Node.js 22)

test: Run #55

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
2686 2686 0 0 0 0 0 3m10s

🎉 All tests passed!

Github Test Reporter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking review: these findings are follow-up suggestions, not requests to block merging. The comments cover generator selection, grading correctness, evidence retention, replay, and red-team lifecycle behavior.

const deployment =
configuredModel ||
process.env.SCOPE_EVAL_AZURE_OPENAI_DEPLOYMENT ||
process.env.AZURE_OPENAI_DEPLOYMENT;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: configuring the Azure grader endpoint, API key, and deployment also switches generation to that deployment, ignoring PROMPT_EVAL_MODEL. With PROMPT_EVAL_MODEL=intended-generator and SCOPE_EVAL_AZURE_OPENAI_DEPLOYMENT=separate-grader, the transport sends generation to separate-grader. Could we keep generator endpoint/model selection separate from grader configuration? Otherwise a run can evaluate a different model than the developer requested.

f"unknown deterministic check {check_name!r} for {family!r}"
)
for row in family_rows:
observations.extend(check(row, raw_config))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: infrastructure-error rows still reach every output-dependent check here. A transport failure gives unresolved generation_success and output_schema, but failed non_empty_prompt and valid_identifier, even though no response exists. Could these checks propagate the infrastructure failure as unresolved, including case-level diversity? This would prevent an outage from being reported as a prompt-quality regression.

},
invocationMetadata: {
requestCount: requests.length,
...(requests.length > 1 ? { additionalRequests: requests.slice(1) } : {}),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: the completion transport returns actual model metadata and tool-call traces, but capture() retains only its content, and this metadata contains only request counts/additional requests. Even when the judge executes tools, fresh output rows therefore lack the evidence required by the blocking tool_call_accuracy evaluator. Could we retain completion metadata and associate traces with their requests for independent judging?

"features",
"featureIds",
"selectedFeatureIds",
"suggestedFeatures",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: response rendering picks the first nonempty field, so normal feature-extraction output is rendered from results and omits suggestedFeatures. The novelty grader receives that rendered response, and changing only the generated suggestions leaves its mapped inputs identical. Could the novelty grader receive an explicit suggestions projection, or could the response retain both detection results and suggestions?

affected = [p["key"] for p in plans if p["action"] == "rerun-required"]
selection = set(config.regrade)
if selection - set(affected):
raise ValueError(f"--regrade contains unaffected or unknown graders: {sorted(selection - set(affected))}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: replay eligibility checks configuration/input differences and missing files, but not whether the retained native output has usable assessments. If a failed evaluator leaves input-only native rows with unchanged inputs/configuration, this marks it for reparsing, and an explicit --regrade is rejected as unaffected. Could missing or invalid assessments make a grader eligible for explicitly selected regrading, without automatically expanding paid execution?

if path.name == "manifest.json":
return _read_manifest_cases(path)
return read_jsonl(path)
dataset_root = path if path.is_dir() else path.parent

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: a nonexistent explicit dataset filename falls back to its directory's manifest. For example, datasets/misspelled-custom-cases.jsonl loads all 222 committed cases rather than failing, potentially expanding paid generation/grading beyond the requested subset. Could we limit this fallback to the documented legacy default filename and reject other nonexistent explicit files?

deterministic:
- check: generation_success
- check: output_schema
requiredAny: [output.results]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: without allowEmpty, output_schema treats results: [] as absent. Both committed empty-catalog cases fail this check even for their reviewed expected output, {"results":[],"suggestedFeatures":[]}. Could this schema permit an empty results list, leaving the separate feature-coverage check to enforce completeness for nonempty catalogs?

}
total_items = result["totalItems"]
result["attackSuccessRate"] = (
result["successfulAttacks"] / total_items if total_items else 0.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: per-surface attack-success rates use completed items, but the overall rate divides by all items, including infrastructure errors. One completed successful attack plus one errored item is reported as 100% at the surface and 50% overall. Could the overall denominator sum completed items too, keeping errors separate so they don't appear to be successfully defended attacks?

request = {
...result.requests[0],
metadata: {
...result.requests[0].metadata,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: independent judging creates separate requests per criterion, so the first captured request may not contain the attacked criterion. The synthetic FAIL also prevents dependent attacked criteria from being reached. A benign root followed by an attacked child can therefore fail composition with zero attack placeholders. Could we ensure prerequisites allow reaching the selected criterion and select the captured request containing the attack, rather than always using requests[0]?

definition=definition,
metadata=metadata,
description="Temporary Scope static-prompt red-team target",
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking follow-up: cancellation of this asyncio.to_thread await doesn't stop the SDK creation call. Surface cleanup can run before it has a target identity, then the thread finishes creating an orphan despite keep_remote=False. Cancellation also reaches the surface's finally block before summary is initialized, masking cancellation with UnboundLocalError. Could resource creation retain/await its outcome on cancellation so any created identity is cleaned up, and initialize the summary before entering the lifecycle?

This branch has not been deployed

No deployments
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.

2 participants