Skip to content
23 changes: 20 additions & 3 deletions .claude/skills/run-assert-eval/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,17 @@ runs*, or *watch a live run*.

## Preconditions (check, don't assume)

1. **ASSERT installed**: `assert-ai --help` succeeds. If not, guide install:
1. **ASSERT installed**: `assert-ai --help` succeeds. If not, guide install from
PyPI — not an editable install of the user's own repo:
```
python -m pip install -e ".[otel,langgraph]"
python -m pip install "assert-ai[otel]"
```
Add route-specific extras as needed, for example `assert-ai[otel,langgraph]`
for LangGraph. `target.endpoint` needs `aiohttp`, which ships transitively via
`litellm`'s own dependency — no separate extra to install. Use
`pip install -e ".[otel,langgraph]"` **only** when the working directory is a
clone of the ASSERT repo itself; inside a customer repo it installs the wrong
package.

2. **Provider creds exist** in `.env`. NEVER read or print `.env`. If a run fails
with an auth error, tell the user which variable NAMES are required
Expand Down Expand Up @@ -76,7 +83,9 @@ Help the user set the right target in the config:
- **Hosted model** with a system prompt and optional tools:
use `target.model` and `target.tools`.
- **Pre-collected traces** (no live inference needed):
use `assert-ai judge-traces --traces <path> --config <path>`.
use `assert-ai judge-traces --traces <path> --config <path>`; do not add a `--trace` flag to `assert-ai run`.
- **Black-box HTTP endpoint** you cannot import as Python:
use `target.endpoint` — the runtime POSTs `{"message": ..., "history": [...]}` and reads `{"response": ...}`, so no wrapper code is needed (requires `aiohttp`). Only write a thin `target.callable` shim if the service's request/response shape differs. Either way the judge sees only final text, so this is a fallback, not the recommended path.

### 3. Run the pipeline

Expand Down Expand Up @@ -137,6 +146,14 @@ Suggest it specifically when the user wants to:

See `docs/guides/use-local-viewer.md` for the full layout.

### 6. Hand off to CI

Once the eval is passing locally and the user wants it enforced on PRs, hand off to the `wire-assert-ci` skill. Do not author CI wiring here; direct the user or agent to the action bootstrap:

```
read https://raw.githubusercontent.com/responsibleai/assert-ai-action/main/ONBOARD.md
```

## Output format

Present a short summary with this structure:
Expand Down
16 changes: 15 additions & 1 deletion .cursor/rules/assert.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ stepping with citations highlighted). Answer in chat for "what / why / which"; h
- **Framework agent** (LangGraph, CrewAI, etc.) with a Python entry function: `target.callable`
WITH `target.trace` so the judge can cite tool calls and routing.
- **Hosted model** with a system prompt and optional tools: `target.model` and `target.tools`.
- **Pre-collected traces** (no live inference): `assert-ai judge-traces --traces <path> --config <path>`.
- **Pre-collected traces** (no live inference): `assert-ai judge-traces --traces <path> --config <path>`;
do not add a `--trace` flag to `assert-ai run`.
- **Black-box HTTP endpoint** you cannot import as Python: use `target.endpoint` — the runtime POSTs
`{"message": ..., "history": [...]}` and reads `{"response": ...}`, so no wrapper code is needed
(requires `aiohttp`). Only write a thin `target.callable` shim if the service's request/response
shape differs. Either way the judge sees only final text, so this is a fallback, not the
recommended path.

### 3. Run the pipeline

Expand Down Expand Up @@ -100,6 +106,14 @@ drawer), **compare against a baseline** (viewer compare view, or `assert-ai resu
<runA> <runB>`), or **watch a run in progress** (live run monitor). See
`docs/guides/use-local-viewer.md` for the layout.

### 6. Hand off to CI

Once the eval is passing locally and the user wants it enforced on PRs, hand off to the `wire-assert-ci` skill. Do not author CI wiring here; direct the user or agent to the action bootstrap:

```
read https://raw.githubusercontent.com/responsibleai/assert-ai-action/main/ONBOARD.md
```

### Output format

- **Headline metrics** (per dimension): policy violation rate X% (N/M), overrefusal rate X% (N/M),
Expand Down
16 changes: 13 additions & 3 deletions .github/prompts/run-assert-eval.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ Copilot is for *answering questions* and *synthesis* — direct answers, failure

## Preconditions (check, don't assume)

1. **ASSERT installed**: verify `assert-ai --help` succeeds. If not, guide install:
1. **ASSERT installed**: verify `assert-ai --help` succeeds. If not, guide install from PyPI — not an editable install of the user's own repo:
```
python -m pip install -e ".[otel,langgraph]"
python -m pip install "assert-ai[otel]"
```
Add route-specific extras as needed, for example `assert-ai[otel,langgraph]` for LangGraph. `target.endpoint` needs `aiohttp`, which ships transitively via `litellm`'s own dependency — no separate extra to install. Use `pip install -e ".[otel,langgraph]"` **only** when the working directory is a clone of the ASSERT repo itself; inside a customer repo it installs the wrong package.

2. **Provider creds exist** in `.env`. NEVER read or print `.env`. If a run fails with an auth error, tell the user which variable NAMES are required (AZURE_API_KEY, AZURE_API_BASE, OPENAI_API_KEY, etc.) — never their values.

Expand Down Expand Up @@ -55,7 +56,8 @@ Help the user set the right target in the config:

- **Framework agent** (LangGraph, CrewAI, etc.) with a Python entry function: use `target.callable` WITH `target.trace` so the judge can cite tool calls and routing.
- **Hosted model** with a system prompt and optional tools: use `target.model` and `target.tools`.
- **Pre-collected traces** (no live inference needed): use `assert-ai judge-traces --traces <path> --config <path>`.
- **Pre-collected traces** (no live inference needed): use `assert-ai judge-traces --traces <path> --config <path>`; do not add a `--trace` flag to `assert-ai run`.
- **Black-box HTTP endpoint** you cannot import as Python: use `target.endpoint` — the runtime POSTs `{"message": ..., "history": [...]}` and reads `{"response": ...}`, so no wrapper code is needed (requires `aiohttp`). Only write a thin `target.callable` shim if the service's request/response shape differs. Either way the judge sees only final text, so this is a fallback, not the recommended path.

### 3. Run the pipeline

Expand Down Expand Up @@ -100,6 +102,14 @@ Select the suite and run for forest plots, per-dimension breakdowns, facet group

See `docs/guides/use-local-viewer.md` for the full layout.

### 6. Hand off to CI

Once the eval is passing locally and the user wants it enforced on PRs, hand off to the `wire-assert-ci` skill. Do not author CI wiring here; direct the user or agent to the action bootstrap:

```
read https://raw.githubusercontent.com/responsibleai/assert-ai-action/main/ONBOARD.md
```

## Output format

Present a short summary with this structure:
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ cp .env.example .env # add your provider key
assert-ai run --config examples/travel_planner_langgraph/eval_config.yaml
```

### Add a CI safety gate

Use [`responsibleai/assert-ai-action`](https://github.com/responsibleai/assert-ai-action) to run ASSERT as a PR regression gate — it fails the build when a change makes agent behavior significantly worse.

Install the skills into your coding agent (Cursor, Claude Code, Copilot, and [40+ others](https://github.com/vercel-labs/skills#supported-agents)). Two commands, because the bundle spans two repos on purpose — the evaluation skill is owned here in ASSERT and installed from here, so it never goes stale:

```bash
npx skills add responsibleai/ASSERT --skill run-assert-eval --yes
npx skills add responsibleai/assert-ai-action --skill wire-assert-ci --yes
```

Run them separately. `skills add` takes one package per invocation and silently ignores extras while still exiting 0, so a combined command looks like it worked and leaves you with half the bundle.

Then ask it to wire the gate:

> Use the `wire-assert-ci` skill to add an ASSERT safety gate to this repo.

No Node? Paste this instead — the agent fetches the skills itself:

```text
read https://raw.githubusercontent.com/responsibleai/assert-ai-action/main/ONBOARD.md
```

See [`docs/ci/`](docs/ci/README.md) for the short hand-off.

<table align="center" style="width: 100%; border: 1px solid #d0d7de; border-collapse: collapse;">
<tr>
<th style="border: 1px solid #d0d7de; padding: 10px; text-align: left;">🌐 Project website ↗</th>
Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ Command reference for creating, running, and inspecting evaluations.
- [CLI Overview](cli/overview.md): Learn the core CLI workflow for initializing, running, and comparing evaluations.
- [CLI Commands](cli/commands.md): Browse command syntax, options, and examples for each CLI command group.

## CI

- [CI Safety Gate](ci/README.md): Wire ASSERT into pull requests with `assert-ai-action` to block on evidence of new safety regressions.

## Targets

Choose the right target integration path for your system.
Expand Down
34 changes: 34 additions & 0 deletions docs/ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# CI safety gate

Use [`responsibleai/assert-ai-action`](https://github.com/responsibleai/assert-ai-action) to run ASSERT in pull requests and fail on safety regressions.

## Setup

Install the skills into your coding agent — works with Cursor, Claude Code, Copilot, Gemini CLI, Windsurf, Codex, and [40+ others](https://github.com/vercel-labs/skills#supported-agents):

```bash
npx skills add responsibleai/ASSERT --skill run-assert-eval --yes
npx skills add responsibleai/assert-ai-action --skill wire-assert-ci --yes
```

Two commands, because the bundle spans two repositories on purpose. `wire-assert-ci` wires CI and delegates every live evaluation to `run-assert-eval`, which is owned here in ASSERT. Installing it from here rather than copying it into the action repo means it cannot drift out of sync.

Run them separately: `skills add` accepts one package per invocation and silently ignores extras while still exiting 0, so a combined command looks successful and installs half the bundle.

Then: *"Use the `wire-assert-ci` skill to add an ASSERT safety gate to this repo."*

Without Node, paste the bootstrap URL instead and the agent fetches the skills itself:

```text
read https://raw.githubusercontent.com/responsibleai/assert-ai-action/main/ONBOARD.md
```

## What the agent does

Scans the repo, picks the highest-fidelity way to reach your agent (auto-traced → bring-your-own-trace → callable → HTTP endpoint → prompt-agent), drafts an eval spec from your own README and prompts, asks you to confirm or replace it, splits it **one behavior per YAML**, runs a baseline, and opens the gate PR.

## Notes

Generated workflows call `responsibleai/assert-ai-action@v1`. Keep provider credentials in CI secrets and reference environment variable names only — there is no shared endpoint, so you supply your own model credentials.

The gate blocks on **evidence of harm**: a statistically significant regression fails the build. A missing baseline, a changed test set, or an inconclusive result does not block — see the action's README for the full verdict table and its security model.
67 changes: 67 additions & 0 deletions docs/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ assert-ai [GLOBAL_OPTIONS] COMMAND [ARGS] [OPTIONS]
- `results`: list/status/compare suites and runs
- `analysis`: post-hoc metrics commands
- `judge-traces`: score pre-collected OTel traces
- `acs`: generate, validate, and regression-check ACS policies
- `library`: browse built-in behavior/judge presets

## `init`
Expand Down Expand Up @@ -177,6 +178,72 @@ Optional:
- `--group-by <attribute>` default `session.id`
- `--output <path>`

## `acs generate`

Requires the `acs` extra: `python -m pip install -e ".[acs]"` (editable install of the ASSERT
repo itself — this is not yet published as an installable extra on PyPI's `assert-ai` package).

Generate a deployable ACS policy from an ASSERT run.

```bash
assert-ai acs generate [OPTIONS]
```

Required:

- `--run-dir <path>` or `--suite <suite> --run <run>`

Optional:

- `--out <path>`
- `--min-rate <float>` default `0.0`
- `--min-count <int>` default `1`
- `--model <name>`
- `--lm-kind assert|openai-compatible` default `assert`
- `--strict/--no-strict` default `--no-strict`
- `--validate/--no-validate` default `--validate`
- `--fail-on-allow` optional flag
- `--require-block` optional flag

## `acs validate`

Validate an ACS manifest against an ASSERT run.

```bash
assert-ai acs validate --manifest <path> [OPTIONS]
```

Required:

- `--manifest <path>`
- `--run-dir <path>` or `--suite <suite> --run <run>`

Optional:

- `--min-rate <float>` default `0.0`
- `--min-count <int>` default `1`
- `--max-cases <int>`
- `--fail-on-allow` optional flag
- `--require-block` optional flag

## `acs eval-config`

Generate an ASSERT eval config from an existing ACS manifest.

```bash
assert-ai acs eval-config --manifest <path> --target-callable <module:function> --out <path> [OPTIONS]
```

Required:

- `--manifest <path>`
- `--target-callable <module:function>`
- `--out <path>`

Optional:

- `--model <name>`

## `library list`

List available built-in presets.
Expand Down
43 changes: 41 additions & 2 deletions docs/targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Pick a target based on how your agent is built.
|---|---|---|
| A system prompt + tool schema, no orchestration code yet | **Prompt Agent target** (`target.model`, `target.system_prompt`, `target.tools`): the runtime owns the tool-call loop (up to 10 rounds, real or simulated tools). Best for test-driven prompt + toolset design before any agent is implemented | [Prompt Agent Target (model + tools)](model-and-tools.md) |
| Any agent or multi-agent system you can invoke from Python (LangGraph, CrewAI, OpenAI Agents SDK, DSPy, LlamaIndex, AutoGen / MAF, custom orchestration, and others) | **Callable target with OTel traces (recommended)**: point `target.callable` at your entry function and add `target.trace` so Phoenix/OpenInference (or your own OTel SDK spans) feed tool calls, routing, model calls, and latency to the judge | [Callable Target](callable.md) |
| A black-box API you cannot instrument | **Plain callable (customization fallback, not recommended)**: `target.callable` with no `target.trace`. The judge sees only the final response; use only when instrumentation is impossible | [Callable Target (without traces)](callable.md#customization-without-traces) |
| Existing OpenTelemetry traces from a prior run | **Judge pre-collected traces**: parse the trace file into an inference set with `assert-ai judge-traces --traces <path> --config <path>`, then score it with `assert-ai run --config <path> --force-stage judge` | [CLI reference](../cli/commands.md#judge-traces) |
| A black-box HTTP service you cannot import as Python | **HTTP endpoint target**: point `target.endpoint` at the service URL. The runtime POSTs to it directly — no wrapper code. Same black-box visibility as a plain callable: the judge sees only the final response | [HTTP endpoint (`target.endpoint`)](callable.md#http-endpoint-targetendpoint) |

**Use simulated tools intentionally:** simulated tools are helpful for Prompt Agents when real backends are not ready. They are not a substitute for tracing a real multi-agent framework.

Expand All @@ -31,6 +32,23 @@ For unsupported frameworks or custom orchestration, emit your own OTel spans wit

After an eval finds policy violations, see [Securing agents with ACS](../guides/securing-agents-with-acs.md) to generate an ACS guard and re-run the same callable target secured.

## Offline path: bring your own OTel traces

If your repo already emits OpenTelemetry spans, you can turn a captured trace file into scored
results without running live inference — in two steps:

```bash
assert-ai judge-traces --traces <path> --config <path>
assert-ai run --config <path> --force-stage judge
```

`judge-traces` parses the OTel spans into an inference set (`inference_set.jsonl`); it does not
call the judge itself. `--force-stage judge` runs the judge stage against that inference set and
produces `scores.jsonl`. This is separate from `assert-ai run`'s normal path: there is no
`--trace` flag on `assert-ai run`. Use `target.callable` + `target.trace` when ASSERT should run
the target and collect traces; use `judge-traces` + `--force-stage judge` when traces already
exist.

## Simple path: Prompt Agent (model + tools)

Use the **Prompt Agent target** (`target.model` + `target.system_prompt` + optional `target.tools`) when you have a system prompt and a tool schema but no orchestration code yet. The runtime owns the tool-call loop. Real Python tools or LLM-simulated tool responses both work. Useful for test-driven prompt + toolset design *before* any agent is implemented.
Expand All @@ -41,13 +59,34 @@ Use the **Prompt Agent target** (`target.model` + `target.system_prompt` + optio

The callable target also accepts a plain Python function with no `target.trace` block. **This is not recommended for real agents** — the judge sees only the final response and misses tool calls, routing, and intermediate decisions. Use it only as a fallback when you cannot instrument the target (for example, evaluating a black-box third-party API), or for pipeline smoke testing.

ASSERT evaluates HTTP services natively — see [HTTP endpoint (`target.endpoint`)](callable.md#http-endpoint-targetendpoint). Reach for a callable shim only when your service's request or response shape differs from the one `target.endpoint` expects (it POSTs `{"message": ..., "history": [...]}` and reads `{"response": ...}`):

```python
import requests


def call_agent(message: str) -> str:
"""Adapter for a service whose contract differs from target.endpoint's."""
response = requests.post(
"https://example.com/agent",
json={"input": message}, # this service wants "input", not "message"
timeout=30,
)
response.raise_for_status()
return response.json()["output"] # ...and returns "output", not "response"
```

Because this path has no trace capture, the judge sees only the returned text. Prefer a traced Python callable whenever you control the agent runtime.

## Target paths at a glance

| Path | Who owns the tool-call loop? | Best for | Config anchor |
|---|---|---|---|
| Callable target with OTel traces (recommended) | You (your callable runs the loop; ASSERT reads the OTel spans) | Any agent or multi-agent system you can invoke from Python | `target.callable` + `target.trace` |
| Pre-collected OTel traces | You (`judge-traces` parses spans into an inference set; `--force-stage judge` scores them) | Repos that already captured spans from a prior run | `assert-ai judge-traces --traces <path> --config <path>` then `assert-ai run --config <path> --force-stage judge` |
| Prompt Agent (model + tools) | ASSERT runtime (declared in YAML; runtime orchestrates up to 10 rounds) | Test-driven prompt + toolset design; agents that haven't been written yet | `target.model`, `target.system_prompt`, `target.tools` |
| Plain callable (customization fallback) | Whoever (ASSERT doesn't see inside) | Black-box APIs you cannot instrument; pipeline smoke tests | `target.callable` (no `target.trace`) |
| HTTP endpoint | The HTTP service (ASSERT doesn't see inside) | A deployed service you cannot import as Python | `target.endpoint` |
| Plain callable (customization fallback) | Whoever (ASSERT doesn't see inside) | Uninstrumentable targets; services whose HTTP contract differs from `target.endpoint`'s; pipeline smoke tests | `target.callable` (no `target.trace`) |

## Current support

Expand Down
Loading