Skip to content

Commit 1a8c22a

Browse files
authored
feat: agent evals — retrieval, scoring, and eval canaries (#91)
1 parent e4e2008 commit 1a8c22a

17 files changed

Lines changed: 3892 additions & 0 deletions

File tree

.env

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# Include additional compose files (comment out to disable)
88
INCLUDE_COMPOSE_EXAMPLES=docker-compose.examples.yml
99
# INCLUDE_COMPOSE_OTEL_DEMO=docker-compose.otel-demo.yml
10+
# INCLUDE_COMPOSE_AGENT_EVAL_LLM=docker-compose.agent-eval-llm.yml
1011
INCLUDE_COMPOSE_LOCAL_OPENSEARCH=docker-compose.local-opensearch.yml
1112
INCLUDE_COMPOSE_LOCAL_OPENSEARCH_DASHBOARDS=docker-compose.local-opensearch-dashboards.yml
1213

@@ -69,6 +70,10 @@ NETWORK_NAME=observability-stack-network
6970
WEATHER_AGENT_PORT=8000
7071
CANARY_INTERVAL=120
7172

73+
# Agent Eval Canary Configuration
74+
EVAL_CANARY_INTERVAL=120
75+
EVAL_CANARY_LOOKBACK_MINUTES=15
76+
7277
# ******************
7378
# OpenTelemetry Demo Configuration
7479
# ******************

docker-compose.agent-eval-llm.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# LLM Eval Canary
2+
# Periodically runs LLM-as-judge (Bedrock Claude) on agent traces.
3+
# Requires AWS credentials via AWS_* env vars.
4+
#
5+
# Enable by uncommenting INCLUDE_COMPOSE_AGENT_EVAL_LLM in .env
6+
7+
x-default-logging: &logging
8+
driver: "json-file"
9+
options:
10+
max-size: "5m"
11+
max-file: "2"
12+
tag: "{{.Name}}"
13+
14+
services:
15+
example-agent-eval-canary-llm:
16+
build:
17+
context: ./docker-compose/agent-eval-canary-llm
18+
dockerfile: Dockerfile
19+
container_name: agent-eval-canary-llm
20+
environment:
21+
- OPENSEARCH_HOST=https://${OPENSEARCH_HOST}:${OPENSEARCH_PORT}
22+
- OPENSEARCH_USER=${OPENSEARCH_USER}
23+
- OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD}
24+
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_COLLECTOR_HOST}:${OTEL_COLLECTOR_PORT_GRPC}
25+
- EVAL_CANARY_LLM_INTERVAL=${EVAL_CANARY_LLM_INTERVAL:-60}
26+
- EVAL_CANARY_LLM_LOOKBACK_MINUTES=${EVAL_CANARY_LLM_LOOKBACK_MINUTES:-10}
27+
- EVAL_CANARY_LLM_MAX_PER_CYCLE=${EVAL_CANARY_LLM_MAX_PER_CYCLE:-20}
28+
- EVAL_CANARY_LLM_CONCURRENCY=${EVAL_CANARY_LLM_CONCURRENCY:-8}
29+
- EVAL_JUDGE_MODEL=${EVAL_JUDGE_MODEL:-us.anthropic.claude-sonnet-4-20250514-v1:0}
30+
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
31+
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
32+
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-}
33+
- AWS_REGION=${AWS_REGION:-us-west-2}
34+
depends_on:
35+
- opensearch
36+
- otel-collector
37+
networks:
38+
- observability-stack-network
39+
restart: unless-stopped
40+
deploy:
41+
resources:
42+
limits:
43+
memory: 512M
44+
logging: *logging

docker-compose.examples.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,28 @@ services:
136136
limits:
137137
memory: 128M
138138
logging: *logging
139+
140+
# Agent Eval Canary: periodically scores un-evaluated agent traces
141+
example-agent-eval-canary:
142+
build:
143+
context: ./docker-compose/agent-eval-canary
144+
dockerfile: Dockerfile
145+
container_name: agent-eval-canary
146+
environment:
147+
- OPENSEARCH_HOST=https://${OPENSEARCH_HOST}:${OPENSEARCH_PORT}
148+
- OPENSEARCH_USER=${OPENSEARCH_USER}
149+
- OPENSEARCH_PASSWORD=${OPENSEARCH_PASSWORD}
150+
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_COLLECTOR_HOST}:${OTEL_COLLECTOR_PORT_GRPC}
151+
- EVAL_CANARY_INTERVAL=${EVAL_CANARY_INTERVAL}
152+
- EVAL_CANARY_LOOKBACK_MINUTES=${EVAL_CANARY_LOOKBACK_MINUTES}
153+
depends_on:
154+
- opensearch
155+
- otel-collector
156+
networks:
157+
- observability-stack-network
158+
restart: unless-stopped
159+
deploy:
160+
resources:
161+
limits:
162+
memory: ${CANARY_MEMORY_LIMIT}
163+
logging: *logging

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ name: observability-stack
66
include:
77
- path: ${INCLUDE_COMPOSE_EXAMPLES:-docker-compose/util/docker-compose.empty.yml}
88
- path: ${INCLUDE_COMPOSE_OTEL_DEMO:-docker-compose/util/docker-compose.empty.yml}
9+
- path: ${INCLUDE_COMPOSE_AGENT_EVAL_LLM:-docker-compose/util/docker-compose.empty.yml}
910
- path: ${INCLUDE_COMPOSE_LOCAL_OPENSEARCH:-docker-compose/util/docker-compose.empty.yml}
1011
- path: ${INCLUDE_COMPOSE_LOCAL_OPENSEARCH_DASHBOARDS:-docker-compose/util/docker-compose.empty.yml}
1112

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM python:3.12-slim
2+
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
3+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
4+
WORKDIR /app
5+
COPY pyproject.toml ./
6+
RUN uv sync --no-dev
7+
COPY eval_canary_llm.py .
8+
CMD ["uv", "run", "eval_canary_llm.py"]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# LLM Eval Canary (LLM-as-Judge)
2+
3+
Periodically evaluates agent traces using HelpfulnessEvaluator (Bedrock Claude). Opt-in — requires AWS credentials.
4+
5+
Enable by uncommenting `INCLUDE_COMPOSE_AGENT_EVAL_LLM` in `.env`.
6+
7+
## Evaluation Signals
8+
9+
Emits **1 evaluation span** per trace:
10+
11+
| Eval Name | What it measures | Value |
12+
|---|---|---|
13+
| `helpfulness` | LLM judges whether the agent's response addressed the user's request | 0.0–1.0 (7-point scale) |
14+
15+
## Configuration
16+
17+
| Env Var | Default | Description |
18+
|---|---|---|
19+
| `EVAL_CANARY_LLM_INTERVAL` | `60` | Seconds between poll cycles |
20+
| `EVAL_CANARY_LLM_LOOKBACK_MINUTES` | `10` | How far back to search for traces |
21+
| `EVAL_CANARY_LLM_MAX_PER_CYCLE` | `20` | Max traces to evaluate per cycle |
22+
| `EVAL_CANARY_LLM_CONCURRENCY` | `8` | Parallel Bedrock calls |
23+
| `EVAL_JUDGE_MODEL` | `us.anthropic.claude-sonnet-4-20250514-v1:0` | Bedrock model ID |
24+
25+
AWS credentials must be available in the environment (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`).
26+
27+
## Running with the Deterministic Canary
28+
29+
Both canaries can run simultaneously without duplicating scores. Each deduplicates by its own evaluation name — this canary checks for existing `helpfulness` scores, the deterministic canary checks for `span_coverage`. Neither blocks the other.
30+
31+
A trace scored by both gets **5 evaluation spans**:
32+
33+
```
34+
invoke_agent Events Agent
35+
├── evaluation span_coverage (deterministic)
36+
├── evaluation error_free (deterministic)
37+
├── evaluation tool_call_success_rate (deterministic)
38+
├── evaluation tool_diversity (deterministic)
39+
└── evaluation helpfulness (LLM-as-judge)
40+
```
41+
42+
## OTel Compliance
43+
44+
All evaluation spans follow [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/):
45+
46+
- `gen_ai.operation.name = "evaluation"`
47+
- `gen_ai.evaluation.name = "helpfulness"`
48+
- `gen_ai.evaluation.score.value` — numeric score
49+
- `gen_ai.evaluation.score.label` — human-readable label (e.g. "Very helpful", "Somewhat unhelpful")
50+
- `gen_ai.evaluation.explanation` — LLM's reasoning
51+
- `gen_ai.evaluation.result` event emitted per span
52+
53+
Score spans attach as children of the evaluated agent's `invoke_agent` span, so they appear in the same trace waterfall in OpenSearch Dashboards.

0 commit comments

Comments
 (0)