evalrun 0.4.0__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.
- agents/__init__.py +6 -0
- agents/auditor/__init__.py +13 -0
- agents/auditor/budget_auditor.py +91 -0
- agents/auditor/parser.py +139 -0
- agents/auditor/prompts.py +64 -0
- agents/auditor/schema.py +67 -0
- agents/base.py +20 -0
- agents/reflection/__init__.py +3 -0
- agents/reflection/agent.py +77 -0
- agents/reflection/prompts.py +18 -0
- agents/research/__init__.py +4 -0
- agents/research/agent.py +45 -0
- agents/research/planner.py +52 -0
- agents/research/prompts.py +14 -0
- agents/support/__init__.py +5 -0
- agents/support/triage_agent.py +45 -0
- agents/travel/__init__.py +11 -0
- agents/travel/agent.py +377 -0
- agents/travel/prompts.py +30 -0
- agents/travel/session.py +110 -0
- cli/__init__.py +6 -0
- cli/demo.py +47 -0
- cli/formatter.py +93 -0
- cli/html_reporter.py +647 -0
- cli/main.py +423 -0
- cli/progress.py +38 -0
- cli/resolver.py +99 -0
- evalrun-0.4.0.dist-info/METADATA +268 -0
- evalrun-0.4.0.dist-info/RECORD +100 -0
- evalrun-0.4.0.dist-info/WHEEL +5 -0
- evalrun-0.4.0.dist-info/entry_points.txt +2 -0
- evalrun-0.4.0.dist-info/licenses/LICENSE +0 -0
- evalrun-0.4.0.dist-info/top_level.txt +4 -0
- framework/__init__.py +70 -0
- framework/core/__init__.py +17 -0
- framework/core/adapters.py +118 -0
- framework/core/contracts.py +88 -0
- framework/core/suite.py +44 -0
- framework/evaluation/__init__.py +22 -0
- framework/evaluation/base.py +29 -0
- framework/evaluation/dimensions.py +7 -0
- framework/evaluation/engine.py +110 -0
- framework/evaluation/evaluators/__init__.py +7 -0
- framework/evaluation/evaluators/adaptability.py +27 -0
- framework/evaluation/evaluators/base_llm.py +104 -0
- framework/evaluation/evaluators/constraint.py +27 -0
- framework/evaluation/evaluators/information_accuracy.py +41 -0
- framework/evaluation/evaluators/personalization.py +27 -0
- framework/evaluation/evaluators/planning.py +27 -0
- framework/evaluation/evaluators/support.py +81 -0
- framework/evaluation/prompts/__init__.py +11 -0
- framework/evaluation/prompts/adaptability.py +57 -0
- framework/evaluation/prompts/base.py +52 -0
- framework/evaluation/prompts/constraint.py +41 -0
- framework/evaluation/prompts/information_accuracy.py +79 -0
- framework/evaluation/prompts/personalization.py +57 -0
- framework/evaluation/prompts/planning.py +61 -0
- framework/evaluation/runner.py +363 -0
- framework/evaluation/testing.py +25 -0
- framework/exceptions.py +49 -0
- framework/llms/__init__.py +8 -0
- framework/llms/base.py +37 -0
- framework/llms/factory.py +38 -0
- framework/llms/gemini.py +85 -0
- framework/llms/mock.py +25 -0
- framework/llms/openai.py +94 -0
- framework/llms/openai_compatible.py +139 -0
- framework/mcp/__init__.py +20 -0
- framework/mcp/client.py +62 -0
- framework/mcp/constraints.py +125 -0
- framework/mcp/revision_summary.py +122 -0
- framework/mcp/server.py +49 -0
- framework/memory/__init__.py +3 -0
- framework/memory/base.py +17 -0
- framework/models.py +83 -0
- framework/parser.py +45 -0
- framework/parsers/__init__.py +12 -0
- framework/parsers/frontmatter.py +28 -0
- framework/parsers/mapper.py +66 -0
- framework/parsers/markdown.py +122 -0
- framework/parsers/transformers.py +112 -0
- framework/profiles/__init__.py +28 -0
- framework/profiles/registry.py +104 -0
- framework/profiles/support.py +27 -0
- framework/profiles/travel.py +78 -0
- framework/regression/__init__.py +17 -0
- framework/regression/comparator.py +273 -0
- framework/regression/loader.py +145 -0
- framework/sdk.py +151 -0
- framework/utils.py +41 -0
- framework/verification/__init__.py +13 -0
- framework/verification/base.py +32 -0
- framework/verification/extractor.py +105 -0
- framework/verification/local.py +122 -0
- framework/verification/models.py +112 -0
- framework/verification/pipeline.py +36 -0
- framework/verification/prompts.py +24 -0
- framework/verification/utils.py +54 -0
- ui/__init__.py +1 -0
- ui/server.py +255 -0
ui/server.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Local-first guided web interface server for non-technical users."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import urllib.parse
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
|
+
|
|
12
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def list_available_scenarios(base_dir: str = "evals/scenarios") -> List[Dict[str, str]]:
|
|
16
|
+
"""Scans base_dir for benchmark scenario (.md) files."""
|
|
17
|
+
scenarios = []
|
|
18
|
+
p = Path(base_dir)
|
|
19
|
+
if p.exists() and p.is_dir():
|
|
20
|
+
for file_path in p.glob("**/*.md"):
|
|
21
|
+
try:
|
|
22
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
23
|
+
content = f.read(500)
|
|
24
|
+
name = file_path.stem.replace("-", " ").title()
|
|
25
|
+
for line in content.splitlines():
|
|
26
|
+
if line.startswith("name:"):
|
|
27
|
+
name = line.split(":", 1)[1].strip()
|
|
28
|
+
break
|
|
29
|
+
elif line.startswith("# Benchmark Scenario:"):
|
|
30
|
+
name = line.split(":", 1)[1].strip()
|
|
31
|
+
break
|
|
32
|
+
scenarios.append({
|
|
33
|
+
"id": file_path.stem,
|
|
34
|
+
"name": name,
|
|
35
|
+
"path": str(file_path),
|
|
36
|
+
})
|
|
37
|
+
except Exception:
|
|
38
|
+
scenarios.append({
|
|
39
|
+
"id": file_path.stem,
|
|
40
|
+
"name": file_path.stem,
|
|
41
|
+
"path": str(file_path),
|
|
42
|
+
})
|
|
43
|
+
return sorted(scenarios, key=lambda x: x["name"])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def list_available_baselines(search_dirs: List[str] = None) -> List[Dict[str, str]]:
|
|
47
|
+
"""Scans output directories for valid baseline run folders containing manifest.json."""
|
|
48
|
+
if search_dirs is None:
|
|
49
|
+
search_dirs = ["eval_results", "results"]
|
|
50
|
+
baselines = []
|
|
51
|
+
for d in search_dirs:
|
|
52
|
+
p = Path(d)
|
|
53
|
+
if p.exists() and p.is_dir():
|
|
54
|
+
for manifest_file in p.glob("**/manifest.json"):
|
|
55
|
+
run_dir = manifest_file.parent
|
|
56
|
+
try:
|
|
57
|
+
with open(manifest_file, "r", encoding="utf-8") as f:
|
|
58
|
+
data = json.load(f)
|
|
59
|
+
run_id = data.get("run_id", run_dir.name)
|
|
60
|
+
timestamp = data.get("timestamp_utc", "")
|
|
61
|
+
baselines.append({
|
|
62
|
+
"run_id": run_id,
|
|
63
|
+
"timestamp": timestamp,
|
|
64
|
+
"path": str(run_dir),
|
|
65
|
+
})
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return sorted(baselines, key=lambda x: x.get("timestamp", ""), reverse=True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class UIRequestHandler(SimpleHTTPRequestHandler):
|
|
72
|
+
"""HTTP Request Handler serving static frontend files and API endpoints."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, *args, **kwargs):
|
|
75
|
+
super().__init__(*args, directory=str(STATIC_DIR), **kwargs)
|
|
76
|
+
|
|
77
|
+
def end_headers(self):
|
|
78
|
+
# The UI is a local development surface; always serve the latest
|
|
79
|
+
# source files after a restart instead of allowing stale browser cache.
|
|
80
|
+
self.send_header("Cache-Control", "no-store")
|
|
81
|
+
super().end_headers()
|
|
82
|
+
|
|
83
|
+
def send_json_response(self, code: int, data: Dict[str, Any]):
|
|
84
|
+
body = json.dumps(data).encode("utf-8")
|
|
85
|
+
self.send_response(code)
|
|
86
|
+
self.send_header("Content-Type", "application/json")
|
|
87
|
+
self.send_header("Content-Length", str(len(body)))
|
|
88
|
+
self.end_headers()
|
|
89
|
+
self.wfile.write(body)
|
|
90
|
+
|
|
91
|
+
def do_GET(self):
|
|
92
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
93
|
+
path = parsed.path
|
|
94
|
+
|
|
95
|
+
if path == "/api/scenarios":
|
|
96
|
+
scenarios = list_available_scenarios()
|
|
97
|
+
self.send_json_response(200, {"scenarios": scenarios})
|
|
98
|
+
return
|
|
99
|
+
elif path == "/api/baselines":
|
|
100
|
+
baselines = list_available_baselines()
|
|
101
|
+
self.send_json_response(200, {"baselines": baselines})
|
|
102
|
+
return
|
|
103
|
+
elif path.startswith("/eval_results/") or path.startswith("/results/"):
|
|
104
|
+
# Enforce strict path traversal protection: ensure file remains within workspace root
|
|
105
|
+
try:
|
|
106
|
+
requested_path = (Path.cwd() / path.lstrip("/")).resolve()
|
|
107
|
+
workspace_root = Path.cwd().resolve()
|
|
108
|
+
if not requested_path.is_relative_to(workspace_root):
|
|
109
|
+
self.send_json_response(403, {"error": "Access denied: Path traversal outside workspace is forbidden."})
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
if requested_path.exists() and requested_path.is_file():
|
|
113
|
+
content_type = "text/html" if requested_path.suffix == ".html" else "application/json"
|
|
114
|
+
with open(requested_path, "rb") as f:
|
|
115
|
+
data = f.read()
|
|
116
|
+
self.send_response(200)
|
|
117
|
+
self.send_header("Content-Type", content_type)
|
|
118
|
+
self.send_header("Content-Length", str(len(data)))
|
|
119
|
+
self.end_headers()
|
|
120
|
+
self.wfile.write(data)
|
|
121
|
+
return
|
|
122
|
+
else:
|
|
123
|
+
self.send_json_response(404, {"error": "File not found."})
|
|
124
|
+
return
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.send_json_response(500, {"error": str(e)})
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Serve static UI files
|
|
130
|
+
if path == "/":
|
|
131
|
+
self.path = "/index.html"
|
|
132
|
+
return super().do_GET()
|
|
133
|
+
|
|
134
|
+
def do_POST(self):
|
|
135
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
136
|
+
path = parsed.path
|
|
137
|
+
|
|
138
|
+
if path == "/api/run":
|
|
139
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
140
|
+
body = self.rfile.read(length).decode("utf-8")
|
|
141
|
+
try:
|
|
142
|
+
payload = json.loads(body)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.send_json_response(400, {"error": f"Invalid JSON payload: {e}"})
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
scenario = payload.get("scenario")
|
|
148
|
+
agent = payload.get("agent", "agents.travel:TravelPlanningAgent")
|
|
149
|
+
model = payload.get("model", "qwen2.5-72b-instruct")
|
|
150
|
+
base_url = payload.get("base_url", "https://api.openai.com/v1")
|
|
151
|
+
api_key = payload.get("api_key")
|
|
152
|
+
judge_model = payload.get("judge_model")
|
|
153
|
+
baseline = payload.get("baseline")
|
|
154
|
+
output_dir = payload.get("output_dir")
|
|
155
|
+
if not output_dir:
|
|
156
|
+
run_stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
157
|
+
output_dir = f"./eval_results/ui-run-{run_stamp}"
|
|
158
|
+
|
|
159
|
+
if not scenario:
|
|
160
|
+
self.send_json_response(400, {"error": "Missing required field 'scenario'."})
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# UI users may provide a custom scenario path, but it must remain
|
|
164
|
+
# inside the local workspace. Never allow the UI to read arbitrary
|
|
165
|
+
# files from the host machine.
|
|
166
|
+
try:
|
|
167
|
+
workspace_root = Path.cwd().resolve()
|
|
168
|
+
scenario_path = (workspace_root / str(scenario)).resolve()
|
|
169
|
+
if not scenario_path.is_relative_to(workspace_root):
|
|
170
|
+
self.send_json_response(
|
|
171
|
+
403,
|
|
172
|
+
{"error": "Scenario path must remain inside the EvalRun workspace."},
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
if not scenario_path.exists():
|
|
176
|
+
self.send_json_response(400, {"error": f"Scenario path does not exist: {scenario}"})
|
|
177
|
+
return
|
|
178
|
+
except Exception as e:
|
|
179
|
+
self.send_json_response(400, {"error": f"Invalid scenario path: {e}"})
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
from framework.sdk import evaluate, compare
|
|
184
|
+
|
|
185
|
+
# Execute evaluation programmatically via SDK
|
|
186
|
+
results = evaluate(
|
|
187
|
+
scenario=scenario,
|
|
188
|
+
agent=agent,
|
|
189
|
+
model=model,
|
|
190
|
+
base_url=base_url,
|
|
191
|
+
api_key=api_key,
|
|
192
|
+
judge_model=judge_model,
|
|
193
|
+
output_dir=output_dir,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
regression_report_dict = None
|
|
197
|
+
release_blocked = False
|
|
198
|
+
if baseline:
|
|
199
|
+
reg_report = compare(
|
|
200
|
+
candidate_results=results,
|
|
201
|
+
baseline=baseline,
|
|
202
|
+
)
|
|
203
|
+
regression_report_dict = reg_report.to_dict()
|
|
204
|
+
release_blocked = reg_report.release_blocked
|
|
205
|
+
|
|
206
|
+
scenarios_data = []
|
|
207
|
+
for r in results:
|
|
208
|
+
gate = getattr(r, "agent_metadata", {}).get("audit_gate_decision", "PASS")
|
|
209
|
+
if gate == "BLOCK":
|
|
210
|
+
release_blocked = True
|
|
211
|
+
scenarios_data.append({
|
|
212
|
+
"benchmark_id": r.benchmark_id,
|
|
213
|
+
"name": r.benchmark_name,
|
|
214
|
+
"overall_score": r.overall_score,
|
|
215
|
+
"passed": r.passed,
|
|
216
|
+
"auditor_gate": gate,
|
|
217
|
+
"dimension_scores": [
|
|
218
|
+
{"dimension": ds.dimension, "score": ds.score, "reason": ds.reason}
|
|
219
|
+
for ds in r.dimension_scores
|
|
220
|
+
],
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
html_report_rel = f"/{Path(output_dir).relative_to(Path.cwd())}/report.html"
|
|
225
|
+
except Exception:
|
|
226
|
+
html_report_rel = f"/{output_dir}/report.html"
|
|
227
|
+
|
|
228
|
+
response_payload = {
|
|
229
|
+
"status": "success",
|
|
230
|
+
"total_scenarios": len(results),
|
|
231
|
+
"all_passed": all(r.passed for r in results),
|
|
232
|
+
"release_blocked": release_blocked,
|
|
233
|
+
"html_report_path": html_report_rel,
|
|
234
|
+
"scenarios": scenarios_data,
|
|
235
|
+
"regression": regression_report_dict,
|
|
236
|
+
}
|
|
237
|
+
self.send_json_response(200, response_payload)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.send_json_response(500, {"error": f"Evaluation execution failed: {e}"})
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
self.send_json_response(404, {"error": "Endpoint not found."})
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def run_ui_server(host: str = "127.0.0.1", port: int = 8501) -> HTTPServer:
|
|
246
|
+
"""Starts the local UI web server on the specified host and port."""
|
|
247
|
+
server = HTTPServer((host, port), UIRequestHandler)
|
|
248
|
+
print(f"=====================================================================")
|
|
249
|
+
print(f" EVALRUN GUIDED LOCAL UI SERVER STARTED")
|
|
250
|
+
print(f" URL: http://{host}:{port}")
|
|
251
|
+
if host not in ("127.0.0.1", "localhost"):
|
|
252
|
+
print(f" SECURITY WARNING: Bound to non-localhost address '{host}'.")
|
|
253
|
+
print(f" Ensure plain HTTP port is protected behind a secure network firewall.")
|
|
254
|
+
print(f"=====================================================================")
|
|
255
|
+
return server
|