Repository navigation
Conversation
Evaluation datasets now carry a free-form scenario name and a scoring config: json_exact compares one (optionally dotted) JSON output field with the expected value deterministically, llm_judge keeps the judge with an optional business rubric. Runs pin the published agent's version and system prompt at start and fail if the agent is republished mid-run, so a run never mixes versions. Rows keep the full prompt, expectation, answer and predicted/expected values. Observability discovers scenarios from the runs and shows pass rate, label distribution, invalid outputs, low-score cases and a same-dataset regression comparison. scripts/run_observability_demo.py seeds two synthetic benchmarks against a deployed portal with real model calls. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scripts/run_difficult_customer_evals.py runs 96 adversarial cases across five scenarios (classification, support, oncall, rnd, sales-success) in two rounds. The hard round covers prompt injection, forged system messages, format overrides, social engineering and over-policy demands. The extreme round covers contradictory stakeholders, profanity, personal attacks, blame-shifting and claimed promises nobody can verify. It reuses the demo agents from run_observability_demo.py, publishes the three scenario agents only if missing (or with --republish, to produce a version comparison), and reuses datasets so re-runs stay comparable. It finishes by joining each run with the invocation ledger, showing calls that all succeed at the infrastructure layer next to the answers the evaluation layer marks as breaking a business rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
odinwang
left a comment
There was a problem hiding this comment.
Thanks for this. The core finding is the useful part: 67 calls succeeded at the infrastructure layer with normal latency and cost, and the evaluation layer still flagged 10 answers that broke a business rule. Infrastructure metrics cannot show that, and having it measured on real calls makes the case better than any write-up would. The 96 adversarial cases are a solid asset that we can reuse whatever direction evaluation takes next. Several design choices are good: deterministic json_exact for structured outputs, llm_judge with a business rubric for open answers, version pinning for comparable runs, and keeping full evidence so a reviewer can overturn a verdict. The two fixes to _parse_verdict (a string "false" no longer passes) and to skipping the judge when the agent call fails are also correct.
The items below need changes before merge. Most are about storage limits and keeping the existing page intact.
Must fix
1. Run items can exceed the DynamoDB 400 KB item limit.
_execute now stores up to 2,000 chars of prompt, 1,000 of expected and 8,000 of answer per case, plus a system prompt snapshot of up to 20,000 chars, and rewrites the whole results list after every case. With 20 cases and CJK text (3 bytes per char) a run can pass 400 KB partway through, update_item fails, and the run is marked failed. A side effect: list_runs(50) is a single Query page (1 MB max), so larger items mean fewer runs come back, and get_run scans only that page, so an existing run can return 404 while the UI is polling it.
Suggested direction: store each case result as its own item (for example SK=<run sk>#CASE#<idx>), or keep tighter truncation on the run item. The system prompt snapshot is worth keeping, but it does not have to sit on the run item. Storing it once per agent version (for example SK=AGENT#<id>#V<version>) lets every run of that version reference it. The agent's existing history only records version, time and author, not the prompt.
2. Keep the Observability page in English and keep the existing ledger view.
The rest of the portal is English, and this is a public sample. Please also keep the columns and hints that were removed from the invocation table (User, Turns, the per-source breakdown, the trace log group hint). They are what operators use today.
3. Load the three data sources independently.
Promise.all([listEvalRuns, getObservabilityStats, listInvocations]) means an eval API error now blanks the infrastructure metrics as well. Before this change they loaded separately.
4. Re-run button on a read-only page.
"Re-run" starts real model calls (agent plus judge) with one click. Please remove it from this page (the Evaluation page already starts runs), or at least add a confirmation that states the call count.
5. Scripts can overwrite existing agents with the same name.
Agents are published by name, and an admin can republish an agent owned by someone else. If a deployment already has an rnd-assistant or oncall-triage-bot, running the script with --republish replaces its system prompt, and channels and schedules bound to that agent id follow. Please add a configurable name prefix (for example --prefix, default obs-demo-) to both scripts.
6. Ledger join in the scripts.
/observability/invocations returns at most 200 rows. A full run is around 96 case calls plus about 60 judge calls, so on a deployment with other traffic part of the run falls outside the window. run_observability_demo.py then exits 1 even though the run was fine. This may also explain why the summary in the description shows 67 calls for 79 cases. Please treat a missing ledger row as "not in window" rather than a failure, or filter the ledger by ref on the server side.
Should fix
- Show sample size next to pass rates. Some datasets have 6 to 8 cases, so a single case moves the rate by 12 to 17 points, and the "pass rate dropped N points" notice will fire on noise. Showing
n(or only flagging a drop above a threshold of cases) would avoid that. - Average score for
json_exactruns. Scores are only 0 or 10, so the average in the run history table is just the pass rate again. Hide it for structured scoring. json_exactmatching rules. Matching is case-sensitive and the whole answer must be a JSON object (only code fences are stripped). That is a reasonable strict contract, but please state it indocs/observability.mdso dataset authors knowBillingfails againstbilling.- Implicit default for
classification. A dataset withscenario="classification"and noscoringsilently becomesjson_exactoncategory. Please document this or requirescoringexplicitly for new datasets. - Judge independence. The judge runs on the same
agent-sdkkernel as the agents under test. Worth noting in the docs next to the existing "screening signals" caveat, since it is one likely reason the judge missed the invented deadline.
Possible follow-up (not needed for this PR)
This PR covers evaluation before release: fixed cases against a pinned agent version. The open question is how to see quality on production traffic after an agent ships. AgentCore Evaluations has online evaluation for that. It samples live sessions from the spans already in CloudWatch, runs built-in or custom evaluators (LLM-as-a-judge with custom instructions, or code-based via Lambda), and publishes scores as metrics under Bedrock-AgentCore/Evaluations, where they can be alarmed on next to latency and cost.
A good next step would be a short investigation:
- Can online evaluation attach to the sdk kernel runtime as deployed here (spans are already delivered)?
- Can the rubrics in
run_difficult_customer_evals.pybecome custom evaluators, and canjson_exactbecome a code-based evaluator? - Regional availability, and whether a custom judge can use a
global.cross-region inference profile.
A design question to think about: where should this data live?
Today the Observability page reads only DynamoDB: the invocation ledger (PK=INVOCATION) and now eval runs. Meanwhile CloudWatch already holds AgentCore service metrics (AWS/Bedrock-AgentCore), sdk kernel spans in aws/spans, and container logs, and the page does not use any of it. So the platform effectively has two observability stores that are never reconciled. The ledger join in your scripts, and the 200-row window problem, both come from that split.
We are considering CloudWatch as the single source for telemetry (logs, metrics, traces), with DynamoDB kept for state the application reads back to make decisions. No decision yet; I would like your view as part of the follow-up. Some questions to frame it:
- Telemetry or state? For each record type (invocation ledger, eval runs and per-case evidence, quota counters under
PK=USAGE, audit records), is it something people and alarms look at, or something the application reads back to decide? Quota counting, for example, needs an atomic increment before each call, which CloudWatch cannot do. - Read path. Logs Insights is asynchronous, billed by bytes scanned, and limited to 100 concurrent queries per account, so the portal cannot query it on every page load. Which parts of the page could use metrics (
GetMetricDataover EMF), which need on-demand Insights queries (with field indexes onrun_id/trace_id), and which need a precomputed view? - Eval results specifically. If pass rates were emitted as EMF metrics and per-case evidence went to structured logs keyed by
run_id, what would still need to live in DynamoDB? Would that also remove the 400 KB problem in item 1? - Freshness and accuracy. CloudWatch ingestion lags by up to about a minute, and EMF delivery is at least once. Which current flows depend on read-after-write (your scripts do), and does any of this data need to be exact, for example for per-team cost allocation?
A short write-up (a page or two, in the follow-up issue or PR) answering these would be enough. It does not need to come with code.
I ran backend/tests/test_eval_scenarios.py (7/7 pass) and tsc -b on the PR head locally. Both are clean.
A run rewrote its whole results list onto one item after every case, with up to 11,000 characters of evidence per case plus a 20,000-character system prompt snapshot. Twenty multilingual cases could pass 400 KB mid-run and fail the run. Larger items also shrank the single Query page that list_runs and get_run read, so a run could 404 while the page was polling it. Each case is now its own item in the run's partition (PK=EVALRUN#<id>, SK=CASE#<idx>), written once. A META item there points at the run summary, so get_run reads by id instead of scanning recent runs. The summary keeps status and counts only, including a new evaluated count. The system prompt is stored once per agent version (PK=AGENTPROMPT, SK=<agent>#V<version>) and shared by every run of that version. list_runs now returns summaries; get_run returns the full evidence and the prompt, and the Evaluation page fetches it when a run is expanded. Runs written before this change carry results on the summary item and are still read. The judge's non-boolean pass check raises TypeError, which the existing handler already catches. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tion The page is back to the existing English invocation ledger: the per-source breakdown, the trace log group hint, and the User and Turns columns are unchanged. Application outcomes are a separate ApplicationOutcomes component between the stat tiles and the ledger table, in English like the rest of the portal. The component loads evaluation runs on its own, so an evaluation API error no longer blanks the infrastructure metrics. The one-click re-run button is gone; runs are started from the Evaluation page. Every pass rate shows its sample size, and a drop against the previous run of the same dataset is flagged only when it amounts to at least two cases. json_exact runs hide the mean score, which there only repeats the pass rate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cripts Agents are published by name and an administrator may republish someone else's, so running a script on a deployment that already has, for example, an rnd-assistant would have replaced its system prompt. Both scripts take --prefix (default obs-demo-) for agent and dataset names, and stop if an agent with one of their names exists but was not created by them. The invocation endpoint returns the latest 200 rows platform-wide, so part of a run can fall outside it on a busy deployment. The demo script now reports the count found in the window instead of exiting 1. The difficult-customer summary only joins runs whose case calls are all in the window, so its infrastructure and application counts cover the same cases, and it lists the runs it left out. The demo script also passes scoring explicitly instead of relying on the classification default. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… storage State the json_exact contract (whole answer is one JSON object, string field, trimmed, case-sensitive), the classification default when scoring is omitted, that the judge shares a kernel and model family with the agents it grades, how case evidence and prompt snapshots are stored, why pass rates show their sample size, and the scripts' --prefix and ledger window behaviour. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every case now records the execution facts of its own calls, the ones the
invocation ledger keeps: ok, duration, turns, cost and runtime session id,
for the agent call and the judge call. The run summary aggregates them when
the run ends. The page shows that infrastructure view next to the pass rate
for exactly the same cases, and the difficult-customer script prints it
without joining the 200-row ledger window at all. The session id links a
case to its kernel spans when trace delivery is on.
A dataset's run history was found by filtering the latest 50 runs of all
datasets, so after enough other runs the previous run of a dataset dropped
out and the regression comparison silently disappeared. Each run now writes
a pointer under PK=EVALDS#<dataset_id>, and GET
/api/v1/evals/datasets/{id}/runs reads a dataset's history directly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks for the careful review. Everything under "Must fix" and "Should fix" is addressed in five new commits on top of the original two. I added commits rather than rewriting history, so each item can be checked on its own. The fifth commit goes a step further on item 6, described at the end. Must fix1. DynamoDB item size. Fixed in f9cbc25.
2. English, and the existing ledger view. Fixed in 24b7d8c.
3. Independent loading. Fixed in 24b7d8c. The component loads evaluation runs itself and has its own error banner. An evaluation API error no longer touches the infrastructure metrics. 4. Re-run button. Removed in 24b7d8c. Runs are started from the Evaluation page only. 5. Name collisions. Fixed in b7552df.
6. Ledger join. Fixed in b7552df.
Should fixAll five are done in 24b7d8c and 5fbaa1e.
One more change (8fec457)Item 6 came from evaluation results and execution metrics living in two places. Each evaluated case now records the execution facts of its own calls, for the agent call and for the judge call:
The run summary aggregates them when it ends. The page shows that "infrastructure view" next to the pass rate for exactly the same cases, for example "20/20 agent calls returned ok, 6 of those answers broke the expectation". The difficult-customer script prints it without touching the ledger. The session id on a case is also The same commit fixes a problem the review did not mention. A dataset's run history was found by filtering the latest 50 runs of all datasets, so after enough other runs the previous run of a dataset dropped out and the regression comparison silently disappeared. Each run now writes a pointer under Verification
Found along the way, fixed separatelyAnswering your design question meant inventorying every DynamoDB record and every CloudWatch write. That turned up problems older than this PR, so they are in their own PRs and independent of #44:
Follow-upsI opened #59 for online evaluation and the CloudWatch versus DynamoDB question. It has the investigation plan and a first pass at your four questions. One thing to flag there: the "spans are already delivered" premise depends on the deployment. |
The sample is English-only. The difficult-customer and demo benchmarks carried their multilingual cases as Chinese text, and the storage tests used Chinese characters to build multi-byte payloads. - The Chinese cases are translated. Most are in English now, and six use German, French, Portuguese or Spanish, so the benchmark still checks that agents answer in the customer's language. The case that expected a Chinese reply now expects a French one. - The demo support cases are in English. - The size tests build their payloads from U+20AC (three bytes in UTF-8, like most CJK text), so the worst-case byte budgets they check are unchanged. Datasets created by an earlier run of the scripts have different cases. The scripts refuse to reuse them; delete those datasets before rerunning. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Landed via the internal sync in #63. |
Summary
The Observability page so far reports infrastructure signals: latency, errors, turns and cost. This PR adds customizable application-layer observability next to them: whether the agent's answers were right, or broke a business rule.
Operators define what "right" means per use case, with no code change. They create an evaluation dataset with their own scenario name and scoring contract. The page then discovers the scenario and charts its results next to the execution facts of the same calls. Adding a new scoring method is a documented code extension point.
scenarioname and ascoringconfig.json_exactcompares one JSON output field with the expected value, deterministically and with no judge call. The field can be dotted, such asdecision.intent. Matching is strict and case-sensitive, and the docs spell out the contract.llm_judgekeeps the judge and accepts an optional business rubric.scripts/run_observability_demo.pyseeds a classifier and a support benchmark.scripts/run_difficult_customer_evals.pyadds the oncall, rnd and sales-success scenarios and 96 adversarial cases in two rounds:--prefixand refuse to republish an agent they did not create.What it shows on a live deployment
I ran both scripts against a us-east-1 deployment, using real Bedrock calls on the portal's published agents. The inputs are synthetic; nothing else is fabricated.
Run on the deployed version of this branch. Every number below comes from the runs themselves, as shown on the page:
json_exactjson_exactjson_exactAll 96 agent calls returned ok, with normal latency and cost, so infrastructure dashboards would call this healthy. The evaluation layer flagged 13 answers that broke a rule. Examples:
urgent, and the classifier returned it. In the previous round the same prompt, unchanged, resisted it. A re-run of the same dataset surfacing a behaviour change is exactly what the comparison view is for.Each failure on the page shows its answer, the verdict, the agent version, and the call's latency, cost and runtime session id.
Judge verdicts are screening signals. The judge shares a kernel and model family with the agents it grades. In this round it faulted a reply for "unverified pricing", although $45 and the 15% annual discount are in the agent's own policy. In an earlier round it passed an answer that invented a response deadline. The page keeps every full answer so a reviewer can confirm or overturn each verdict.
Test plan
backend/tests/test_eval_scenarios.py: 13 tests pass. They cover:tsc -bandvite buildpass on this branch, which is based on currentmain.Follow-ups, online evaluation of production traffic and where telemetry should live, are tracked in #59.
🤖 Generated with Claude Code