Skip to content

Commit 147c5b3

Browse files
vchen5938Vincent Chen
andauthored
feat(evaluation): surface batch evaluation insight results (#704)
* feat(evaluation): surface batch evaluation insight results GetBatchEvaluation returns insightResults when a batch evaluation includes an analysis evaluator such as Builtin.FailureAnalysis, but BatchEvaluationRunner built its result from a fixed set of response fields and dropped them. Add models for the insight shapes and an insight_results field on BatchEvaluationResult. An absent field and an empty list mean different things, so the runner keeps them distinct: None when no analysis evaluator ran or its results could not be read, and an empty list when the analysis ran and found no failures. The insight shapes reach the client through the service model that botocore ships, so raise the boto3 and botocore floors to the first release that carries them. * docs(evaluation): name the evaluator that produces insight results The insight results example added two evaluators but said only "add one", leaving it unclear which of them produces the insights. --------- Co-authored-by: Vincent Chen <vinchenx@amazon.com>
1 parent 5d85607 commit 147c5b3

7 files changed

Lines changed: 379 additions & 11 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Evaluation insight results
2+
3+
Some evaluators go beyond scoring and group related failures across the evaluated
4+
sessions. `Builtin.FailureAnalysis` is one. Add it to `evaluator_ids` and the
5+
completed result carries its insights in `insight_results`, alongside the scores
6+
from any other evaluators in the same run:
7+
8+
```python
9+
from bedrock_agentcore.evaluation import (
10+
BatchEvaluationRunConfig,
11+
BatchEvaluationRunner,
12+
BatchEvaluatorConfig,
13+
CloudWatchDataSourceConfig,
14+
FileDatasetProvider,
15+
)
16+
17+
config = BatchEvaluationRunConfig(
18+
batch_evaluation_name="my-failure-analysis",
19+
evaluator_config=BatchEvaluatorConfig(
20+
evaluator_ids=["Builtin.FailureAnalysis", "Builtin.GoalSuccessRate"],
21+
),
22+
data_source=CloudWatchDataSourceConfig(
23+
service_names=["MyAgent.DEFAULT"],
24+
log_group_names=["/aws/bedrock-agentcore/runtimes/MyAgent-abc123-DEFAULT"],
25+
),
26+
)
27+
28+
runner = BatchEvaluationRunner(region="us-west-2")
29+
result = runner.run_dataset_evaluation(
30+
agent_invoker=agent_invoker,
31+
dataset=FileDatasetProvider("dataset.json").get_dataset(),
32+
config=config,
33+
)
34+
35+
for insight in result.insight_results or []:
36+
print(f"{insight.name} ({insight.affected_session_count} sessions)")
37+
for cause in insight.root_causes or []:
38+
print(f" {cause.name}: {cause.suggested_fix}")
39+
```
40+
41+
`run_dataset_evaluation` polls until the job reaches a terminal state, so the
42+
insights are already on the result it returns.
43+
44+
An empty list and `None` mean different things. An empty list means the analysis
45+
ran and found no failures, while `None` means no analysis evaluator was requested
46+
or its results could not be read, in which case `error_details` names the
47+
evaluator. Branch on the two separately rather than treating both as "no
48+
insights":
49+
50+
```python
51+
if result.insight_results is None:
52+
print("no analysis ran, or its results were unavailable:", result.error_details)
53+
elif not result.insight_results:
54+
print("analysis ran and found no failures")
55+
```
56+
57+
The models take the response as it arrives on the wire and expose it as
58+
snake_case attributes. Fields the response omits come back as `None`:
59+
60+
```python
61+
from bedrock_agentcore.evaluation import InsightResult
62+
63+
insight = InsightResult.model_validate(
64+
{
65+
"type": "FAILURE",
66+
"name": "Tool calls fail on missing permissions",
67+
"description": "Tool calls that change service state fail for lack of permissions.",
68+
"createdAt": "2026-10-15T21:21:11Z",
69+
"affectedSessionCount": 2,
70+
"rootCauses": [
71+
{
72+
"name": "Execution role lacks the actions the tools call",
73+
"explanation": "The role is missing the actions the restart tool calls.",
74+
"suggestedFix": "Grant the role those actions.",
75+
"sampleSessions": [
76+
{
77+
"sessionId": "session-0001",
78+
"findings": [
79+
{
80+
"description": "restart_service failed with PermissionError.",
81+
"targetId": "1a027c6a3f598862",
82+
"targetType": "SPAN_ID",
83+
}
84+
],
85+
}
86+
],
87+
}
88+
],
89+
}
90+
)
91+
92+
assert insight.affected_session_count == 2
93+
assert insight.created_at.year == 2026
94+
95+
finding = insight.root_causes[0].sample_sessions[0].findings[0]
96+
assert finding.target_type == "SPAN_ID"
97+
assert finding.confidence is None
98+
assert insight.root_causes[0].sample_sessions[0].last_seen_at is None
99+
```
100+
101+
Sample sessions normally sit on each root cause. An insight carries its own
102+
`sample_sessions` when it has no root causes.

‎pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ classifiers = [
2626
"Topic :: Software Development :: Libraries :: Python Modules",
2727
]
2828
dependencies = [
29-
"boto3>=1.43.103",
30-
"botocore>=1.43.103",
29+
"boto3>=1.43.115",
30+
"botocore>=1.43.115",
3131
"pydantic>=2.0.0,<2.41.3",
3232
"urllib3>=1.26.0",
3333
"starlette>=0.46.2",

‎src/bedrock_agentcore/evaluation/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
EvaluatorStatistics,
2020
EvaluatorSummary,
2121
FailedScenario,
22+
InsightResult,
23+
InsightRootCause,
2224
OnlineEvaluationDataSourceConfig,
25+
SampleSession,
26+
SessionFinding,
2327
)
2428
from bedrock_agentcore.evaluation.runner.batch.batch_evaluation_runner import (
2529
BatchEvaluationRunner,
@@ -81,6 +85,10 @@
8185
"EvaluatorStatistics",
8286
"EvaluatorSummary",
8387
"FailedScenario",
88+
"InsightResult",
89+
"InsightRootCause",
90+
"SampleSession",
91+
"SessionFinding",
8492
"AgentInvokerInput",
8593
"AgentInvokerOutput",
8694
"CloudWatchAgentSpanCollector",

‎src/bedrock_agentcore/evaluation/runner/batch/batch_evaluation_models.py‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,103 @@ class CloudWatchOutputDataConfig(BaseModel):
245245
log_stream_name: str
246246

247247

248+
class SessionFinding(BaseModel):
249+
"""A failure found in a sampled session that placed it under an insight or root cause.
250+
251+
.. warning::
252+
This feature is in preview and may change in future releases.
253+
254+
Attributes:
255+
description: Evidence text describing the failure.
256+
target_id: Identifier of the span, trace, or span name the finding points at,
257+
interpreted according to ``target_type``.
258+
target_type: What ``target_id`` refers to, such as ``"SPAN_ID"``.
259+
category: Failure category assigned by the analysis.
260+
confidence: Confidence in the finding, between 0 and 1.
261+
"""
262+
263+
model_config = ConfigDict(alias_generator=alias_generators.to_camel, populate_by_name=True)
264+
265+
description: Optional[str] = None
266+
target_id: Optional[str] = None
267+
target_type: Optional[str] = None
268+
category: Optional[str] = None
269+
confidence: Optional[float] = None
270+
271+
272+
class SampleSession(BaseModel):
273+
"""A sampled session illustrating an insight or root cause.
274+
275+
.. warning::
276+
This feature is in preview and may change in future releases.
277+
278+
Attributes:
279+
session_id: Identifier of the sampled session.
280+
last_seen_at: Latest timestamp observed for the session in the batch input.
281+
findings: Failures that placed this session under the insight or root cause.
282+
"""
283+
284+
model_config = ConfigDict(alias_generator=alias_generators.to_camel, populate_by_name=True)
285+
286+
session_id: Optional[str] = None
287+
last_seen_at: Optional[datetime] = None
288+
findings: Optional[List[SessionFinding]] = None
289+
290+
291+
class InsightRootCause(BaseModel):
292+
"""A root cause grouped under an insight.
293+
294+
.. warning::
295+
This feature is in preview and may change in future releases.
296+
297+
Attributes:
298+
name: Short label for the root cause.
299+
explanation: Why the grouped failures happen.
300+
suggested_fix: Recommended change to address the root cause.
301+
affected_session_count: Number of distinct sessions affected. A session can
302+
appear under more than one root cause, so these counts can add up to more
303+
than the insight's own count.
304+
sample_sessions: Sessions illustrating this root cause.
305+
"""
306+
307+
model_config = ConfigDict(alias_generator=alias_generators.to_camel, populate_by_name=True)
308+
309+
name: Optional[str] = None
310+
explanation: Optional[str] = None
311+
suggested_fix: Optional[str] = None
312+
affected_session_count: Optional[int] = None
313+
sample_sessions: Optional[List[SampleSession]] = None
314+
315+
316+
class InsightResult(BaseModel):
317+
"""A job-level insight grouping related failures across the batch.
318+
319+
.. warning::
320+
This feature is in preview and may change in future releases.
321+
322+
Attributes:
323+
type: Kind of insight, such as ``"FAILURE"``.
324+
name: Short label for the insight.
325+
description: What the grouped failures have in common.
326+
created_at: When the insight was generated.
327+
affected_session_count: Number of distinct sessions affected.
328+
sample_sessions: Sessions illustrating the insight. Populated only when the
329+
insight has no root causes, since otherwise the samples sit under each
330+
root cause.
331+
root_causes: Root causes grouped under this insight.
332+
"""
333+
334+
model_config = ConfigDict(alias_generator=alias_generators.to_camel, populate_by_name=True)
335+
336+
type: Optional[str] = None
337+
name: Optional[str] = None
338+
description: Optional[str] = None
339+
created_at: Optional[datetime] = None
340+
affected_session_count: Optional[int] = None
341+
sample_sessions: Optional[List[SampleSession]] = None
342+
root_causes: Optional[List[InsightRootCause]] = None
343+
344+
248345
class BatchEvaluationResult(BaseModel):
249346
"""Result returned by :py:meth:`BatchEvaluationRunner.run`.
250347
@@ -263,6 +360,11 @@ class BatchEvaluationResult(BaseModel):
263360
the service. ``None`` if the API did not return it.
264361
evaluation_results: Aggregated per-evaluator statistics. Present when
265362
the job completed successfully; ``None`` otherwise.
363+
insight_results: Job-level insights produced by an analysis evaluator such
364+
as ``Builtin.FailureAnalysis``. An empty list means the analysis ran and
365+
found no failures. ``None`` means no analysis evaluator was requested, or
366+
its results could not be read, in which case ``error_details`` names the
367+
evaluator.
266368
error_details: Service-reported error messages when the job failed.
267369
agent_invocation_failures: Scenarios that failed during the agent
268370
invocation phase (before the evaluation job was started). A
@@ -285,6 +387,7 @@ class BatchEvaluationResult(BaseModel):
285387
created_at: datetime
286388
updated_at: Optional[datetime] = None
287389
evaluation_results: Optional[BatchEvaluationSummary] = None
390+
insight_results: Optional[List[InsightResult]] = None
288391
error_details: Optional[List[str]] = None
289392
agent_invocation_failures: List[FailedScenario] = Field(default_factory=list)
290393
output_data_config: Optional[CloudWatchOutputDataConfig] = None

‎src/bedrock_agentcore/evaluation/runner/batch/batch_evaluation_runner.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
BatchEvaluationSummary,
2121
CloudWatchOutputDataConfig,
2222
FailedScenario,
23+
InsightResult,
2324
)
2425
from bedrock_agentcore.evaluation.runner.dataset_types import Dataset, PredefinedScenario, Scenario, SimulatedScenario
2526
from bedrock_agentcore.evaluation.runner.invoker_types import AgentInvokerFn
@@ -429,6 +430,13 @@ def run_dataset_evaluation(
429430
if "evaluationResults" in response:
430431
evaluation_results = BatchEvaluationSummary.model_validate(response["evaluationResults"])
431432

433+
# Absent and empty mean different things: the field is omitted when no
434+
# analysis evaluator ran or its results were unreadable, and empty when the
435+
# analysis ran and found no failures. Keep that distinction in the result.
436+
insight_results = None
437+
if "insightResults" in response:
438+
insight_results = [InsightResult.model_validate(insight) for insight in response["insightResults"]]
439+
432440
output_data_config = None
433441
if "outputConfig" in response:
434442
odc = response["outputConfig"].get("cloudWatchConfig")
@@ -448,6 +456,7 @@ def run_dataset_evaluation(
448456
description=response.get("description"),
449457
agent_invocation_failures=failed_scenarios,
450458
evaluation_results=evaluation_results,
459+
insight_results=insight_results,
451460
error_details=response.get("errorDetails"),
452461
output_data_config=output_data_config,
453462
kms_key_arn=response.get("kmsKeyArn"),

0 commit comments

Comments
 (0)