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.
Files changed (100) hide show
  1. agents/__init__.py +6 -0
  2. agents/auditor/__init__.py +13 -0
  3. agents/auditor/budget_auditor.py +91 -0
  4. agents/auditor/parser.py +139 -0
  5. agents/auditor/prompts.py +64 -0
  6. agents/auditor/schema.py +67 -0
  7. agents/base.py +20 -0
  8. agents/reflection/__init__.py +3 -0
  9. agents/reflection/agent.py +77 -0
  10. agents/reflection/prompts.py +18 -0
  11. agents/research/__init__.py +4 -0
  12. agents/research/agent.py +45 -0
  13. agents/research/planner.py +52 -0
  14. agents/research/prompts.py +14 -0
  15. agents/support/__init__.py +5 -0
  16. agents/support/triage_agent.py +45 -0
  17. agents/travel/__init__.py +11 -0
  18. agents/travel/agent.py +377 -0
  19. agents/travel/prompts.py +30 -0
  20. agents/travel/session.py +110 -0
  21. cli/__init__.py +6 -0
  22. cli/demo.py +47 -0
  23. cli/formatter.py +93 -0
  24. cli/html_reporter.py +647 -0
  25. cli/main.py +423 -0
  26. cli/progress.py +38 -0
  27. cli/resolver.py +99 -0
  28. evalrun-0.4.0.dist-info/METADATA +268 -0
  29. evalrun-0.4.0.dist-info/RECORD +100 -0
  30. evalrun-0.4.0.dist-info/WHEEL +5 -0
  31. evalrun-0.4.0.dist-info/entry_points.txt +2 -0
  32. evalrun-0.4.0.dist-info/licenses/LICENSE +0 -0
  33. evalrun-0.4.0.dist-info/top_level.txt +4 -0
  34. framework/__init__.py +70 -0
  35. framework/core/__init__.py +17 -0
  36. framework/core/adapters.py +118 -0
  37. framework/core/contracts.py +88 -0
  38. framework/core/suite.py +44 -0
  39. framework/evaluation/__init__.py +22 -0
  40. framework/evaluation/base.py +29 -0
  41. framework/evaluation/dimensions.py +7 -0
  42. framework/evaluation/engine.py +110 -0
  43. framework/evaluation/evaluators/__init__.py +7 -0
  44. framework/evaluation/evaluators/adaptability.py +27 -0
  45. framework/evaluation/evaluators/base_llm.py +104 -0
  46. framework/evaluation/evaluators/constraint.py +27 -0
  47. framework/evaluation/evaluators/information_accuracy.py +41 -0
  48. framework/evaluation/evaluators/personalization.py +27 -0
  49. framework/evaluation/evaluators/planning.py +27 -0
  50. framework/evaluation/evaluators/support.py +81 -0
  51. framework/evaluation/prompts/__init__.py +11 -0
  52. framework/evaluation/prompts/adaptability.py +57 -0
  53. framework/evaluation/prompts/base.py +52 -0
  54. framework/evaluation/prompts/constraint.py +41 -0
  55. framework/evaluation/prompts/information_accuracy.py +79 -0
  56. framework/evaluation/prompts/personalization.py +57 -0
  57. framework/evaluation/prompts/planning.py +61 -0
  58. framework/evaluation/runner.py +363 -0
  59. framework/evaluation/testing.py +25 -0
  60. framework/exceptions.py +49 -0
  61. framework/llms/__init__.py +8 -0
  62. framework/llms/base.py +37 -0
  63. framework/llms/factory.py +38 -0
  64. framework/llms/gemini.py +85 -0
  65. framework/llms/mock.py +25 -0
  66. framework/llms/openai.py +94 -0
  67. framework/llms/openai_compatible.py +139 -0
  68. framework/mcp/__init__.py +20 -0
  69. framework/mcp/client.py +62 -0
  70. framework/mcp/constraints.py +125 -0
  71. framework/mcp/revision_summary.py +122 -0
  72. framework/mcp/server.py +49 -0
  73. framework/memory/__init__.py +3 -0
  74. framework/memory/base.py +17 -0
  75. framework/models.py +83 -0
  76. framework/parser.py +45 -0
  77. framework/parsers/__init__.py +12 -0
  78. framework/parsers/frontmatter.py +28 -0
  79. framework/parsers/mapper.py +66 -0
  80. framework/parsers/markdown.py +122 -0
  81. framework/parsers/transformers.py +112 -0
  82. framework/profiles/__init__.py +28 -0
  83. framework/profiles/registry.py +104 -0
  84. framework/profiles/support.py +27 -0
  85. framework/profiles/travel.py +78 -0
  86. framework/regression/__init__.py +17 -0
  87. framework/regression/comparator.py +273 -0
  88. framework/regression/loader.py +145 -0
  89. framework/sdk.py +151 -0
  90. framework/utils.py +41 -0
  91. framework/verification/__init__.py +13 -0
  92. framework/verification/base.py +32 -0
  93. framework/verification/extractor.py +105 -0
  94. framework/verification/local.py +122 -0
  95. framework/verification/models.py +112 -0
  96. framework/verification/pipeline.py +36 -0
  97. framework/verification/prompts.py +24 -0
  98. framework/verification/utils.py +54 -0
  99. ui/__init__.py +1 -0
  100. 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