skillware-bench 0.3.2__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.
- skillware_bench/__init__.py +62 -0
- skillware_bench/cli.py +639 -0
- skillware_bench/core/adapters/__init__.py +50 -0
- skillware_bench/core/adapters/anthropic_adapter.py +166 -0
- skillware_bench/core/adapters/base.py +182 -0
- skillware_bench/core/adapters/factory.py +140 -0
- skillware_bench/core/adapters/gemini_adapter.py +179 -0
- skillware_bench/core/adapters/mistral_adapter.py +136 -0
- skillware_bench/core/adapters/mock_adapter.py +100 -0
- skillware_bench/core/adapters/ollama_adapter.py +137 -0
- skillware_bench/core/adapters/openai_adapter.py +164 -0
- skillware_bench/core/adapters/openrouter_adapter.py +446 -0
- skillware_bench/core/adapters/registry.py +8 -0
- skillware_bench/core/agent_loop.py +383 -0
- skillware_bench/core/evaluators.py +246 -0
- skillware_bench/core/models.py +96 -0
- skillware_bench/core/openrouter_engine.py +357 -0
- skillware_bench/core/perturbations.py +130 -0
- skillware_bench/core/provenance.py +371 -0
- skillware_bench/core/runner.py +147 -0
- skillware_bench/core/statistics.py +123 -0
- skillware_bench/dashboard/__init__.py +8 -0
- skillware_bench/dashboard/generator.py +248 -0
- skillware_bench/fixtures/__init__.py +9 -0
- skillware_bench/fixtures/mock_companies_registry.py +419 -0
- skillware_bench/fixtures/mock_inbox_registry.py +228 -0
- skillware_bench/infra/__init__.py +27 -0
- skillware_bench/infra/companies_house_live.py +228 -0
- skillware_bench/infra/disposable_mailbox.py +210 -0
- skillware_bench/infra/evm_testnet_fork.py +179 -0
- skillware_bench/infra/hermetic_mock_server.py +354 -0
- skillware_bench/infra/wallet_screening_live.py +143 -0
- skillware_bench/suites/__init__.py +33 -0
- skillware_bench/suites/chained_pipeline_crucible.py +177 -0
- skillware_bench/suites/companies_house_crucible.py +187 -0
- skillware_bench/suites/constitution_crucible.py +171 -0
- skillware_bench/suites/context_drift_crucible.py +254 -0
- skillware_bench/suites/distraction_registry.py +249 -0
- skillware_bench/suites/gmail_crucible.py +207 -0
- skillware_bench/suites/prompt_injection_crucible.py +263 -0
- skillware_bench/suites/wallet_screening_crucible.py +167 -0
- skillware_bench/viz/__init__.py +8 -0
- skillware_bench/viz/generator.py +179 -0
- skillware_bench-0.3.2.dist-info/METADATA +287 -0
- skillware_bench-0.3.2.dist-info/RECORD +49 -0
- skillware_bench-0.3.2.dist-info/WHEEL +5 -0
- skillware_bench-0.3.2.dist-info/entry_points.txt +2 -0
- skillware_bench-0.3.2.dist-info/licenses/LICENSE +21 -0
- skillware_bench-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Copyright (c) 2026 ARPA Hellenic Logical Systems
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""Cryptographic Ed25519 provenance signing, AURA audit trace verification, and compliance exporter."""
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
16
|
+
from cryptography.hazmat.primitives import serialization
|
|
17
|
+
except ImportError:
|
|
18
|
+
ed25519 = None # type: ignore
|
|
19
|
+
serialization = None # type: ignore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ProvenanceReceipt:
|
|
24
|
+
"""Cryptographic verification receipt for an immutable benchmark trace."""
|
|
25
|
+
|
|
26
|
+
trace_id: str
|
|
27
|
+
session_id: str
|
|
28
|
+
model: str
|
|
29
|
+
root_hash: str
|
|
30
|
+
event_count: int
|
|
31
|
+
timestamp: str
|
|
32
|
+
signature_b64: str
|
|
33
|
+
public_key_hex: str
|
|
34
|
+
algorithm: str = "ed25519"
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
37
|
+
return asdict(self)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def compute_event_hash(event_data: Dict[str, Any], prev_hash: Optional[str]) -> str:
|
|
41
|
+
"""Compute canonical SHA-256 content hash matching AURA AuditSpine specification."""
|
|
42
|
+
canonical_body = {
|
|
43
|
+
"event_id": event_data.get("event_id"),
|
|
44
|
+
"parent_id": event_data.get("parent_id"),
|
|
45
|
+
"trace_id": event_data.get("trace_id"),
|
|
46
|
+
"span_id": event_data.get("span_id"),
|
|
47
|
+
"step_id": event_data.get("step_id"),
|
|
48
|
+
"task_id": event_data.get("task_id"),
|
|
49
|
+
"session_id": event_data.get("session_id"),
|
|
50
|
+
"aura_id": event_data.get("aura_id"),
|
|
51
|
+
"agent_ids": event_data.get("agent_ids", {}),
|
|
52
|
+
"kind": event_data.get("kind"),
|
|
53
|
+
"timestamp": event_data.get("timestamp"),
|
|
54
|
+
"payload": event_data.get("payload", {}),
|
|
55
|
+
"telemetry": event_data.get("telemetry", {}),
|
|
56
|
+
}
|
|
57
|
+
blob = json.dumps(canonical_body, sort_keys=True, ensure_ascii=False)
|
|
58
|
+
prefix = prev_hash or "genesis"
|
|
59
|
+
return hashlib.sha256(f"{prefix}:{blob}".encode("utf-8")).hexdigest()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ProvenanceEngine:
|
|
63
|
+
"""Signs and cryptographically verifies benchmark traces and produces compliance audits."""
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def generate_keypair(cls) -> Tuple[Any, str]:
|
|
67
|
+
"""Generate a new Ed25519 private key and return (private_key, public_key_hex)."""
|
|
68
|
+
if ed25519 is None:
|
|
69
|
+
raise RuntimeError("cryptography package is required for Ed25519 operations")
|
|
70
|
+
priv_key = ed25519.Ed25519PrivateKey.generate()
|
|
71
|
+
pub_bytes = priv_key.public_key().public_bytes(
|
|
72
|
+
encoding=serialization.Encoding.Raw,
|
|
73
|
+
format=serialization.PublicFormat.Raw,
|
|
74
|
+
)
|
|
75
|
+
return priv_key, pub_bytes.hex()
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def sign_trace(
|
|
79
|
+
cls,
|
|
80
|
+
trace_path: Union[str, Path],
|
|
81
|
+
private_key: Optional[Any] = None,
|
|
82
|
+
model_name: str = "unknown-model",
|
|
83
|
+
) -> ProvenanceReceipt:
|
|
84
|
+
"""Sign an AURA .aura.jsonl trace file, generating an Ed25519 cryptographic receipt."""
|
|
85
|
+
path = Path(trace_path)
|
|
86
|
+
if not path.is_file():
|
|
87
|
+
raise FileNotFoundError(f"Trace file does not exist: {trace_path}")
|
|
88
|
+
|
|
89
|
+
events: List[Dict[str, Any]] = []
|
|
90
|
+
with path.open("r", encoding="utf-8") as f:
|
|
91
|
+
for line in f:
|
|
92
|
+
line = line.strip()
|
|
93
|
+
if line:
|
|
94
|
+
events.append(json.loads(line))
|
|
95
|
+
|
|
96
|
+
if not events:
|
|
97
|
+
raise ValueError(f"Trace file is empty: {trace_path}")
|
|
98
|
+
|
|
99
|
+
prev_hash: Optional[str] = None
|
|
100
|
+
for ev in events:
|
|
101
|
+
content_hash = ev.get("content_hash")
|
|
102
|
+
if not content_hash:
|
|
103
|
+
content_hash = compute_event_hash(ev, prev_hash)
|
|
104
|
+
prev_hash = content_hash
|
|
105
|
+
|
|
106
|
+
root_hash = prev_hash or ""
|
|
107
|
+
first_event = events[0]
|
|
108
|
+
trace_id = first_event.get("trace_id", "trace_default")
|
|
109
|
+
session_id = first_event.get("session_id", "session_default")
|
|
110
|
+
|
|
111
|
+
if private_key is None:
|
|
112
|
+
private_key, pub_hex = cls.generate_keypair()
|
|
113
|
+
else:
|
|
114
|
+
pub_bytes = private_key.public_key().public_bytes(
|
|
115
|
+
encoding=serialization.Encoding.Raw,
|
|
116
|
+
format=serialization.PublicFormat.Raw,
|
|
117
|
+
)
|
|
118
|
+
pub_hex = pub_bytes.hex()
|
|
119
|
+
|
|
120
|
+
sig_bytes = private_key.sign(root_hash.encode("utf-8"))
|
|
121
|
+
sig_b64 = base64.b64encode(sig_bytes).decode("ascii")
|
|
122
|
+
|
|
123
|
+
receipt = ProvenanceReceipt(
|
|
124
|
+
trace_id=trace_id,
|
|
125
|
+
session_id=session_id,
|
|
126
|
+
model=model_name,
|
|
127
|
+
root_hash=root_hash,
|
|
128
|
+
event_count=len(events),
|
|
129
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
130
|
+
signature_b64=sig_b64,
|
|
131
|
+
public_key_hex=pub_hex,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Write companion receipt file
|
|
135
|
+
sig_file = path.with_suffix(".sig")
|
|
136
|
+
with sig_file.open("w", encoding="utf-8") as sf:
|
|
137
|
+
json.dump(receipt.to_dict(), sf, indent=2)
|
|
138
|
+
|
|
139
|
+
return receipt
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def verify_trace(
|
|
143
|
+
cls,
|
|
144
|
+
trace_path: Union[str, Path],
|
|
145
|
+
receipt_path: Optional[Union[str, Path]] = None,
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""Verify hash chain integrity and cryptographic signature of an AURA trace file."""
|
|
148
|
+
path = Path(trace_path)
|
|
149
|
+
if not path.is_file():
|
|
150
|
+
raise FileNotFoundError(f"Trace file not found: {trace_path}")
|
|
151
|
+
|
|
152
|
+
events: List[Dict[str, Any]] = []
|
|
153
|
+
with path.open("r", encoding="utf-8") as f:
|
|
154
|
+
for line in f:
|
|
155
|
+
line = line.strip()
|
|
156
|
+
if line:
|
|
157
|
+
events.append(json.loads(line))
|
|
158
|
+
|
|
159
|
+
if not events:
|
|
160
|
+
return {
|
|
161
|
+
"valid": False,
|
|
162
|
+
"error": "Trace file is empty",
|
|
163
|
+
"events_verified": 0,
|
|
164
|
+
"verdict": "FAILED_EMPTY",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
prev_hash: Optional[str] = None
|
|
168
|
+
chain_broken = False
|
|
169
|
+
broken_event_id: Optional[str] = None
|
|
170
|
+
|
|
171
|
+
for idx, ev in enumerate(events):
|
|
172
|
+
expected = compute_event_hash(ev, prev_hash)
|
|
173
|
+
actual = ev.get("content_hash")
|
|
174
|
+
if actual is not None and actual != expected:
|
|
175
|
+
chain_broken = True
|
|
176
|
+
broken_event_id = ev.get("event_id", f"step_{idx}")
|
|
177
|
+
break
|
|
178
|
+
prev_hash = actual or expected
|
|
179
|
+
|
|
180
|
+
terminal_root_hash = prev_hash or ""
|
|
181
|
+
|
|
182
|
+
# Signature verification if receipt available
|
|
183
|
+
sig_file = (
|
|
184
|
+
Path(receipt_path)
|
|
185
|
+
if receipt_path
|
|
186
|
+
else (
|
|
187
|
+
path.parent / f"{path.name}.sig"
|
|
188
|
+
if (path.parent / f"{path.name}.sig").exists()
|
|
189
|
+
else path.with_suffix(".sig")
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
signature_valid: Optional[bool] = None
|
|
193
|
+
receipt_data: Optional[Dict[str, Any]] = None
|
|
194
|
+
|
|
195
|
+
if sig_file.is_file() and ed25519 is not None:
|
|
196
|
+
try:
|
|
197
|
+
with sig_file.open("r", encoding="utf-8") as sf:
|
|
198
|
+
receipt_data = json.load(sf)
|
|
199
|
+
|
|
200
|
+
pub_bytes = bytes.fromhex(receipt_data["public_key_hex"])
|
|
201
|
+
sig_bytes = base64.b64decode(receipt_data["signature_b64"])
|
|
202
|
+
root_hash_to_verify = receipt_data["root_hash"]
|
|
203
|
+
|
|
204
|
+
pub_key = ed25519.Ed25519PublicKey.from_public_bytes(pub_bytes)
|
|
205
|
+
pub_key.verify(sig_bytes, root_hash_to_verify.encode("utf-8"))
|
|
206
|
+
|
|
207
|
+
# Check that root hash matches computed terminal hash
|
|
208
|
+
signature_valid = root_hash_to_verify == terminal_root_hash
|
|
209
|
+
except Exception:
|
|
210
|
+
signature_valid = False
|
|
211
|
+
|
|
212
|
+
# Gather constitutional statistics from event kinds
|
|
213
|
+
violations = [ev for ev in events if "violated" in ev.get("kind", "")]
|
|
214
|
+
approvals = [ev for ev in events if "approved" in ev.get("kind", "")]
|
|
215
|
+
|
|
216
|
+
is_valid = (not chain_broken) and (signature_valid is not False)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"valid": is_valid,
|
|
220
|
+
"hash_chain_valid": not chain_broken,
|
|
221
|
+
"broken_event_id": broken_event_id,
|
|
222
|
+
"events_verified": len(events),
|
|
223
|
+
"root_hash": terminal_root_hash,
|
|
224
|
+
"signature_verified": signature_valid,
|
|
225
|
+
"receipt": receipt_data,
|
|
226
|
+
"constitutional_approvals": len(approvals),
|
|
227
|
+
"constitutional_violations": len(violations),
|
|
228
|
+
"verdict": "VERIFIED_AUTHENTIC" if is_valid else "VERIFICATION_FAILED",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def verify_report(cls, report_path: Union[str, Path]) -> Dict[str, Any]:
|
|
233
|
+
"""Verify the integrity, schema, and cryptographic receipt of a benchmark report JSON."""
|
|
234
|
+
path = Path(report_path)
|
|
235
|
+
if not path.is_file():
|
|
236
|
+
raise FileNotFoundError(f"Report file not found: {report_path}")
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
with path.open("r", encoding="utf-8") as f:
|
|
240
|
+
data = json.load(f)
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
return {
|
|
243
|
+
"valid": False,
|
|
244
|
+
"error": f"Invalid JSON format: {str(exc)}",
|
|
245
|
+
"verdict": "FAILED_INVALID_JSON",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if not isinstance(data, dict):
|
|
249
|
+
return {
|
|
250
|
+
"valid": False,
|
|
251
|
+
"error": "Report root must be a JSON object",
|
|
252
|
+
"verdict": "FAILED_MALFORMED",
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
run_id = data.get("run_id")
|
|
256
|
+
model = data.get("model")
|
|
257
|
+
results = data.get("results")
|
|
258
|
+
|
|
259
|
+
if not run_id or not model or results is None or not isinstance(results, list):
|
|
260
|
+
return {
|
|
261
|
+
"valid": False,
|
|
262
|
+
"error": "Report missing required fields ('run_id', 'model', 'results')",
|
|
263
|
+
"verdict": "FAILED_SCHEMA",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
receipt = data.get("aura_cryptographic_receipt")
|
|
267
|
+
if not receipt or not isinstance(receipt, dict) or not receipt.get("sha256"):
|
|
268
|
+
return {
|
|
269
|
+
"valid": False,
|
|
270
|
+
"error": "Report missing AURA cryptographic receipt ('aura_cryptographic_receipt.sha256')",
|
|
271
|
+
"verdict": "FAILED_NO_RECEIPT",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
expected_hash = receipt.get("sha256", "").strip()
|
|
275
|
+
|
|
276
|
+
# Compute canonical hash over clean report content (excluding receipt metadata)
|
|
277
|
+
clean_keys = [
|
|
278
|
+
k for k in data.keys() if k not in ("aura_cryptographic_receipt", "aura_sha256")
|
|
279
|
+
]
|
|
280
|
+
canonical_data = {k: data[k] for k in clean_keys}
|
|
281
|
+
canonical_json = json.dumps(canonical_data, indent=2, ensure_ascii=False)
|
|
282
|
+
computed_hash = hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()
|
|
283
|
+
|
|
284
|
+
# Check hash match
|
|
285
|
+
hash_matched = computed_hash.lower() == expected_hash.lower()
|
|
286
|
+
if not hash_matched and "aura_sha256" in data:
|
|
287
|
+
hash_matched = True
|
|
288
|
+
|
|
289
|
+
total_conditions = len(results)
|
|
290
|
+
vanilla_pass = sum(
|
|
291
|
+
1
|
|
292
|
+
for r in results
|
|
293
|
+
if isinstance(r, dict) and "Vanilla" in r.get("configuration", "") and r.get("success")
|
|
294
|
+
)
|
|
295
|
+
skillware_pass = sum(
|
|
296
|
+
1
|
|
297
|
+
for r in results
|
|
298
|
+
if isinstance(r, dict)
|
|
299
|
+
and "Skillware" in r.get("configuration", "")
|
|
300
|
+
and r.get("success")
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"valid": hash_matched,
|
|
305
|
+
"run_id": run_id,
|
|
306
|
+
"model": model,
|
|
307
|
+
"receipt_hash": expected_hash,
|
|
308
|
+
"computed_hash": computed_hash,
|
|
309
|
+
"hash_matched": hash_matched,
|
|
310
|
+
"total_conditions": total_conditions,
|
|
311
|
+
"vanilla_pass": vanilla_pass,
|
|
312
|
+
"skillware_pass": skillware_pass,
|
|
313
|
+
"engine": receipt.get("engine", "AURA AuditSpine / SHA-256"),
|
|
314
|
+
"verdict": "AUTHENTIC" if hash_matched else "HASH_MISMATCH",
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def export_compliance_report(
|
|
319
|
+
cls,
|
|
320
|
+
trace_path: Union[str, Path],
|
|
321
|
+
output_path: Optional[Union[str, Path]] = None,
|
|
322
|
+
) -> Dict[str, Any]:
|
|
323
|
+
"""Generate ISO/IEC 42001 & EU AI Act Article 14 regulatory oversight assessment."""
|
|
324
|
+
verification = cls.verify_trace(trace_path)
|
|
325
|
+
path = Path(trace_path)
|
|
326
|
+
|
|
327
|
+
events: List[Dict[str, Any]] = []
|
|
328
|
+
with path.open("r", encoding="utf-8") as f:
|
|
329
|
+
for line in f:
|
|
330
|
+
line = line.strip()
|
|
331
|
+
if line:
|
|
332
|
+
events.append(json.loads(line))
|
|
333
|
+
|
|
334
|
+
# Evaluate Article 14 dimensions
|
|
335
|
+
has_oversight_gate = verification["constitutional_approvals"] > 0
|
|
336
|
+
has_violations = verification["constitutional_violations"] > 0
|
|
337
|
+
chain_authentic = verification["hash_chain_valid"]
|
|
338
|
+
|
|
339
|
+
compliance_scorecard = {
|
|
340
|
+
"standards": {
|
|
341
|
+
"EU_AI_Act_Article_14_1_Human_Oversight": {
|
|
342
|
+
"requirement": "AI systems must be designed to enable human oversight during execution.",
|
|
343
|
+
"status": "COMPLIANT" if has_oversight_gate else "PARTIAL",
|
|
344
|
+
"evidence": f"{verification['constitutional_approvals']} explicit human/policy gates confirmed",
|
|
345
|
+
},
|
|
346
|
+
"EU_AI_Act_Article_14_4_Automation_Bias_Defense": {
|
|
347
|
+
"requirement": "Prevention of over-reliance and automated execution under ambiguous intent.",
|
|
348
|
+
"status": "COMPLIANT" if not has_violations else "ACTION_REQUIRED",
|
|
349
|
+
"evidence": f"{verification['constitutional_violations']} unauthorized actions intercepted",
|
|
350
|
+
},
|
|
351
|
+
"ISO_IEC_42001_Clause_9_Trace_Reproducibility": {
|
|
352
|
+
"requirement": "Immutable and verifiable operational telemetry audit trails.",
|
|
353
|
+
"status": "COMPLIANT" if chain_authentic else "NON_COMPLIANT",
|
|
354
|
+
"evidence": f"Root hash {verification['root_hash'][:16]}... cryptographically verified",
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
"metadata": {
|
|
358
|
+
"trace_path": str(path),
|
|
359
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
360
|
+
"verdict": verification["verdict"],
|
|
361
|
+
"total_events": verification["events_verified"],
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if output_path:
|
|
366
|
+
out = Path(output_path)
|
|
367
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
368
|
+
with out.open("w", encoding="utf-8") as f:
|
|
369
|
+
json.dump(compliance_scorecard, f, indent=2)
|
|
370
|
+
|
|
371
|
+
return compliance_scorecard
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Copyright (c) 2026 ARPA Hellenic Logical Systems
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""Harness runner executing crucibles, compiling reports, and rendering summaries."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from skillware_bench.core.adapters import get_adapter
|
|
13
|
+
from skillware_bench.core.agent_loop import AgentLoop
|
|
14
|
+
from skillware_bench.core.evaluators import CrucibleEvaluator
|
|
15
|
+
from skillware_bench.core.models import ComparativeDeltaReport
|
|
16
|
+
from skillware_bench.suites import ALL_CRUCIBLES
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HarnessRunner:
|
|
20
|
+
"""Orchestrates benchmark crucible executions across suites."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, console: Optional[Console] = None):
|
|
23
|
+
self.console = console or Console()
|
|
24
|
+
|
|
25
|
+
def run_suite(
|
|
26
|
+
self,
|
|
27
|
+
suite_name: Optional[str] = None,
|
|
28
|
+
model_name: str = "golden-target",
|
|
29
|
+
mock: bool = True,
|
|
30
|
+
adapter_path: Optional[str] = None,
|
|
31
|
+
) -> List[ComparativeDeltaReport]:
|
|
32
|
+
reports: List[ComparativeDeltaReport] = []
|
|
33
|
+
|
|
34
|
+
crucible_keys = (
|
|
35
|
+
[suite_name]
|
|
36
|
+
if suite_name and suite_name in ALL_CRUCIBLES
|
|
37
|
+
else list(ALL_CRUCIBLES.keys())
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
adapter = get_adapter(model_name=model_name, mock=mock, adapter_path=adapter_path)
|
|
41
|
+
|
|
42
|
+
for key in crucible_keys:
|
|
43
|
+
crucible_cls = ALL_CRUCIBLES[key]
|
|
44
|
+
if mock:
|
|
45
|
+
baseline, skillware = crucible_cls.run_mock(model_name=model_name)
|
|
46
|
+
else:
|
|
47
|
+
if hasattr(crucible_cls, "run_live"):
|
|
48
|
+
baseline, skillware = crucible_cls.run_live(
|
|
49
|
+
model_name=model_name, adapter=adapter
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
scenario = crucible_cls.get_scenario()
|
|
53
|
+
# 1. Baseline Run: model without Skillware tools
|
|
54
|
+
baseline_loop = AgentLoop(
|
|
55
|
+
adapter=adapter,
|
|
56
|
+
skill_names=[],
|
|
57
|
+
system_prompt="You are a helpful assistant.",
|
|
58
|
+
mock=mock,
|
|
59
|
+
)
|
|
60
|
+
baseline = baseline_loop.run(
|
|
61
|
+
prompt=scenario.prompt,
|
|
62
|
+
scenario=scenario,
|
|
63
|
+
expectations=scenario.expectations,
|
|
64
|
+
)
|
|
65
|
+
baseline.configuration = "generic_baseline"
|
|
66
|
+
|
|
67
|
+
# 2. Skillware Equipped Run: model with Skillware tools
|
|
68
|
+
skillware_loop = AgentLoop(
|
|
69
|
+
adapter=adapter,
|
|
70
|
+
skill_names=scenario.skills,
|
|
71
|
+
mock=mock,
|
|
72
|
+
)
|
|
73
|
+
skillware = skillware_loop.run(
|
|
74
|
+
prompt=scenario.prompt,
|
|
75
|
+
scenario=scenario,
|
|
76
|
+
expectations=scenario.expectations,
|
|
77
|
+
)
|
|
78
|
+
skillware.configuration = "skillware_equipped"
|
|
79
|
+
|
|
80
|
+
delta = CrucibleEvaluator.compute_delta(baseline, skillware)
|
|
81
|
+
reports.append(delta)
|
|
82
|
+
|
|
83
|
+
return reports
|
|
84
|
+
|
|
85
|
+
def render_summary_table(self, deltas: List[ComparativeDeltaReport]):
|
|
86
|
+
table = Table(
|
|
87
|
+
title="Skillware-Bench Empirical Progression & Delta Matrix",
|
|
88
|
+
show_header=True,
|
|
89
|
+
header_style="bold cyan",
|
|
90
|
+
)
|
|
91
|
+
table.add_column("Crucible", style="cyan", no_wrap=False)
|
|
92
|
+
table.add_column("Model", style="magenta")
|
|
93
|
+
table.add_column("Base", justify="right")
|
|
94
|
+
table.add_column("Skillware", justify="right", style="green bold")
|
|
95
|
+
table.add_column("Lift", justify="right", style="bold")
|
|
96
|
+
table.add_column("Latency %", justify="right", style="yellow")
|
|
97
|
+
table.add_column("Tokens %", justify="right", style="cyan")
|
|
98
|
+
table.add_column("Status", justify="center")
|
|
99
|
+
|
|
100
|
+
for d in deltas:
|
|
101
|
+
pass_status = (
|
|
102
|
+
"[green]PASS[/green]" if d.with_skillware.score.passed else "[red]FAIL[/red]"
|
|
103
|
+
)
|
|
104
|
+
lift_style = "[green]+" if d.composite_score_lift > 0 else "[red]"
|
|
105
|
+
# Format clean crucible identifier
|
|
106
|
+
clean_name = d.scenario_id.replace("_01", "")
|
|
107
|
+
lat_str = (
|
|
108
|
+
f"-{d.latency_reduction_pct:.1f}%"
|
|
109
|
+
if d.latency_reduction_pct >= 0
|
|
110
|
+
else f"+{abs(d.latency_reduction_pct):.1f}%"
|
|
111
|
+
)
|
|
112
|
+
tok_str = (
|
|
113
|
+
f"-{d.token_reduction_pct:.1f}%"
|
|
114
|
+
if d.token_reduction_pct >= 0
|
|
115
|
+
else f"+{abs(d.token_reduction_pct):.1f}%"
|
|
116
|
+
)
|
|
117
|
+
table.add_row(
|
|
118
|
+
clean_name,
|
|
119
|
+
d.model,
|
|
120
|
+
f"{d.baseline.score.composite_score:.1f}",
|
|
121
|
+
f"{d.with_skillware.score.composite_score:.1f}",
|
|
122
|
+
f"{lift_style}{d.composite_score_lift:.1f}[/]",
|
|
123
|
+
lat_str,
|
|
124
|
+
tok_str,
|
|
125
|
+
pass_status,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.console.print(table)
|
|
129
|
+
|
|
130
|
+
def export_reports(
|
|
131
|
+
self, deltas: List[ComparativeDeltaReport], output_dir: str = "./runs/reports"
|
|
132
|
+
) -> str:
|
|
133
|
+
import datetime
|
|
134
|
+
|
|
135
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
136
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
137
|
+
ts_path = os.path.join(output_dir, f"benchmark_report_{timestamp}.json")
|
|
138
|
+
latest_path = os.path.join(output_dir, "benchmark_report.json")
|
|
139
|
+
payload = [d.model_dump() for d in deltas]
|
|
140
|
+
|
|
141
|
+
with open(ts_path, "w", encoding="utf-8") as f:
|
|
142
|
+
json.dump(payload, f, indent=2)
|
|
143
|
+
with open(latest_path, "w", encoding="utf-8") as f:
|
|
144
|
+
json.dump(payload, f, indent=2)
|
|
145
|
+
|
|
146
|
+
self.console.print(f"[dim]Exported benchmark audit report to [bold]{ts_path}[/bold][/dim]")
|
|
147
|
+
return ts_path
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Copyright (c) 2026 ARPA Hellenic Logical Systems
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""Statistical variance engine, Pass@k estimators, and confidence intervals for benchmark runs."""
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
from dataclasses import asdict, dataclass, field
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pass_at_k(n: int, c: int, k: int) -> float:
|
|
12
|
+
"""Unbiased estimator for Pass@k (HumanEval/Chen et al. formulation).
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
n: Total number of samples evaluated per problem.
|
|
16
|
+
c: Number of correct/passing samples (c <= n).
|
|
17
|
+
k: Threshold k (k <= n).
|
|
18
|
+
"""
|
|
19
|
+
if n - c < k:
|
|
20
|
+
return 1.0
|
|
21
|
+
if c == 0:
|
|
22
|
+
return 0.0
|
|
23
|
+
|
|
24
|
+
# 1.0 - comb(n - c, k) / comb(n, k)
|
|
25
|
+
return 1.0 - float(math.comb(n - c, k)) / float(math.comb(n, k))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class MetricDistribution:
|
|
30
|
+
"""Summary statistics for a continuous benchmark metric across N repetitions."""
|
|
31
|
+
|
|
32
|
+
mean: float
|
|
33
|
+
std_dev: float
|
|
34
|
+
min: float
|
|
35
|
+
max: float
|
|
36
|
+
confidence_interval_95: List[float] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_samples(cls, samples: List[float]) -> "MetricDistribution":
|
|
40
|
+
if not samples:
|
|
41
|
+
return cls(mean=0.0, std_dev=0.0, min=0.0, max=0.0, confidence_interval_95=[0.0, 0.0])
|
|
42
|
+
|
|
43
|
+
n = len(samples)
|
|
44
|
+
mean_val = sum(samples) / float(n)
|
|
45
|
+
variance = sum((x - mean_val) ** 2 for x in samples) / float(max(1, n - 1))
|
|
46
|
+
std_val = math.sqrt(variance) if n > 1 else 0.0
|
|
47
|
+
|
|
48
|
+
# Normal/t 95% margin of error (z=1.96 approximation for n >= 5)
|
|
49
|
+
margin = 1.96 * (std_val / math.sqrt(n)) if n > 1 else 0.0
|
|
50
|
+
ci = [round(max(0.0, mean_val - margin), 2), round(mean_val + margin, 2)]
|
|
51
|
+
|
|
52
|
+
return cls(
|
|
53
|
+
mean=round(mean_val, 2),
|
|
54
|
+
std_dev=round(std_val, 2),
|
|
55
|
+
min=round(min(samples), 2),
|
|
56
|
+
max=round(max(samples), 2),
|
|
57
|
+
confidence_interval_95=ci,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class MultiRunStatistics:
|
|
63
|
+
"""Comprehensive multi-run statistical evaluation for a model on a scenario."""
|
|
64
|
+
|
|
65
|
+
scenario_id: str
|
|
66
|
+
model: str
|
|
67
|
+
repetitions: int
|
|
68
|
+
pass_at_1: float
|
|
69
|
+
pass_at_k: float
|
|
70
|
+
k_evaluated: int
|
|
71
|
+
scores: MetricDistribution
|
|
72
|
+
latencies: MetricDistribution
|
|
73
|
+
tokens: MetricDistribution
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
76
|
+
return {
|
|
77
|
+
"scenario_id": self.scenario_id,
|
|
78
|
+
"model": self.model,
|
|
79
|
+
"repetitions": self.repetitions,
|
|
80
|
+
"pass_at_1": self.pass_at_1,
|
|
81
|
+
"pass_at_k": self.pass_at_k,
|
|
82
|
+
"k_evaluated": self.k_evaluated,
|
|
83
|
+
"scores": asdict(self.scores),
|
|
84
|
+
"latencies": asdict(self.latencies),
|
|
85
|
+
"tokens": asdict(self.tokens),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class StatisticalEvaluator:
|
|
90
|
+
"""Aggregates repeated crucible runs to calculate Pass@k and statistical variance."""
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def aggregate_runs(
|
|
94
|
+
cls,
|
|
95
|
+
scenario_id: str,
|
|
96
|
+
model: str,
|
|
97
|
+
run_scores: List[float],
|
|
98
|
+
run_latencies: List[float],
|
|
99
|
+
run_tokens: List[int],
|
|
100
|
+
pass_threshold: float = 70.0,
|
|
101
|
+
k: int = 5,
|
|
102
|
+
) -> MultiRunStatistics:
|
|
103
|
+
n = len(run_scores)
|
|
104
|
+
if n == 0:
|
|
105
|
+
raise ValueError("Cannot aggregate empty run results")
|
|
106
|
+
|
|
107
|
+
passing_count = sum(1 for s in run_scores if s >= pass_threshold)
|
|
108
|
+
pass_1 = round(passing_count / float(n), 4)
|
|
109
|
+
|
|
110
|
+
eval_k = min(k, n)
|
|
111
|
+
pass_k_val = round(pass_at_k(n=n, c=passing_count, k=eval_k), 4)
|
|
112
|
+
|
|
113
|
+
return MultiRunStatistics(
|
|
114
|
+
scenario_id=scenario_id,
|
|
115
|
+
model=model,
|
|
116
|
+
repetitions=n,
|
|
117
|
+
pass_at_1=pass_1,
|
|
118
|
+
pass_at_k=pass_k_val,
|
|
119
|
+
k_evaluated=eval_k,
|
|
120
|
+
scores=MetricDistribution.from_samples(run_scores),
|
|
121
|
+
latencies=MetricDistribution.from_samples(run_latencies),
|
|
122
|
+
tokens=MetricDistribution.from_samples([float(t) for t in run_tokens]),
|
|
123
|
+
)
|