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
@@ -0,0 +1,363 @@
1
+ """Benchmark runner for orchestrating end-to-end agent evaluation pipelines."""
2
+
3
+ import inspect
4
+ import json
5
+ import os
6
+ import sys
7
+ import time
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Dict
11
+ from framework.core import RunTrace
12
+ from framework.models import AgentOutput, Benchmark, EvaluationResult
13
+ from framework.parser import parse_benchmark
14
+ from framework.profiles import PROFILE_REGISTRY, TRAVEL_PROFILE
15
+ from framework.evaluation.engine import EvaluationEngine
16
+ from framework.evaluation.dimensions import (
17
+ CONSTRAINT_SATISFACTION,
18
+ PLANNING_QUALITY,
19
+ INFORMATION_ACCURACY,
20
+ PERSONALIZATION,
21
+ ADAPTABILITY,
22
+ )
23
+ from framework.evaluation.evaluators import (
24
+ ConstraintEvaluator,
25
+ PlanningQualityEvaluator,
26
+ InformationAccuracyEvaluator,
27
+ PersonalizationEvaluator,
28
+ AdaptabilityEvaluator,
29
+ )
30
+ from framework.evaluation.evaluators.support import (
31
+ SupportAdaptabilityEvaluator,
32
+ SupportInformationAccuracyEvaluator,
33
+ SupportPersonalizationEvaluator,
34
+ SupportPlanningEvaluator,
35
+ )
36
+ from framework.verification.extractor import ClaimExtractor
37
+ from framework.verification.local import LocalKnowledgeBaseVerifier
38
+ from framework.verification.pipeline import VerificationPipeline
39
+
40
+
41
+ class BenchmarkRunner:
42
+ """Automates the entire evaluation pipeline for a given agent under test.
43
+
44
+ Orchestrates benchmark ingestion, profile lookup, agent execution, evaluation via LLM judges,
45
+ and report generation.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ agent,
51
+ judge_llm,
52
+ local_verifier_path: str = "ground_truth/japan_demo.json",
53
+ output_dir: str = "scratch",
54
+ auditor: Any = None,
55
+ ):
56
+ """Initializes the BenchmarkRunner.
57
+
58
+ Args:
59
+ agent: The agent under test (must expose a run(prompt: str) method returning AgentOutput).
60
+ judge_llm: The reference judge LLM client.
61
+ local_verifier_path: Path to the local database file for factual verification.
62
+ output_dir: Folder to store reports and itineraries.
63
+ auditor: Optional IndependentBudgetAuditor instance for v3 read-only gate checks.
64
+ """
65
+ self.agent = agent
66
+ self.judge_llm = judge_llm
67
+ self.local_verifier_path = local_verifier_path
68
+ self.output_dir = output_dir
69
+ self.auditor = auditor
70
+
71
+ # Initialize the verification pipeline
72
+ self.verifier = LocalKnowledgeBaseVerifier(local_verifier_path)
73
+ self.extractor = ClaimExtractor(judge_llm)
74
+ self.pipeline = VerificationPipeline(self.extractor, self.verifier)
75
+
76
+ # Initialize the evaluation engine
77
+ evaluators = {
78
+ CONSTRAINT_SATISFACTION: ConstraintEvaluator(judge_llm),
79
+ PLANNING_QUALITY: PlanningQualityEvaluator(judge_llm),
80
+ INFORMATION_ACCURACY: InformationAccuracyEvaluator(judge_llm, self.pipeline),
81
+ PERSONALIZATION: PersonalizationEvaluator(judge_llm),
82
+ ADAPTABILITY: AdaptabilityEvaluator(judge_llm),
83
+ }
84
+
85
+ # Merge dynamic evaluator plugins from registry
86
+ try:
87
+ from framework.profiles.registry import _DYNAMIC_EVALUATOR_REGISTRY
88
+ for dim_name, plugin_instance in _DYNAMIC_EVALUATOR_REGISTRY.items():
89
+ evaluators[dim_name] = plugin_instance
90
+ except Exception:
91
+ pass
92
+
93
+ self.engine = EvaluationEngine(evaluators=evaluators)
94
+
95
+ def _configure_profile_evaluators(self, profile) -> None:
96
+ """Apply domain-specific rubrics after the scenario profile is known."""
97
+ if getattr(profile, "name", "").lower() != "support-triage":
98
+ return
99
+ self.engine.evaluators.update(
100
+ {
101
+ PLANNING_QUALITY: SupportPlanningEvaluator(self.judge_llm),
102
+ INFORMATION_ACCURACY: SupportInformationAccuracyEvaluator(
103
+ self.judge_llm, self.pipeline
104
+ ),
105
+ PERSONALIZATION: SupportPersonalizationEvaluator(self.judge_llm),
106
+ ADAPTABILITY: SupportAdaptabilityEvaluator(self.judge_llm),
107
+ }
108
+ )
109
+
110
+ def run(self, filepath: str) -> EvaluationResult:
111
+ """Runs the complete evaluation pipeline for a single benchmark file.
112
+
113
+ Args:
114
+ filepath: Path to the benchmark markdown file.
115
+
116
+ Returns:
117
+ The final EvaluationResult containing overall and dimension scores.
118
+ """
119
+ # 1. Parse scenario file
120
+ benchmark = parse_benchmark(filepath)
121
+
122
+ # 2. Resolve evaluation profile (Static -> Dynamic -> Fallback/Error)
123
+ prof_name = benchmark.profile
124
+ profile = PROFILE_REGISTRY.get(prof_name)
125
+ if not profile:
126
+ try:
127
+ from framework.profiles.registry import get_custom_profile
128
+ profile = get_custom_profile(prof_name)
129
+ except Exception:
130
+ pass
131
+ if not profile:
132
+ if prof_name in ("travel-agent", "travel", "default"):
133
+ profile = TRAVEL_PROFILE
134
+ else:
135
+ raise ValueError(
136
+ f"Evaluation profile '{prof_name}' not found in static or dynamic profile registries."
137
+ )
138
+
139
+ # Dimension names are shared across domains, but their rubrics are not.
140
+ # Configure support prompts only after this scenario's profile resolves.
141
+ self._configure_profile_evaluators(profile)
142
+
143
+ # 3. Execute agent with RunTrace instrumentation
144
+ t0 = time.time()
145
+ start_time = datetime.now(timezone.utc)
146
+ run_kwargs = {}
147
+ run_parameters = inspect.signature(self.agent.run).parameters
148
+ if "planning_mode" in run_parameters:
149
+ run_kwargs["planning_mode"] = "closed_world_evaluation"
150
+ if "validation_scenario_id" in run_parameters:
151
+ run_kwargs["validation_scenario_id"] = benchmark.benchmark_id
152
+
153
+ agent_output = None
154
+ status = "success"
155
+ err_msg = None
156
+
157
+ try:
158
+ agent_output = self.agent.run(benchmark.prompt, **run_kwargs)
159
+
160
+ # 4. Optional v3 Independent Budget Auditor Gate Pass
161
+ if self.auditor is not None:
162
+ audit_report = self.auditor.audit(
163
+ scenario_prompt=benchmark.prompt,
164
+ itinerary_content=agent_output.content,
165
+ )
166
+ agent_output.metadata["audit_report"] = audit_report.to_dict()
167
+ agent_output.metadata["audit_gate_decision"] = audit_report.status
168
+
169
+ # 5. Evaluate using the engine
170
+ result = self.engine.evaluate(benchmark, agent_output, profile)
171
+ result.agent_metadata = agent_output.metadata
172
+
173
+ latency = time.time() - t0
174
+ finished_time = datetime.now(timezone.utc)
175
+
176
+ # 6. Record RunTrace metadata
177
+ llm_obj = getattr(self.agent, "llm", None)
178
+ raw_usage = getattr(llm_obj, "last_token_usage", None) if llm_obj else None
179
+ token_usage = raw_usage if isinstance(raw_usage, dict) else None
180
+
181
+ trace = RunTrace(
182
+ trace_id=f"tr-{uuid.uuid4().hex[:8]}",
183
+ scenario_id=benchmark.benchmark_id,
184
+ agent_id=getattr(self.agent, "agent_name", self.agent.__class__.__name__),
185
+ model_name=getattr(llm_obj, "model_name", "unknown") if llm_obj else "unknown",
186
+ started_at_utc=start_time.isoformat(),
187
+ finished_at_utc=finished_time.isoformat(),
188
+ latency_seconds=round(latency, 2),
189
+ status="success",
190
+ error=None,
191
+ token_usage=token_usage,
192
+ metadata=agent_output.metadata,
193
+ )
194
+ agent_output.metadata["run_trace"] = {
195
+ "trace_id": trace.trace_id,
196
+ "started_at_utc": trace.started_at_utc,
197
+ "finished_at_utc": trace.finished_at_utc,
198
+ "latency_seconds": trace.latency_seconds,
199
+ "status": trace.status,
200
+ "token_usage": trace.token_usage,
201
+ }
202
+
203
+ # 7. Save reports and outputs
204
+ self._save_execution_files(benchmark, agent_output, result, profile)
205
+ return result
206
+
207
+ except Exception as e:
208
+ latency = time.time() - t0
209
+ finished_time = datetime.now(timezone.utc)
210
+ status = "timeout" if isinstance(e, (TimeoutError, TimeoutError)) or "timeout" in str(e).lower() else "error"
211
+ err_msg = str(e)
212
+
213
+ llm_obj = getattr(self.agent, "llm", None)
214
+ raw_usage = getattr(llm_obj, "last_token_usage", None) if llm_obj else None
215
+ token_usage = raw_usage if isinstance(raw_usage, dict) else None
216
+
217
+ agent_name = getattr(self.agent, "agent_name", None) or self.agent.__class__.__name__
218
+ model_name = getattr(llm_obj, "model_name", "unknown") if llm_obj and hasattr(llm_obj, "model_name") else "unknown"
219
+
220
+ trace = RunTrace(
221
+ trace_id=f"tr-{uuid.uuid4().hex[:8]}",
222
+ scenario_id=benchmark.benchmark_id,
223
+ agent_id=str(agent_name),
224
+ model_name=str(model_name),
225
+ started_at_utc=start_time.isoformat(),
226
+ finished_at_utc=finished_time.isoformat(),
227
+ latency_seconds=round(latency, 2),
228
+ status=status,
229
+ error=err_msg,
230
+ token_usage=token_usage,
231
+ )
232
+
233
+ # Persist error trace metadata file
234
+ self._save_error_trace_file(benchmark, trace)
235
+ raise
236
+
237
+ def run_directory(self, dirpath: str) -> Dict[str, EvaluationResult]:
238
+ """Runs evaluations for all benchmark files in the specified directory.
239
+
240
+ Args:
241
+ dirpath: Path to the folder containing benchmark markdown files.
242
+
243
+ Returns:
244
+ A dictionary mapping benchmark ID to the EvaluationResult.
245
+ """
246
+ results = {}
247
+ for filename in sorted(os.listdir(dirpath)):
248
+ if filename.endswith(".md"):
249
+ filepath = os.path.join(dirpath, filename)
250
+ try:
251
+ result = self.run(filepath)
252
+ results[result.benchmark_id] = result
253
+ except Exception as e:
254
+ print(f"Error evaluating benchmark '{filename}': {e}", file=sys.stderr)
255
+ return results
256
+
257
+ def _save_execution_files(
258
+ self,
259
+ benchmark: Benchmark,
260
+ agent_output: AgentOutput,
261
+ result: EvaluationResult,
262
+ profile: Any,
263
+ ):
264
+ """Helper to serialize agent output, JSON data reports, and Markdown summaries."""
265
+ os.makedirs(self.output_dir, exist_ok=True)
266
+ agent_name = getattr(self.agent, "llm", self.agent).__class__.__name__.lower()
267
+ if hasattr(self.agent, "llm") and hasattr(self.agent.llm, "model_name"):
268
+ model_slug = self.agent.llm.model_name.replace("/", "_").replace(".", "_")
269
+ else:
270
+ model_slug = agent_name
271
+
272
+ base_name = f"{model_slug}_{benchmark.benchmark_id}"
273
+
274
+ # 1. Save Raw Agent Output/Itinerary
275
+ itinerary_path = os.path.join(self.output_dir, f"{base_name}_itinerary.md")
276
+ with open(itinerary_path, "w", encoding="utf-8") as f:
277
+ f.write(agent_output.content)
278
+
279
+ # 2. Save Detailed JSON Report
280
+ report_data = {
281
+ "benchmark_id": result.benchmark_id,
282
+ "benchmark_name": result.benchmark_name,
283
+ "agent_model": getattr(self.agent, "llm", self.agent).__class__.__name__,
284
+ "model_name": getattr(getattr(self.agent, "llm", None), "model_name", "unknown"),
285
+ "profile": profile.name,
286
+ "overall_score": result.overall_score,
287
+ "passed": result.passed,
288
+ "agent_metadata": agent_output.metadata,
289
+ "dimension_scores": [
290
+ {
291
+ "dimension": ds.dimension,
292
+ "score": ds.score,
293
+ "reason": ds.reason,
294
+ }
295
+ for ds in result.dimension_scores
296
+ ],
297
+ }
298
+ json_path = os.path.join(self.output_dir, f"{base_name}_report.json")
299
+ with open(json_path, "w", encoding="utf-8") as f:
300
+ json.dump(report_data, f, indent=2)
301
+
302
+ # 3. Save Summary Markdown Report
303
+ markdown_path = os.path.join(self.output_dir, f"{base_name}_report.md")
304
+ with open(markdown_path, "w", encoding="utf-8") as f:
305
+ f.write(f"# Evaluation Summary Report\n\n")
306
+ f.write(f"- **Benchmark**: {result.benchmark_name} (`{result.benchmark_id}`)\n")
307
+ f.write(f"- **Agent Model**: {report_data['model_name']}\n")
308
+ f.write(f"- **Evaluation Profile**: `{profile.name}`\n")
309
+ f.write(f"- **Overall Score**: **{result.overall_score:.2f} / 100**\n")
310
+ f.write(f"- **Outcome**: {'PASSED' if result.passed else 'FAILED'}\n\n")
311
+
312
+ # Render Auditor Gate metadata section if present
313
+ if "audit_report" in agent_output.metadata:
314
+ audit = agent_output.metadata["audit_report"]
315
+ f.write(f"## Independent Auditor Gate Report\n\n")
316
+ f.write(f"- **Gate Decision**: `{'PASS' if audit.get('passed') else 'BLOCK'}`\n")
317
+ f.write(f"- **Audit Score**: {audit.get('audit_score', 0.0):.2f} / 100\n")
318
+ f.write(f"- **Audit Confidence**: {audit.get('audit_confidence', 0.0):.2f}\n")
319
+ f.write(f"- **Reasoning Summary**: {audit.get('reasoning_summary', 'N/A')}\n\n")
320
+ if audit.get("violations"):
321
+ f.write("### Detected Budget Violations\n\n")
322
+ for v in audit["violations"]:
323
+ f.write(f"- **{v['violation_type']}**: {v['description']} (Est. Discrepancy: ₹{v.get('estimated_discrepancy_inr', 0.0):,.2f})\n")
324
+ f.write("\n")
325
+
326
+ # Render MCP metadata trace section if present
327
+ if "mcp_validation" in agent_output.metadata:
328
+ mcp = agent_output.metadata["mcp_validation"]
329
+ f.write(f"## MCP Validation Trace\n\n")
330
+ f.write(f"- **Status**: `{mcp.get('status', 'unknown')}`\n")
331
+ f.write(f"- **Locked-Booking Check**: `{'valid' if mcp.get('locked_valid') else 'invalid'}`\n")
332
+ f.write(f"- **Savings Check**: ₹{mcp.get('savings_realized_inr', 0):,} verified (target: ₹{mcp.get('target_savings_inr', 0):,})\n\n")
333
+
334
+ f.write(f"## Dimension Breakdown\n\n")
335
+ f.write(f"| Dimension | Score | Weight |\n")
336
+ f.write(f"| :--- | :---: | :---: |\n")
337
+ for ds in result.dimension_scores:
338
+ weight = profile.weights.get(ds.dimension, 0.0)
339
+ f.write(f"| {ds.dimension} | {ds.score:.1f} | {weight:.1f}% |\n")
340
+ f.write(f"\n## Reasoning Details\n\n")
341
+ for ds in result.dimension_scores:
342
+ f.write(f"### {ds.dimension} (Score: {ds.score:.1f})\n\n")
343
+ f.write(f"{ds.reason}\n\n")
344
+
345
+ def _save_error_trace_file(self, benchmark: Benchmark, trace: RunTrace) -> None:
346
+ """Persists an error/timeout trace JSON record when an evaluation fails."""
347
+ os.makedirs(self.output_dir, exist_ok=True)
348
+ model_name = trace.model_name.replace("/", "_").replace(".", "_")
349
+ json_path = os.path.join(self.output_dir, f"{model_name}_{benchmark.benchmark_id}_error_trace.json")
350
+ trace_data = {
351
+ "trace_id": trace.trace_id,
352
+ "scenario_id": trace.scenario_id,
353
+ "agent_id": trace.agent_id,
354
+ "model_name": trace.model_name,
355
+ "started_at_utc": trace.started_at_utc,
356
+ "finished_at_utc": trace.finished_at_utc,
357
+ "latency_seconds": trace.latency_seconds,
358
+ "status": trace.status,
359
+ "error": trace.error,
360
+ "token_usage": trace.token_usage,
361
+ }
362
+ with open(json_path, "w", encoding="utf-8") as f:
363
+ json.dump(trace_data, f, indent=2)
@@ -0,0 +1,25 @@
1
+ """Mock and testing utilities for evaluation pipelines."""
2
+
3
+ from framework.evaluation.base import BaseEvaluator
4
+ from framework.models import AgentOutput, Benchmark, DimensionScore
5
+
6
+
7
+ class DummyEvaluator(BaseEvaluator):
8
+ """Simple evaluator returning a static score for profile verification."""
9
+
10
+ def __init__(self, dimension: str, score: float, reason: str):
11
+ """Initializes the DummyEvaluator.
12
+
13
+ Args:
14
+ dimension: Dimension name key this evaluator represents.
15
+ score: Static numeric score to return.
16
+ reason: Static reason explanation to return.
17
+ """
18
+ self._dimension = dimension
19
+ self.score = score
20
+ self.reason = reason
21
+
22
+ def evaluate(self, benchmark: Benchmark, output: AgentOutput) -> DimensionScore:
23
+ return DimensionScore(
24
+ dimension=self._dimension, score=self.score, reason=self.reason
25
+ )
@@ -0,0 +1,49 @@
1
+ """Common exceptions for the evaluation framework."""
2
+
3
+
4
+ class BenchmarkError(Exception):
5
+ """Base class for all benchmark-related exceptions."""
6
+
7
+ pass
8
+
9
+
10
+ class FrontmatterParseError(BenchmarkError):
11
+ """Raised when parsing YAML frontmatter from content fails."""
12
+
13
+ pass
14
+
15
+
16
+ class MarkdownParseError(BenchmarkError):
17
+ """Raised when parsing markdown body into sections fails."""
18
+
19
+ pass
20
+
21
+
22
+ class TransformerError(BenchmarkError):
23
+ """Raised when transforming raw markdown sections into Python structures fails."""
24
+
25
+ pass
26
+
27
+
28
+ class MapperError(BenchmarkError):
29
+ """Raised when mapping parsed content to a Benchmark dataclass fails."""
30
+
31
+ pass
32
+
33
+
34
+ class EvaluationError(Exception):
35
+ """Raised when evaluation orchestration or execution fails."""
36
+
37
+ pass
38
+
39
+
40
+ class VerificationError(EvaluationError):
41
+ """Base class for factual claim verification errors."""
42
+
43
+ pass
44
+
45
+
46
+ class KnowledgeBaseLoadError(VerificationError):
47
+ """Raised when loading or parsing a verifier knowledge base fails."""
48
+
49
+ pass
@@ -0,0 +1,8 @@
1
+ """LLM abstractions package containing wrapper clients and factory methods."""
2
+
3
+ from framework.llms.base import BaseLLM, Message
4
+ from framework.llms.mock import MockLLM
5
+ from framework.llms.openai import OpenAILLM
6
+ from framework.llms.gemini import GeminiLLM
7
+ from framework.llms.openai_compatible import OpenAICompatibleLLM
8
+ from framework.llms.factory import create_llm
framework/llms/base.py ADDED
@@ -0,0 +1,37 @@
1
+ """Base interfaces and data classes for LLM clients."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Dict, List
6
+
7
+
8
+ @dataclass
9
+ class Message:
10
+ """Represents a chat message sent to/from an LLM."""
11
+
12
+ role: str
13
+ content: str
14
+
15
+
16
+ @dataclass
17
+ class LLMResponse:
18
+ """Represents a structured response from an LLM."""
19
+
20
+ text: str
21
+ metadata: Dict[str, Any] = field(default_factory=dict)
22
+
23
+
24
+ class BaseLLM(ABC):
25
+ """Abstract base class representing an LLM client wrapper."""
26
+
27
+ @abstractmethod
28
+ def generate(self, messages: List[Message]) -> LLMResponse:
29
+ """Generates a structured completion for the given list of chat messages.
30
+
31
+ Args:
32
+ messages: A list of Message dataclasses.
33
+
34
+ Returns:
35
+ An LLMResponse containing the text outcome.
36
+ """
37
+ pass
@@ -0,0 +1,38 @@
1
+ """Factory module for creating LLM client wrappers."""
2
+
3
+ from typing import Any, Dict, Type
4
+ from framework.llms.base import BaseLLM
5
+ from framework.llms.mock import MockLLM
6
+ from framework.llms.openai import OpenAILLM
7
+ from framework.llms.gemini import GeminiLLM
8
+
9
+ # Registry of supported providers mapping provider name to class wrapper
10
+ _PROVIDERS: Dict[str, Type[BaseLLM]] = {
11
+ "openai": OpenAILLM,
12
+ "gemini": GeminiLLM,
13
+ "mock": MockLLM,
14
+ }
15
+
16
+
17
+ def create_llm(provider: str, **kwargs: Any) -> BaseLLM:
18
+ """Factory function to instantiate an LLM wrapper by provider name.
19
+
20
+ Args:
21
+ provider: Name of the LLM provider ('openai', 'gemini', 'mock').
22
+ **kwargs: Arguments passed to the provider class constructor.
23
+
24
+ Returns:
25
+ An instance of BaseLLM.
26
+
27
+ Raises:
28
+ ValueError: If an unknown provider name is supplied.
29
+ """
30
+ try:
31
+ provider_cls = _PROVIDERS[provider.lower()]
32
+ except KeyError:
33
+ supported = ", ".join(sorted(_PROVIDERS.keys()))
34
+ raise ValueError(
35
+ f"Unknown LLM provider '{provider}'. Supported providers: {supported}"
36
+ )
37
+
38
+ return provider_cls(**kwargs)
@@ -0,0 +1,85 @@
1
+ """Google Gemini client wrapper implementation."""
2
+
3
+ from typing import List, Optional
4
+ from framework.llms.base import BaseLLM, LLMResponse, Message
5
+
6
+
7
+ class GeminiLLM(BaseLLM):
8
+ """Google Gemini API client wrapper."""
9
+
10
+ def __init__(
11
+ self, model_name: str = "gemini-1.5-pro", api_key: Optional[str] = None
12
+ ):
13
+ """Initializes the GeminiLLM.
14
+
15
+ Validates that the 'google-generativeai' library is installed immediately.
16
+
17
+ Args:
18
+ model_name: The target model name.
19
+ api_key: The Gemini API key. If not provided, configuration is skipped.
20
+
21
+ Raises:
22
+ ImportError: If the 'google-generativeai' library is not installed.
23
+ """
24
+ try:
25
+ # pyrefly: ignore [missing-import]
26
+ import google.generativeai as genai
27
+ except ImportError:
28
+ raise ImportError(
29
+ "The 'google-generativeai' library is required to use GeminiLLM. "
30
+ "Please install it using 'pip install google-generativeai'."
31
+ )
32
+
33
+ if api_key:
34
+ genai.configure(api_key=api_key)
35
+
36
+ self._genai = genai
37
+ # Ensure name is prefixed correctly (e.g. models/gemini-1.5-pro)
38
+ if model_name and not model_name.startswith("models/"):
39
+ self.model_name = f"models/{model_name}"
40
+ else:
41
+ self.model_name = model_name
42
+
43
+ def generate(self, messages: List[Message]) -> LLMResponse:
44
+ system_instruction = None
45
+ gemini_contents = []
46
+
47
+ for msg in messages:
48
+ if msg.role == "system":
49
+ system_instruction = msg.content
50
+ else:
51
+ # Map standard roles: user -> user, assistant/model -> model
52
+ role = "user" if msg.role == "user" else "model"
53
+ gemini_contents.append(
54
+ {"role": role, "parts": [{"text": msg.content}]}
55
+ )
56
+
57
+ config = {}
58
+ if system_instruction:
59
+ config["system_instruction"] = system_instruction
60
+
61
+ import time
62
+
63
+ model = self._genai.GenerativeModel(self.model_name, **config)
64
+
65
+ max_retries = 4
66
+ for attempt in range(max_retries):
67
+ try:
68
+ response = model.generate_content(
69
+ gemini_contents,
70
+ request_options={"timeout": 300.0},
71
+ )
72
+ response_text = response.text or ""
73
+ return LLMResponse(text=response_text)
74
+ except Exception as e:
75
+ err_str = str(e).lower()
76
+ is_transient = any(
77
+ err in err_str
78
+ for err in ["504", "deadline", "timeout", "resourceexhausted", "429", "503", "unavailable"]
79
+ )
80
+ if is_transient and attempt < max_retries - 1:
81
+ sleep_seconds = 5 * (attempt + 1)
82
+ print(f"[GeminiLLM] Transient error ({e}). Retrying in {sleep_seconds}s (attempt {attempt + 1}/{max_retries})...")
83
+ time.sleep(sleep_seconds)
84
+ else:
85
+ raise e
framework/llms/mock.py ADDED
@@ -0,0 +1,25 @@
1
+ """Mock LLM client wrapper for testing and offline validation."""
2
+
3
+ from typing import List, Union
4
+ from framework.llms.base import BaseLLM, LLMResponse, Message
5
+
6
+
7
+ class MockLLM(BaseLLM):
8
+ """A mock LLM wrapper for testing and offline execution."""
9
+
10
+ def __init__(self, responses: Union[str, List[str]]):
11
+ """Initializes the MockLLM.
12
+
13
+ Args:
14
+ responses: A single response string or a list of response strings
15
+ to return sequentially on each generate call.
16
+ """
17
+ self.responses = [responses] if isinstance(responses, str) else list(responses)
18
+ self.call_count = 0
19
+
20
+ def generate(self, messages: List[Message]) -> LLMResponse:
21
+ if not self.responses:
22
+ return LLMResponse(text="Mock response")
23
+ response = self.responses[self.call_count % len(self.responses)]
24
+ self.call_count += 1
25
+ return LLMResponse(text=response)