evalt 0.8.5__py3-none-any.whl

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.
evalt/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ """Public SDK surface for Evalt."""
2
+
3
+ from .core import (
4
+ BudgetExceeded,
5
+ Client,
6
+ DraftAnswer,
7
+ Evalt,
8
+ Example,
9
+ GateReport,
10
+ ModelResult,
11
+ OptimizationResult,
12
+ ProviderError,
13
+ RolePlan,
14
+ RoutedAnswer,
15
+ Suite,
16
+ Turn,
17
+ check_result,
18
+ select_role_plan,
19
+ )
20
+
21
+ __all__ = [
22
+ "BudgetExceeded",
23
+ "Client",
24
+ "DraftAnswer",
25
+ "Evalt",
26
+ "Example",
27
+ "GateReport",
28
+ "ModelResult",
29
+ "OptimizationResult",
30
+ "ProviderError",
31
+ "RolePlan",
32
+ "RoutedAnswer",
33
+ "Suite",
34
+ "Turn",
35
+ "check_result",
36
+ "select_role_plan",
37
+ ]
38
+
39
+ __version__ = "0.8.5"
evalt/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Support ``python -m evalt``."""
2
+
3
+ from .cli import main
4
+
5
+ raise SystemExit(main())
evalt/cli.py ADDED
@@ -0,0 +1,228 @@
1
+ """Command-line interface for Evalt projects, runs, and CI gates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ from .core import BudgetExceeded, Evalt, OpenRouterTransport, ProviderError, Suite, check_result
11
+
12
+
13
+ STARTER_SUITE = {
14
+ "schema": "evalt-suite-v1",
15
+ "name": "support-routing",
16
+ "prompt": "Classify the support message. Return one route label.",
17
+ "examples": [
18
+ {"id": "billing-1", "input": "I was charged twice", "approved_output": "billing"},
19
+ {"id": "account-1", "input": "My reset link expired", "approved_output": "account"},
20
+ {"id": "technical-1", "input": "The app freezes on launch", "approved_output": "technical"},
21
+ {"id": "billing-2", "input": "Please send last month's invoice", "approved_output": "billing"},
22
+ {"id": "account-2", "input": "I cannot sign in", "approved_output": "account"},
23
+ ],
24
+ "models": ["qwen/qwen3.5-9b", "google/gemini-3-flash-preview"],
25
+ "optimizer_model": "openai/gpt-5.6-luna",
26
+ "evaluator_model": "openai/gpt-5.6-luna",
27
+ "objective": "lowest_cost_at_accuracy",
28
+ "quality_threshold": 0.95,
29
+ "max_optimization_cost_usd": 2.00,
30
+ "rounds": 3,
31
+ "minimum_meaningful_quality_gain": 0.03,
32
+ "allow_few_shot": True,
33
+ "max_few_shot_examples": 3,
34
+ }
35
+
36
+
37
+ class _OfflineTransport:
38
+ def estimate_cost(self, *_args, **_kwargs):
39
+ raise RuntimeError("Offline status does not make provider calls.")
40
+
41
+ def complete(self, *_args, **_kwargs):
42
+ raise RuntimeError("Offline status does not make provider calls.")
43
+
44
+
45
+ def parser() -> argparse.ArgumentParser:
46
+ root = argparse.ArgumentParser(
47
+ prog="evalt",
48
+ description="Run prompts through a durable, tested, budget-bounded model route.",
49
+ )
50
+ root.add_argument("--version", action="version", version="evalt 0.8.5")
51
+ commands = root.add_subparsers(dest="command", required=True)
52
+
53
+ init = commands.add_parser("init", help="Write a reviewable starter suite; no provider call.")
54
+ init.add_argument("path", nargs="?", default="evalt.json")
55
+ init.add_argument("--force", action="store_true", help="Replace an existing file.")
56
+
57
+ validate = commands.add_parser("validate", help="Validate a suite offline; no API key or spend.")
58
+ validate.add_argument("suite")
59
+
60
+ draft = commands.add_parser("draft", help="Generate one bounded answer for approval or correction.")
61
+ draft.add_argument("--task", required=True)
62
+ draft.add_argument("--input", required=True)
63
+ draft.add_argument("--model", default="openai/gpt-5-mini")
64
+ draft.add_argument("--max-cost", type=float, default=0.10)
65
+
66
+ run = commands.add_parser("run", help="Execute one prompt through a durable route.")
67
+ run.add_argument("--route", required=True, help="Stable route name used to remember decisions.")
68
+ run.add_argument("--prompt", required=True)
69
+ run.add_argument("--input", required=True)
70
+ run.add_argument("--price", "--budget", dest="price", type=float, required=True, help="Maximum provider price for one production response.")
71
+ run.add_argument("--test-budget", "--maintenance-budget", dest="test_budget", default="auto", help="Automatic or numeric hard cap for a due retest.")
72
+ run.add_argument("--max-test-budget", type=float, default=1.0, help="Hard ceiling when --test-budget=auto.")
73
+ run.add_argument("--target-accuracy", type=float, default=0.95)
74
+ run.add_argument("--objective", choices=("match_baseline_at_lowest_cost", "best_within_price", "lowest_cost_at_accuracy"), default="lowest_cost_at_accuracy")
75
+ run.add_argument("--state", default=".evalt/evalt.db")
76
+ run.add_argument("--model", action="append", dest="models")
77
+ run.add_argument("--approved-output", help="Immediately record an accepted/corrected answer for future tests.")
78
+
79
+ status = commands.add_parser("status", help="Show the durable decision trail for one route; no provider call.")
80
+ status.add_argument("--route", required=True)
81
+ status.add_argument("--state", default=".evalt/evalt.db")
82
+
83
+ optimize = commands.add_parser("optimize", help="Run the suite under its hard provider-spend cap.")
84
+ optimize.add_argument("suite")
85
+ optimize.add_argument("--output", default="evalt-result.json")
86
+ optimize.add_argument("--model", action="append", dest="models", help="Override the suite candidate list; repeat for each model/reasoning configuration.")
87
+ optimize.add_argument("--max-parallel-models", type=int, help="Override parallel model lanes for this run.")
88
+ optimize.add_argument("--max-parallel-scenarios", type=int, help="Override parallel scenario lanes per model for this run.")
89
+ optimize.add_argument("--request-timeout", type=float, help="Maximum wall-clock seconds for one provider response.")
90
+
91
+ check = commands.add_parser("check", help="Gate an exported result for CI; no provider call.")
92
+ check.add_argument("result")
93
+ check.add_argument("--min-pass-rate", type=float, default=0.95)
94
+ check.add_argument("--max-cost-per-success", type=float)
95
+ check.add_argument("--require-complete-coverage", action="store_true")
96
+ check.add_argument("--json", action="store_true", help="Print the gate report as JSON.")
97
+ return root
98
+
99
+
100
+ def _write_starter(path: Path, *, force: bool) -> None:
101
+ if path.exists() and not force:
102
+ raise FileExistsError(f"{path} already exists; pass --force to replace it.")
103
+ path.write_text(json.dumps(STARTER_SUITE, indent=2) + "\n", encoding="utf-8")
104
+
105
+
106
+ def _summary(result, path: str) -> dict[str, object]:
107
+ return {
108
+ "winner_model": result.winner.model,
109
+ "winner_prompt": result.winner.selected_prompt,
110
+ "holdout_pass_rate": result.winner.holdout_pass_rate,
111
+ "final_test_scenarios": result.winner.holdout_unique_scenarios,
112
+ "final_test_executions": result.winner.holdout_executions,
113
+ "final_test_execution_pass_rate": result.winner.holdout_execution_pass_rate,
114
+ "estimated_cost_per_successful_call_usd": result.winner.estimated_cost_per_successful_call_usd,
115
+ "optimization_spend_usd": result.total_provider_spend_usd,
116
+ "elapsed_seconds": result.elapsed_seconds,
117
+ "exploratory": result.exploratory,
118
+ "winner_scope": result.winner_scope,
119
+ "quality_frontier": result.quality_frontier,
120
+ "diminishing_returns": result.diminishing_returns,
121
+ "unavailable_models": result.unavailable_models,
122
+ "result": path,
123
+ }
124
+
125
+
126
+ def main(argv: list[str] | None = None) -> int:
127
+ args = parser().parse_args(argv)
128
+ try:
129
+ if args.command == "init":
130
+ path = Path(args.path)
131
+ _write_starter(path, force=args.force)
132
+ print(
133
+ f"Created {path}. The five examples are a starter, not production evidence. "
134
+ f"Add at least 25 approved scenarios, then run: evalt validate {path}"
135
+ )
136
+ return 0
137
+ if args.command == "validate":
138
+ suite = Suite.load(args.suite)
139
+ print(json.dumps({
140
+ "valid": True,
141
+ "name": suite.name,
142
+ "examples": len(suite.examples),
143
+ "distinct_final_test_scenarios": max(1, len(suite.examples) // 5),
144
+ "exploratory": max(1, len(suite.examples) // 5) < 5,
145
+ "models": list(suite.models),
146
+ "quality_threshold": suite.quality_threshold,
147
+ "hard_provider_spend_cap_usd": suite.max_optimization_cost_usd,
148
+ "provider_call_started": False,
149
+ }, indent=2))
150
+ return 0
151
+ if args.command == "draft":
152
+ draft = Evalt().draft(
153
+ task=args.task, input=args.input, model=args.model, max_cost_usd=args.max_cost
154
+ )
155
+ print(json.dumps({
156
+ "task": draft.task,
157
+ "input": draft.input,
158
+ "answer": draft.answer,
159
+ "model": draft.model,
160
+ "provider_cost_usd": draft.provider_cost_usd,
161
+ "next": "Approve this answer or replace it with the answer you wanted.",
162
+ }, indent=2, ensure_ascii=False))
163
+ return 0
164
+ if args.command == "run":
165
+ answer = Evalt(state_path=args.state).run(
166
+ args.prompt,
167
+ args.input,
168
+ route=args.route,
169
+ price_usd=args.price,
170
+ test_budget_usd=args.test_budget if args.test_budget == "auto" else float(args.test_budget),
171
+ max_test_budget_usd=args.max_test_budget,
172
+ target_accuracy=args.target_accuracy,
173
+ objective=args.objective,
174
+ models=args.models,
175
+ )
176
+ if args.approved_output is not None:
177
+ if args.approved_output == answer.content:
178
+ answer.accept()
179
+ else:
180
+ answer.correct(args.approved_output)
181
+ print(json.dumps(answer.to_dict(), indent=2, ensure_ascii=False))
182
+ return 0
183
+ if args.command == "status":
184
+ print(json.dumps(Evalt(transport=_OfflineTransport(), state_path=args.state).route_status(args.route), indent=2, ensure_ascii=False))
185
+ return 0
186
+ if args.command == "optimize":
187
+ suite = Suite.load(args.suite)
188
+ client = Evalt(
189
+ transport=OpenRouterTransport(timeout_seconds=args.request_timeout)
190
+ if args.request_timeout is not None else None
191
+ )
192
+ optimize_kwargs = suite.optimize_kwargs()
193
+ if args.models:
194
+ optimize_kwargs["models"] = args.models
195
+ if args.max_parallel_models is not None:
196
+ optimize_kwargs["max_parallel_models"] = args.max_parallel_models
197
+ if args.max_parallel_scenarios is not None:
198
+ optimize_kwargs["max_parallel_scenarios"] = args.max_parallel_scenarios
199
+ result = client.client.optimize(
200
+ **optimize_kwargs,
201
+ progress_callback=lambda event: print(
202
+ json.dumps(event, ensure_ascii=False), file=sys.stderr, flush=True
203
+ ),
204
+ )
205
+ result.save(args.output)
206
+ print(json.dumps(_summary(result, args.output), indent=2, ensure_ascii=False))
207
+ return 0
208
+ with Path(args.result).open(encoding="utf-8") as handle:
209
+ report = check_result(
210
+ json.load(handle),
211
+ min_pass_rate=args.min_pass_rate,
212
+ max_cost_per_success_usd=args.max_cost_per_success,
213
+ require_complete_coverage=args.require_complete_coverage,
214
+ )
215
+ if args.json:
216
+ print(json.dumps(report.to_dict(), indent=2))
217
+ elif report.passed:
218
+ print(f"PASS: holdout pass rate {report.holdout_pass_rate:.1%}")
219
+ else:
220
+ print("FAIL: " + "; ".join(report.failures), file=sys.stderr)
221
+ return 0 if report.passed else 1
222
+ except (BudgetExceeded, ProviderError, ValueError, KeyError, OSError, json.JSONDecodeError) as error:
223
+ print(f"evalt: {error}", file=sys.stderr)
224
+ return 2
225
+
226
+
227
+ if __name__ == "__main__":
228
+ raise SystemExit(main())