traceeval-cli 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/PKG-INFO +45 -5
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/README.md +42 -4
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/pyproject.toml +4 -2
- traceeval_cli-0.2.0/scripts/generate_otel_fixtures.py +119 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/cli.py +30 -13
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/core/config.py +28 -19
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/core/schema.py +11 -4
- traceeval_cli-0.2.0/src/traceeval/loaders/otel.py +215 -0
- traceeval_cli-0.2.0/src/traceeval/loaders/otel_attributes.py +20 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/metrics/trajectory_judge.py +120 -51
- traceeval_cli-0.2.0/src/traceeval/pricing.py +62 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/reporting/console.py +6 -0
- traceeval_cli-0.2.0/tests/fixtures/otel/refund_happy.json +344 -0
- traceeval_cli-0.2.0/tests/fixtures/otel/refund_no_args.json +320 -0
- traceeval_cli-0.2.0/tests/test_cli.py +115 -0
- traceeval_cli-0.2.0/tests/test_config.py +50 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/tests/test_loaders.py +27 -1
- traceeval_cli-0.2.0/tests/test_otel_loader.py +186 -0
- traceeval_cli-0.2.0/tests/test_pricing.py +53 -0
- traceeval_cli-0.2.0/tests/test_trajectory_judge.py +490 -0
- traceeval_cli-0.2.0/uv.lock +675 -0
- traceeval_cli-0.1.0/tests/test_trajectory_judge.py +0 -265
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/.github/workflows/ci.yml +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/.github/workflows/publish.yml +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/.gitignore +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/LICENSE +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/examples/reference_agent.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/sample_data/case_01.json +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/sample_data/trace_01.json +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/scripts/batch_runner.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/scripts/generate_dataset.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/__init__.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/core/__init__.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/core/logger.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/loaders/__init__.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/loaders/file.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/loaders/live.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/metrics/__init__.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/reporting/__init__.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/src/traceeval/reporting/export.py +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/test_suite/golden_001_happy.json +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/test_suite/golden_002_dow.json +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/test_suite/golden_003_bypass.json +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/test_suite/golden_004_semantic.json +0 -0
- {traceeval_cli-0.1.0 → traceeval_cli-0.2.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: traceeval-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CI/CD and Evaluation Infrastructure for Autonomous Agents (Agentic Engineering)
|
|
5
5
|
Project-URL: Repository, https://github.com/tej007-awesome/TraceEval
|
|
6
6
|
Project-URL: Issues, https://github.com/tej007-awesome/TraceEval/issues
|
|
@@ -19,6 +19,8 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
19
19
|
Requires-Dist: rich>=13.7.0
|
|
20
20
|
Requires-Dist: typer>=0.9.0
|
|
21
21
|
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-common>=1.20.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'dev'
|
|
22
24
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
23
25
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
24
26
|
Requires-Dist: ruff==0.11.2; extra == 'dev'
|
|
@@ -96,6 +98,11 @@ Perfect for pre-deployment CI/CD gating. Dynamically spawns your agent, captures
|
|
|
96
98
|
```bash
|
|
97
99
|
traceeval run --case sample_data/case_01.json --pipeline examples.reference_agent:process_refund_success --export report.json
|
|
98
100
|
```
|
|
101
|
+
**Mode C: Evaluate an OpenTelemetry (OTel) Trace**
|
|
102
|
+
Audit OpenTelemetry traces adhering to GenAI semantic conventions.
|
|
103
|
+
```bash
|
|
104
|
+
traceeval run --case sample_data/case_01.json --otel-trace tests/fixtures/otel/refund_happy.json
|
|
105
|
+
```
|
|
99
106
|
|
|
100
107
|
*(Tip: Add `--verbose` right after `traceeval` to view detailed middleware logs!)*
|
|
101
108
|
|
|
@@ -140,13 +147,46 @@ TraceEval decouples the **Ingestion Layer** from the **Evaluation Engine** using
|
|
|
140
147
|
|
|
141
148
|
---
|
|
142
149
|
|
|
150
|
+
## Evaluating OpenTelemetry Traces
|
|
151
|
+
|
|
152
|
+
TraceEval natively ingests OpenTelemetry (OTel) JSON traces adhering to the **OTel GenAI Semantic Conventions**.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
traceeval run --case sample_data/case_01.json --otel-trace tests/fixtures/otel/refund_happy.json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Attributes Ingested
|
|
159
|
+
TraceEval inspects the following GenAI span attributes:
|
|
160
|
+
- `gen_ai.operation.name`: Identifies span types (`invoke_agent`, `chat`, `execute_tool`).
|
|
161
|
+
- `gen_ai.agent.name` & `gen_ai.conversation.id`: Extracts agent skills and session metadata.
|
|
162
|
+
- `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`: Tracks per-model token consumption.
|
|
163
|
+
- `gen_ai.tool.name` & `gen_ai.tool.call.arguments`: Maps tool trajectories and parameters.
|
|
164
|
+
- `gen_ai.output.messages`: Captures the agent's final text response.
|
|
165
|
+
|
|
166
|
+
### Cost Computation & Custom Pricing
|
|
167
|
+
Session token costs are automatically computed by matching `gen_ai.request.model` and token counts against model pricing rates. If pricing is missing for an unknown model, the cost check fails (`"cost could not be verified"`) to prevent unmonitored financial risk.
|
|
168
|
+
|
|
169
|
+
You can supply custom pricing via `--pricing my_pricing.json`:
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"my-custom-model": {
|
|
173
|
+
"input_usd_per_1k": 0.00015,
|
|
174
|
+
"output_usd_per_1k": 0.0006
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> **Privacy Note:** Standard OTel instrumentation often redacts `gen_ai.tool.call.arguments` and `gen_ai.output.messages` to comply with privacy policies. When arguments are omitted, TraceEval outputs non-blocking warnings, and argument matching defaults to `{}`.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
143
183
|
## Roadmap
|
|
144
184
|
|
|
145
|
-
v0
|
|
185
|
+
v0.2 adds OpenTelemetry GenAI trace ingestion with computed cost and specific failure reasons. Planned next:
|
|
146
186
|
|
|
147
|
-
- **
|
|
148
|
-
- **
|
|
149
|
-
- **
|
|
187
|
+
- **Flexible argument matching and forbidden tools:** match tool arguments exactly, partially, or not at all (for privacy-redacted traces), and fail any trajectory that calls a denied tool.
|
|
188
|
+
- **More reliable LLM judge:** per-rubric-item verdicts, multiple samples, retries, and an offline mock judge for CI pipelines that can't call an external LLM.
|
|
189
|
+
- **Meta-evaluation benchmark:** a labelled set of good and bad traces that measures how accurately TraceEval itself catches failures.
|
|
150
190
|
|
|
151
191
|
To track granular progress, see our [GitHub Issues](https://github.com/tej007-awesome/TraceEval/issues).
|
|
152
192
|
|
|
@@ -70,6 +70,11 @@ Perfect for pre-deployment CI/CD gating. Dynamically spawns your agent, captures
|
|
|
70
70
|
```bash
|
|
71
71
|
traceeval run --case sample_data/case_01.json --pipeline examples.reference_agent:process_refund_success --export report.json
|
|
72
72
|
```
|
|
73
|
+
**Mode C: Evaluate an OpenTelemetry (OTel) Trace**
|
|
74
|
+
Audit OpenTelemetry traces adhering to GenAI semantic conventions.
|
|
75
|
+
```bash
|
|
76
|
+
traceeval run --case sample_data/case_01.json --otel-trace tests/fixtures/otel/refund_happy.json
|
|
77
|
+
```
|
|
73
78
|
|
|
74
79
|
*(Tip: Add `--verbose` right after `traceeval` to view detailed middleware logs!)*
|
|
75
80
|
|
|
@@ -114,13 +119,46 @@ TraceEval decouples the **Ingestion Layer** from the **Evaluation Engine** using
|
|
|
114
119
|
|
|
115
120
|
---
|
|
116
121
|
|
|
122
|
+
## Evaluating OpenTelemetry Traces
|
|
123
|
+
|
|
124
|
+
TraceEval natively ingests OpenTelemetry (OTel) JSON traces adhering to the **OTel GenAI Semantic Conventions**.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
traceeval run --case sample_data/case_01.json --otel-trace tests/fixtures/otel/refund_happy.json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Attributes Ingested
|
|
131
|
+
TraceEval inspects the following GenAI span attributes:
|
|
132
|
+
- `gen_ai.operation.name`: Identifies span types (`invoke_agent`, `chat`, `execute_tool`).
|
|
133
|
+
- `gen_ai.agent.name` & `gen_ai.conversation.id`: Extracts agent skills and session metadata.
|
|
134
|
+
- `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`: Tracks per-model token consumption.
|
|
135
|
+
- `gen_ai.tool.name` & `gen_ai.tool.call.arguments`: Maps tool trajectories and parameters.
|
|
136
|
+
- `gen_ai.output.messages`: Captures the agent's final text response.
|
|
137
|
+
|
|
138
|
+
### Cost Computation & Custom Pricing
|
|
139
|
+
Session token costs are automatically computed by matching `gen_ai.request.model` and token counts against model pricing rates. If pricing is missing for an unknown model, the cost check fails (`"cost could not be verified"`) to prevent unmonitored financial risk.
|
|
140
|
+
|
|
141
|
+
You can supply custom pricing via `--pricing my_pricing.json`:
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"my-custom-model": {
|
|
145
|
+
"input_usd_per_1k": 0.00015,
|
|
146
|
+
"output_usd_per_1k": 0.0006
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
> **Privacy Note:** Standard OTel instrumentation often redacts `gen_ai.tool.call.arguments` and `gen_ai.output.messages` to comply with privacy policies. When arguments are omitted, TraceEval outputs non-blocking warnings, and argument matching defaults to `{}`.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
117
155
|
## Roadmap
|
|
118
156
|
|
|
119
|
-
v0
|
|
157
|
+
v0.2 adds OpenTelemetry GenAI trace ingestion with computed cost and specific failure reasons. Planned next:
|
|
120
158
|
|
|
121
|
-
- **
|
|
122
|
-
- **
|
|
123
|
-
- **
|
|
159
|
+
- **Flexible argument matching and forbidden tools:** match tool arguments exactly, partially, or not at all (for privacy-redacted traces), and fail any trajectory that calls a denied tool.
|
|
160
|
+
- **More reliable LLM judge:** per-rubric-item verdicts, multiple samples, retries, and an offline mock judge for CI pipelines that can't call an external LLM.
|
|
161
|
+
- **Meta-evaluation benchmark:** a labelled set of good and bad traces that measures how accurately TraceEval itself catches failures.
|
|
124
162
|
|
|
125
163
|
To track granular progress, see our [GitHub Issues](https://github.com/tej007-awesome/TraceEval/issues).
|
|
126
164
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "traceeval-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "CI/CD and Evaluation Infrastructure for Autonomous Agents (Agentic Engineering)"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Tejas Rajesh", email = "tejasrajesh05@gmail.com" }
|
|
@@ -38,7 +38,9 @@ Issues = "https://github.com/tej007-awesome/TraceEval/issues"
|
|
|
38
38
|
dev = [
|
|
39
39
|
"pytest>=8.0.0",
|
|
40
40
|
"pytest-asyncio>=0.23.0",
|
|
41
|
-
"ruff==0.11.2"
|
|
41
|
+
"ruff==0.11.2",
|
|
42
|
+
"opentelemetry-sdk>=1.20.0",
|
|
43
|
+
"opentelemetry-exporter-otlp-proto-common>=1.20.0",
|
|
42
44
|
]
|
|
43
45
|
|
|
44
46
|
[tool.ruff]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from google.protobuf.json_format import MessageToDict
|
|
6
|
+
from opentelemetry.exporter.otlp.proto.common._internal.trace_encoder import encode_spans
|
|
7
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
8
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
9
|
+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
|
|
10
|
+
|
|
11
|
+
FIXTURES_DIR = Path("tests/fixtures/otel")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_trace_data(include_content: bool = True) -> dict:
|
|
15
|
+
provider = TracerProvider()
|
|
16
|
+
exporter = InMemorySpanExporter()
|
|
17
|
+
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
18
|
+
tracer = provider.get_tracer("traceeval.reference_agent")
|
|
19
|
+
|
|
20
|
+
with tracer.start_as_current_span("invoke_agent refund-processor") as root:
|
|
21
|
+
root.set_attribute("gen_ai.operation.name", "invoke_agent")
|
|
22
|
+
root.set_attribute("gen_ai.agent.name", "refund-processor")
|
|
23
|
+
root.set_attribute("gen_ai.conversation.id", "sess_otel_001")
|
|
24
|
+
time.sleep(0.002)
|
|
25
|
+
|
|
26
|
+
# Step 1: LLM initial decision
|
|
27
|
+
with tracer.start_as_current_span("chat gpt-4o-mini") as chat1:
|
|
28
|
+
chat1.set_attribute("gen_ai.operation.name", "chat")
|
|
29
|
+
chat1.set_attribute("gen_ai.request.model", "gpt-4o-mini")
|
|
30
|
+
chat1.set_attribute("gen_ai.usage.input_tokens", 150)
|
|
31
|
+
chat1.set_attribute("gen_ai.usage.output_tokens", 30)
|
|
32
|
+
time.sleep(0.002)
|
|
33
|
+
|
|
34
|
+
# Step 2: Tool call 1 (lookup_order)
|
|
35
|
+
with tracer.start_as_current_span("execute_tool lookup_order") as tool1:
|
|
36
|
+
tool1.set_attribute("gen_ai.operation.name", "execute_tool")
|
|
37
|
+
tool1.set_attribute("gen_ai.tool.name", "lookup_order")
|
|
38
|
+
tool1.set_attribute("gen_ai.tool.call.id", "call_lookup_001")
|
|
39
|
+
if include_content:
|
|
40
|
+
tool1.set_attribute(
|
|
41
|
+
"gen_ai.tool.call.arguments", json.dumps({"order_id": "4521"})
|
|
42
|
+
)
|
|
43
|
+
time.sleep(0.002)
|
|
44
|
+
|
|
45
|
+
# Step 3: LLM reasoning after lookup
|
|
46
|
+
with tracer.start_as_current_span("chat gpt-4o-mini") as chat2:
|
|
47
|
+
chat2.set_attribute("gen_ai.operation.name", "chat")
|
|
48
|
+
chat2.set_attribute("gen_ai.request.model", "gpt-4o-mini")
|
|
49
|
+
chat2.set_attribute("gen_ai.usage.input_tokens", 220)
|
|
50
|
+
chat2.set_attribute("gen_ai.usage.output_tokens", 35)
|
|
51
|
+
time.sleep(0.002)
|
|
52
|
+
|
|
53
|
+
# Step 4: Tool call 2 (check_duplicate_charge)
|
|
54
|
+
with tracer.start_as_current_span("execute_tool check_duplicate_charge") as tool2:
|
|
55
|
+
tool2.set_attribute("gen_ai.operation.name", "execute_tool")
|
|
56
|
+
tool2.set_attribute("gen_ai.tool.name", "check_duplicate_charge")
|
|
57
|
+
tool2.set_attribute("gen_ai.tool.call.id", "call_check_dup_002")
|
|
58
|
+
if include_content:
|
|
59
|
+
tool2.set_attribute(
|
|
60
|
+
"gen_ai.tool.call.arguments", json.dumps({"order_id": "4521"})
|
|
61
|
+
)
|
|
62
|
+
time.sleep(0.002)
|
|
63
|
+
|
|
64
|
+
# Step 5: LLM reasoning after check
|
|
65
|
+
with tracer.start_as_current_span("chat gpt-4o-mini") as chat3:
|
|
66
|
+
chat3.set_attribute("gen_ai.operation.name", "chat")
|
|
67
|
+
chat3.set_attribute("gen_ai.request.model", "gpt-4o-mini")
|
|
68
|
+
chat3.set_attribute("gen_ai.usage.input_tokens", 310)
|
|
69
|
+
chat3.set_attribute("gen_ai.usage.output_tokens", 40)
|
|
70
|
+
time.sleep(0.002)
|
|
71
|
+
|
|
72
|
+
# Step 6: Tool call 3 (issue_refund)
|
|
73
|
+
with tracer.start_as_current_span("execute_tool issue_refund") as tool3:
|
|
74
|
+
tool3.set_attribute("gen_ai.operation.name", "execute_tool")
|
|
75
|
+
tool3.set_attribute("gen_ai.tool.name", "issue_refund")
|
|
76
|
+
tool3.set_attribute("gen_ai.tool.call.id", "call_refund_003")
|
|
77
|
+
if include_content:
|
|
78
|
+
tool3.set_attribute(
|
|
79
|
+
"gen_ai.tool.call.arguments",
|
|
80
|
+
json.dumps({"order_id": "4521", "amount": "full"}),
|
|
81
|
+
)
|
|
82
|
+
time.sleep(0.002)
|
|
83
|
+
|
|
84
|
+
# Step 7: Final response chat
|
|
85
|
+
with tracer.start_as_current_span("chat gpt-4o-mini") as chat4:
|
|
86
|
+
chat4.set_attribute("gen_ai.operation.name", "chat")
|
|
87
|
+
chat4.set_attribute("gen_ai.request.model", "gpt-4o-mini")
|
|
88
|
+
chat4.set_attribute("gen_ai.usage.input_tokens", 420)
|
|
89
|
+
chat4.set_attribute("gen_ai.usage.output_tokens", 55)
|
|
90
|
+
if include_content:
|
|
91
|
+
chat4.set_attribute(
|
|
92
|
+
"gen_ai.output.messages",
|
|
93
|
+
"I have verified the duplicate charge for order #4521. A full refund has been issued.",
|
|
94
|
+
)
|
|
95
|
+
time.sleep(0.002)
|
|
96
|
+
|
|
97
|
+
spans = sorted(exporter.get_finished_spans(), key=lambda s: s.start_time)
|
|
98
|
+
pb_request = encode_spans(spans)
|
|
99
|
+
return MessageToDict(pb_request)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def generate_fixtures():
|
|
103
|
+
FIXTURES_DIR.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
happy_data = generate_trace_data(include_content=True)
|
|
106
|
+
happy_path = FIXTURES_DIR / "refund_happy.json"
|
|
107
|
+
with open(happy_path, "w", encoding="utf-8") as f:
|
|
108
|
+
json.dump(happy_data, f, indent=2)
|
|
109
|
+
print(f"Generated {happy_path}")
|
|
110
|
+
|
|
111
|
+
no_args_data = generate_trace_data(include_content=False)
|
|
112
|
+
no_args_path = FIXTURES_DIR / "refund_no_args.json"
|
|
113
|
+
with open(no_args_path, "w", encoding="utf-8") as f:
|
|
114
|
+
json.dump(no_args_data, f, indent=2)
|
|
115
|
+
print(f"Generated {no_args_path}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
generate_fixtures()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import sys
|
|
2
3
|
import asyncio
|
|
3
4
|
import typer
|
|
@@ -41,35 +42,51 @@ def main(
|
|
|
41
42
|
def run(
|
|
42
43
|
case_file: Path = typer.Option(..., "--case", "-c", help="Path to EDDTestCase JSON"),
|
|
43
44
|
trace_file: Optional[Path] = typer.Option(None, "--trace", "-t", help="Path to static AgentTrace JSON"),
|
|
45
|
+
otel_trace: Optional[Path] = typer.Option(None, "--otel-trace", help="Path to OpenTelemetry trace JSON"),
|
|
44
46
|
pipeline: Optional[str] = typer.Option(None, "--pipeline", "-p", help="Live agent function (e.g. 'examples.reference_agent:process_refund')"),
|
|
47
|
+
pricing: Optional[Path] = typer.Option(None, "--pricing", help="Path to custom JSON pricing file"),
|
|
45
48
|
export_path: Optional[str] = typer.Option(None, "--export", "-e", help="Path to save the JSON EvaluationResult"),
|
|
46
49
|
max_cost: float = typer.Option(0.10, "--max-cost", help="Maximum allowable session budget in USD"),
|
|
47
|
-
score_threshold: float = typer.Option(0.8, "--score-threshold", help="Minimum score
|
|
50
|
+
score_threshold: float = typer.Option(0.8, "--score-threshold", help="Minimum score (0-1) that every non-null judge dimension must meet"),
|
|
48
51
|
):
|
|
49
|
-
"""Run a TraceEval evaluation against a static trace or a live agent pipeline."""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
"""Run a TraceEval evaluation against a static trace, an OTel trace, or a live agent pipeline."""
|
|
53
|
+
has_api_key = bool(settings.llm_api_key or os.environ.get("OPENAI_API_KEY"))
|
|
54
|
+
has_base_url = bool(settings.llm_base_url)
|
|
55
|
+
if not (has_api_key or has_base_url):
|
|
52
56
|
console.print("\n[bold red]Configuration Error:[/bold red]")
|
|
53
57
|
console.print("Either LLM_API_KEY (or OPENAI_API_KEY) or LLM_BASE_URL must be configured.")
|
|
54
58
|
raise typer.Exit(code=1)
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
provided = [x for x in [trace_file, otel_trace, pipeline] if x is not None]
|
|
61
|
+
if len(provided) != 1:
|
|
62
|
+
console.print(
|
|
63
|
+
"[bold red]Error:[/bold red] Exactly one of --trace, --otel-trace, or --pipeline must be provided."
|
|
64
|
+
)
|
|
58
65
|
raise typer.Exit(code=1)
|
|
59
|
-
|
|
66
|
+
|
|
60
67
|
try:
|
|
61
68
|
case = load_test_case(case_file)
|
|
62
|
-
|
|
63
|
-
# Determine Execution Mode (Static vs Live)
|
|
69
|
+
|
|
70
|
+
# Determine Execution Mode (Static vs OTel vs Live)
|
|
64
71
|
if pipeline:
|
|
65
72
|
console.print(f"[dim]Mode: Live Pipeline execution ({pipeline})[/dim]")
|
|
66
73
|
trace = run_live_pipeline(pipeline, case)
|
|
67
74
|
elif trace_file:
|
|
68
75
|
console.print(f"[dim]Mode: Static Batch execution ({trace_file})[/dim]")
|
|
69
76
|
trace = load_trace(trace_file)
|
|
77
|
+
elif otel_trace:
|
|
78
|
+
console.print(f"[dim]Mode: OTel trace ({otel_trace})[/dim]")
|
|
79
|
+
from traceeval.loaders.otel import load_otel_trace
|
|
80
|
+
from traceeval.pricing import DEFAULT_PRICING, load_pricing
|
|
81
|
+
|
|
82
|
+
pricing_table = load_pricing(pricing) if pricing else DEFAULT_PRICING
|
|
83
|
+
otel_result = load_otel_trace(otel_trace, pricing=pricing_table)
|
|
84
|
+
for warning in otel_result.warnings:
|
|
85
|
+
console.print(f"[yellow]Warning:[/yellow] {warning}")
|
|
86
|
+
trace = otel_result.trace
|
|
70
87
|
else:
|
|
71
|
-
raise ValueError("No trace
|
|
72
|
-
|
|
88
|
+
raise ValueError("No valid trace or pipeline provided.")
|
|
89
|
+
|
|
73
90
|
except Exception as e:
|
|
74
91
|
console.print(f"[bold red]Ingestion Error:[/bold red] {e}")
|
|
75
92
|
raise typer.Exit(code=1)
|
|
@@ -83,12 +100,12 @@ def run(
|
|
|
83
100
|
|
|
84
101
|
# Render Terminal Output
|
|
85
102
|
render_result(result)
|
|
86
|
-
|
|
103
|
+
|
|
87
104
|
# Handle Export
|
|
88
105
|
if export_path:
|
|
89
106
|
export_to_json(result, export_path)
|
|
90
107
|
console.print(f"\n[dim]Report successfully exported to {export_path}[/dim]")
|
|
91
|
-
|
|
108
|
+
|
|
92
109
|
if not result.passed:
|
|
93
110
|
raise typer.Exit(code=1)
|
|
94
111
|
|
|
@@ -1,41 +1,50 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import Optional
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
from openai import AsyncOpenAI
|
|
4
5
|
from pydantic import Field, SecretStr
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
class Settings(BaseSettings):
|
|
7
|
-
"""
|
|
8
|
-
|
|
10
|
+
"""Defensive configuration manager.
|
|
11
|
+
|
|
9
12
|
Loads from .env and guarantees required keys are present at startup.
|
|
10
13
|
"""
|
|
14
|
+
|
|
11
15
|
llm_api_key: Optional[SecretStr] = Field(
|
|
12
|
-
default=None,
|
|
16
|
+
default=None,
|
|
13
17
|
alias="LLM_API_KEY",
|
|
14
|
-
description="API Key for the Bring-Your-Own-Judge LLM provider"
|
|
18
|
+
description="API Key for the Bring-Your-Own-Judge LLM provider",
|
|
15
19
|
)
|
|
16
20
|
llm_base_url: Optional[str] = Field(
|
|
17
|
-
default=None,
|
|
21
|
+
default=None,
|
|
18
22
|
alias="LLM_BASE_URL",
|
|
19
|
-
description="Custom base URL for the LLM provider (e.g. Ollama, vLLM)"
|
|
23
|
+
description="Custom base URL for the LLM provider (e.g. Ollama, vLLM)",
|
|
20
24
|
)
|
|
21
25
|
llm_model_name: str = Field(
|
|
22
|
-
default="gpt-4o-mini",
|
|
26
|
+
default="gpt-4o-mini",
|
|
23
27
|
alias="LLM_MODEL_NAME",
|
|
24
|
-
description="Model name to target for the judge evaluations"
|
|
28
|
+
description="Model name to target for the judge evaluations",
|
|
25
29
|
)
|
|
26
30
|
|
|
27
|
-
model_config = SettingsConfigDict(
|
|
28
|
-
|
|
29
|
-
env_file_encoding="utf-8",
|
|
30
|
-
extra="ignore"
|
|
31
|
-
)
|
|
31
|
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
|
32
|
+
|
|
32
33
|
|
|
33
34
|
# Instantiate the settings.
|
|
34
35
|
settings = Settings()
|
|
35
36
|
|
|
36
|
-
# Inject into environment variables for OpenAI SDK
|
|
37
|
-
if settings.llm_api_key is not None:
|
|
38
|
-
os.environ["OPENAI_API_KEY"] = settings.llm_api_key.get_secret_value()
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
def get_judge_client() -> AsyncOpenAI:
|
|
39
|
+
"""Return an AsyncOpenAI client configured explicitly from settings or environment."""
|
|
40
|
+
api_key = (
|
|
41
|
+
settings.llm_api_key.get_secret_value()
|
|
42
|
+
if settings.llm_api_key
|
|
43
|
+
else os.environ.get("OPENAI_API_KEY")
|
|
44
|
+
)
|
|
45
|
+
base_url = settings.llm_base_url
|
|
46
|
+
|
|
47
|
+
if not api_key and base_url:
|
|
48
|
+
api_key = "not-needed"
|
|
49
|
+
|
|
50
|
+
return AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
@@ -5,9 +5,14 @@ from enum import Enum
|
|
|
5
5
|
|
|
6
6
|
class TrajectoryMode(str, Enum):
|
|
7
7
|
"""How strict the agent's tool execution path must be evaluated."""
|
|
8
|
-
EXACT = "EXACT"
|
|
9
|
-
IN_ORDER = "IN_ORDER"
|
|
10
|
-
ANY_ORDER = "ANY_ORDER"
|
|
8
|
+
EXACT = "EXACT"
|
|
9
|
+
IN_ORDER = "IN_ORDER"
|
|
10
|
+
ANY_ORDER = "ANY_ORDER"
|
|
11
|
+
|
|
12
|
+
class CheckResult(BaseModel):
|
|
13
|
+
"""Outcome of a single deterministic gate, with human-readable reasons on failure."""
|
|
14
|
+
passed: bool
|
|
15
|
+
reasons: List[str] = []
|
|
11
16
|
|
|
12
17
|
class GoldenRecord(BaseModel):
|
|
13
18
|
meta_id: str
|
|
@@ -47,6 +52,7 @@ class AgentTrace(BaseModel):
|
|
|
47
52
|
executed_tools: List[ToolCall]
|
|
48
53
|
final_output: str
|
|
49
54
|
total_token_cost_usd: float = Field(ge=0.0)
|
|
55
|
+
cost_complete: bool = True
|
|
50
56
|
|
|
51
57
|
class EvaluationDimensionScore(BaseModel):
|
|
52
58
|
"""Scores mapped directly to the 5 dimensions of Vibe Coding Evaluation."""
|
|
@@ -62,4 +68,5 @@ class EvaluationResult(BaseModel):
|
|
|
62
68
|
case_id: str
|
|
63
69
|
passed: bool
|
|
64
70
|
scores: EvaluationDimensionScore
|
|
65
|
-
trace_summary: AgentTrace
|
|
71
|
+
trace_summary: AgentTrace
|
|
72
|
+
failures: List[str] = []
|