truvyx 0.4.0__tar.gz
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.
- truvyx-0.4.0/PKG-INFO +8 -0
- truvyx-0.4.0/pyproject.toml +19 -0
- truvyx-0.4.0/setup.cfg +4 -0
- truvyx-0.4.0/tests/test_evaluate.py +57 -0
- truvyx-0.4.0/tests/test_suites.py +70 -0
- truvyx-0.4.0/tests/test_trace.py +96 -0
- truvyx-0.4.0/truvyx/__init__.py +37 -0
- truvyx-0.4.0/truvyx/client.py +370 -0
- truvyx-0.4.0/truvyx/trace.py +98 -0
- truvyx-0.4.0/truvyx/types.py +145 -0
- truvyx-0.4.0/truvyx.egg-info/PKG-INFO +8 -0
- truvyx-0.4.0/truvyx.egg-info/SOURCES.txt +12 -0
- truvyx-0.4.0/truvyx.egg-info/dependency_links.txt +1 -0
- truvyx-0.4.0/truvyx.egg-info/top_level.txt +1 -0
truvyx-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: truvyx
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Truvyx evaluation SDK — embed evaluation into any agent pipeline
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://truvyx.org
|
|
7
|
+
Project-URL: Documentation, https://truvyx.org/docs/sdk
|
|
8
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "truvyx"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Truvyx evaluation SDK — embed evaluation into any agent pipeline"
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
Homepage = "https://truvyx.org"
|
|
15
|
+
Documentation = "https://truvyx.org/docs/sdk"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["truvyx*"]
|
truvyx-0.4.0/setup.cfg
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from truvyx.client import evaluate
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_evaluate_maps_api_metadata_to_python_fields(monkeypatch):
|
|
5
|
+
responses = iter([
|
|
6
|
+
{
|
|
7
|
+
"scenario": {
|
|
8
|
+
"id": "scenario-1",
|
|
9
|
+
"name": "Safety",
|
|
10
|
+
"description": "",
|
|
11
|
+
"domain": "TEST",
|
|
12
|
+
"difficulty": "LOW",
|
|
13
|
+
"constraints": [],
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{"runId": "run-1"},
|
|
17
|
+
{
|
|
18
|
+
"ready": True,
|
|
19
|
+
"runId": "run-1",
|
|
20
|
+
"scenarioId": "scenario-1",
|
|
21
|
+
"status": "PASSED",
|
|
22
|
+
"passed": True,
|
|
23
|
+
"costProfile": {
|
|
24
|
+
"totalInputTokens": 10,
|
|
25
|
+
"totalOutputTokens": 5,
|
|
26
|
+
"totalTokens": 15,
|
|
27
|
+
"estimatedCostUsd": 0.01,
|
|
28
|
+
"apiCallCount": 1,
|
|
29
|
+
"toolCallCount": 2,
|
|
30
|
+
"redundantCallCount": 1,
|
|
31
|
+
"avgCallLatencyMs": 25,
|
|
32
|
+
},
|
|
33
|
+
"retryState": {
|
|
34
|
+
"attempts": 2,
|
|
35
|
+
"coldStartRetry": True,
|
|
36
|
+
"workerAvailable": True,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr("truvyx.client._request", lambda *args, **kwargs: next(responses))
|
|
42
|
+
monkeypatch.setattr("truvyx.client.time.sleep", lambda _seconds: None)
|
|
43
|
+
|
|
44
|
+
result = evaluate(
|
|
45
|
+
lambda _scenario: {"answer": 42},
|
|
46
|
+
api_key="trk_test",
|
|
47
|
+
scenario_id="scenario-1",
|
|
48
|
+
on_premise=True,
|
|
49
|
+
on_premise_url="http://localhost:3000",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert result.retry_state is not None
|
|
53
|
+
assert result.retry_state.cold_start_retry is True
|
|
54
|
+
assert result.retry_state.worker_available is True
|
|
55
|
+
assert result.cost_profile is not None
|
|
56
|
+
assert result.cost_profile.total_input_tokens == 10
|
|
57
|
+
assert result.cost_profile.avg_call_latency_ms == 25
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from truvyx.client import create_scenario_suite, evaluate_suite, update_scenario_suite
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
SUITE = {
|
|
5
|
+
"id": "suite-1",
|
|
6
|
+
"name": "Release",
|
|
7
|
+
"description": "release gates",
|
|
8
|
+
"version": 1,
|
|
9
|
+
"scenarios": [
|
|
10
|
+
{
|
|
11
|
+
"scenarioId": "scenario-1",
|
|
12
|
+
"scenarioVersion": 2,
|
|
13
|
+
"position": 0,
|
|
14
|
+
"critical": True,
|
|
15
|
+
"scenario": {
|
|
16
|
+
"id": "scenario-1",
|
|
17
|
+
"name": "Safety",
|
|
18
|
+
"description": "",
|
|
19
|
+
"domain": "TEST",
|
|
20
|
+
"difficulty": "HIGH",
|
|
21
|
+
"agentRoles": [],
|
|
22
|
+
"parameters": {},
|
|
23
|
+
"problemStatement": "",
|
|
24
|
+
"tags": [],
|
|
25
|
+
"constraintCount": 1,
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_python_suite_resource_and_evaluation_workflow(monkeypatch):
|
|
33
|
+
calls = []
|
|
34
|
+
|
|
35
|
+
def fake_request(url, *, headers, method="GET", body=None):
|
|
36
|
+
calls.append((url, headers, method, body))
|
|
37
|
+
if url.endswith("/api/v1/scenario-suites"):
|
|
38
|
+
return {"suite": SUITE}
|
|
39
|
+
if url.endswith("/api/v1/scenario-suites/suite-1") and method == "PATCH":
|
|
40
|
+
return {"suite": {**SUITE, "version": 2}}
|
|
41
|
+
if url.endswith("/api/v1/scenario-suites/suite-1"):
|
|
42
|
+
return {"suite": {**SUITE, "version": 2}}
|
|
43
|
+
if url.endswith("/api/v1/scenario-suites/suite-1/evaluate"):
|
|
44
|
+
assert headers["Idempotency-Key"] == "release-42"
|
|
45
|
+
assert body["outputs"][0]["scenarioId"] == "scenario-1"
|
|
46
|
+
return {"suiteRunId": "suite-run-1"}
|
|
47
|
+
if url.endswith("/api/v1/scenario-suite-runs/suite-run-1"):
|
|
48
|
+
return {
|
|
49
|
+
"ready": True,
|
|
50
|
+
"suiteRunId": "suite-run-1",
|
|
51
|
+
"suite": {"id": "suite-1", "name": "Release"},
|
|
52
|
+
"suiteVersion": 2,
|
|
53
|
+
"status": "PASSED",
|
|
54
|
+
"passed": True,
|
|
55
|
+
"overallScore": 1,
|
|
56
|
+
"threshold": 0.9,
|
|
57
|
+
"blockCritical": True,
|
|
58
|
+
"summary": {"total": 1, "completed": 1, "passed": 1, "failed": 0, "errors": 0, "criticalFailures": 0},
|
|
59
|
+
"runs": [],
|
|
60
|
+
}
|
|
61
|
+
raise AssertionError(f"unexpected URL {url}")
|
|
62
|
+
|
|
63
|
+
monkeypatch.setattr("truvyx.client._request", fake_request)
|
|
64
|
+
created = create_scenario_suite(api_key="trk_test", name="Release", scenarios=[{"scenarioId": "scenario-1", "critical": True}])
|
|
65
|
+
assert created.id == "suite-1"
|
|
66
|
+
updated = update_scenario_suite(api_key="trk_test", suite_id="suite-1", update={"description": "new"})
|
|
67
|
+
assert updated.version == 2
|
|
68
|
+
result = evaluate_suite(lambda scenario: {"scenario": scenario.id}, api_key="trk_test", suite_id="suite-1", fail_threshold=0.9, idempotency_key="release-42")
|
|
69
|
+
assert result.status == "PASSED"
|
|
70
|
+
assert sum(url.endswith("/evaluate") for url, *_ in calls) == 1
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from truvyx.trace import extract_trace, trace
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncTraceTests(unittest.IsolatedAsyncioTestCase):
|
|
8
|
+
async def test_coroutine_is_awaited_and_timing_includes_async_work(self):
|
|
9
|
+
@trace(agent_id="planner")
|
|
10
|
+
async def agent(value):
|
|
11
|
+
await asyncio.sleep(0.015)
|
|
12
|
+
return {"value": value}
|
|
13
|
+
|
|
14
|
+
self.assertEqual(await agent(3), {"value": 3})
|
|
15
|
+
steps = extract_trace(agent)
|
|
16
|
+
self.assertEqual(steps[0]["output"], {"value": 3})
|
|
17
|
+
self.assertGreaterEqual(steps[0]["durationMs"], 10)
|
|
18
|
+
|
|
19
|
+
async def test_exception_identity_and_trace_are_preserved(self):
|
|
20
|
+
failure = RuntimeError("agent failed")
|
|
21
|
+
|
|
22
|
+
@trace
|
|
23
|
+
async def agent(_value):
|
|
24
|
+
await asyncio.sleep(0)
|
|
25
|
+
raise failure
|
|
26
|
+
|
|
27
|
+
with self.assertRaises(RuntimeError) as caught:
|
|
28
|
+
await agent("input")
|
|
29
|
+
self.assertIs(caught.exception, failure)
|
|
30
|
+
self.assertEqual(extract_trace(agent)[0]["output"], None)
|
|
31
|
+
|
|
32
|
+
async def test_concurrent_calls_have_task_local_trace_state(self):
|
|
33
|
+
@trace
|
|
34
|
+
async def agent(value):
|
|
35
|
+
await asyncio.sleep(0.001 if value == "fast" else 0.01)
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
async def invoke(value):
|
|
39
|
+
self.assertEqual(await agent(value), value)
|
|
40
|
+
return extract_trace(agent)
|
|
41
|
+
|
|
42
|
+
fast, slow = await asyncio.gather(invoke("fast"), invoke("slow"))
|
|
43
|
+
self.assertEqual([step["input"] for step in fast], ["fast"])
|
|
44
|
+
self.assertEqual([step["input"] for step in slow], ["slow"])
|
|
45
|
+
self.assertEqual(fast[0]["step"], 0)
|
|
46
|
+
self.assertEqual(slow[0]["step"], 0)
|
|
47
|
+
|
|
48
|
+
async def test_cancellation_is_not_swallowed(self):
|
|
49
|
+
started = asyncio.Event()
|
|
50
|
+
|
|
51
|
+
@trace
|
|
52
|
+
async def agent(_value):
|
|
53
|
+
started.set()
|
|
54
|
+
await asyncio.sleep(10)
|
|
55
|
+
|
|
56
|
+
captured = []
|
|
57
|
+
|
|
58
|
+
async def invoke():
|
|
59
|
+
try:
|
|
60
|
+
await agent("cancelled")
|
|
61
|
+
finally:
|
|
62
|
+
captured.extend(extract_trace(agent))
|
|
63
|
+
|
|
64
|
+
task = asyncio.create_task(invoke())
|
|
65
|
+
await started.wait()
|
|
66
|
+
task.cancel()
|
|
67
|
+
with self.assertRaises(asyncio.CancelledError):
|
|
68
|
+
await task
|
|
69
|
+
self.assertEqual(captured[0]["input"], "cancelled")
|
|
70
|
+
|
|
71
|
+
async def test_recursive_calls_share_ordered_state_within_one_task(self):
|
|
72
|
+
@trace
|
|
73
|
+
async def agent(depth):
|
|
74
|
+
if depth:
|
|
75
|
+
await agent(depth - 1)
|
|
76
|
+
return depth
|
|
77
|
+
|
|
78
|
+
await agent(2)
|
|
79
|
+
steps = extract_trace(agent)
|
|
80
|
+
self.assertEqual([step["step"] for step in steps], [0, 1, 2])
|
|
81
|
+
self.assertEqual([step["input"] for step in steps], [0, 1, 2])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SyncTraceTests(unittest.TestCase):
|
|
85
|
+
def test_sync_behavior_and_extraction_reset_remain_compatible(self):
|
|
86
|
+
@trace(emit_to="organization")
|
|
87
|
+
def agent(value):
|
|
88
|
+
return value * 2
|
|
89
|
+
|
|
90
|
+
self.assertEqual(agent(4), 8)
|
|
91
|
+
self.assertEqual(extract_trace(agent)[0]["emitTo"], "organization")
|
|
92
|
+
self.assertEqual(extract_trace(agent), [])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
unittest.main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
evaluate,
|
|
3
|
+
evaluate_suite,
|
|
4
|
+
create_scenario_suite,
|
|
5
|
+
get_scenario_suite,
|
|
6
|
+
update_scenario_suite,
|
|
7
|
+
assert_feasibility,
|
|
8
|
+
EvaluationError,
|
|
9
|
+
ScenarioSuiteEvaluationError,
|
|
10
|
+
TruvyxError,
|
|
11
|
+
)
|
|
12
|
+
from .trace import trace, extract_trace
|
|
13
|
+
from .types import EvalConfig, EvalResult, Violation, AuditTrail, ScenarioInput, ScenarioSuite, ScenarioSuiteMember, SuiteEvalResult, SuiteRunItem
|
|
14
|
+
|
|
15
|
+
__version__ = "0.4.0"
|
|
16
|
+
__all__ = [
|
|
17
|
+
"evaluate",
|
|
18
|
+
"evaluate_suite",
|
|
19
|
+
"create_scenario_suite",
|
|
20
|
+
"get_scenario_suite",
|
|
21
|
+
"update_scenario_suite",
|
|
22
|
+
"assert_feasibility",
|
|
23
|
+
"EvaluationError",
|
|
24
|
+
"ScenarioSuiteEvaluationError",
|
|
25
|
+
"TruvyxError",
|
|
26
|
+
"trace",
|
|
27
|
+
"extract_trace",
|
|
28
|
+
"EvalConfig",
|
|
29
|
+
"EvalResult",
|
|
30
|
+
"Violation",
|
|
31
|
+
"AuditTrail",
|
|
32
|
+
"ScenarioInput",
|
|
33
|
+
"ScenarioSuite",
|
|
34
|
+
"ScenarioSuiteMember",
|
|
35
|
+
"SuiteEvalResult",
|
|
36
|
+
"SuiteRunItem",
|
|
37
|
+
]
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import time
|
|
3
|
+
import urllib.request
|
|
4
|
+
import urllib.error
|
|
5
|
+
import json
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
EvalResult,
|
|
11
|
+
ScenarioInput,
|
|
12
|
+
Violation,
|
|
13
|
+
AuditTrail,
|
|
14
|
+
ConstraintResult,
|
|
15
|
+
CostProfile,
|
|
16
|
+
RetryState,
|
|
17
|
+
ScenarioSuite,
|
|
18
|
+
ScenarioSuiteMember,
|
|
19
|
+
SuiteEvalResult,
|
|
20
|
+
SuiteRunItem,
|
|
21
|
+
)
|
|
22
|
+
from .trace import extract_trace, trace
|
|
23
|
+
|
|
24
|
+
SDK_VERSION = "0.4.0"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TruvyxError(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EvaluationError(TruvyxError):
|
|
32
|
+
def __init__(self, message: str, result: EvalResult, code: str):
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.result = result
|
|
35
|
+
self.code = code
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ScenarioSuiteEvaluationError(TruvyxError):
|
|
39
|
+
def __init__(self, message: str, result: SuiteEvalResult | None, code: str):
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.result = result
|
|
42
|
+
self.code = code
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _request(url: str, *, headers: dict[str, str], method: str = "GET", body: Any = None) -> Any:
|
|
46
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
47
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
48
|
+
try:
|
|
49
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
50
|
+
return json.loads(resp.read())
|
|
51
|
+
except urllib.error.HTTPError as e:
|
|
52
|
+
raise TruvyxError(f"HTTP {e.code}: {e.read().decode()}") from e
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _cost_profile(data: Any) -> CostProfile | None:
|
|
56
|
+
if not isinstance(data, dict):
|
|
57
|
+
return None
|
|
58
|
+
return CostProfile(
|
|
59
|
+
total_input_tokens=int(data.get("totalInputTokens", 0)),
|
|
60
|
+
total_output_tokens=int(data.get("totalOutputTokens", 0)),
|
|
61
|
+
total_tokens=int(data.get("totalTokens", 0)),
|
|
62
|
+
estimated_cost_usd=float(data.get("estimatedCostUsd", 0)),
|
|
63
|
+
api_call_count=int(data.get("apiCallCount", 0)),
|
|
64
|
+
tool_call_count=int(data.get("toolCallCount", 0)),
|
|
65
|
+
redundant_call_count=int(data.get("redundantCallCount", 0)),
|
|
66
|
+
avg_call_latency_ms=data.get("avgCallLatencyMs"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _retry_state(data: Any) -> RetryState | None:
|
|
71
|
+
if not isinstance(data, dict):
|
|
72
|
+
return None
|
|
73
|
+
return RetryState(
|
|
74
|
+
attempts=int(data.get("attempts", 0)),
|
|
75
|
+
cold_start_retry=bool(data.get("coldStartRetry", False)),
|
|
76
|
+
worker_available=bool(data.get("workerAvailable", False)),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def evaluate(
|
|
81
|
+
agent_fn: Callable[[ScenarioInput], Any],
|
|
82
|
+
*,
|
|
83
|
+
api_key: str,
|
|
84
|
+
scenario_id: str,
|
|
85
|
+
agent_system_name: str = "sdk-agent",
|
|
86
|
+
compliance_mode: bool = False,
|
|
87
|
+
on_premise: bool = False,
|
|
88
|
+
on_premise_url: str | None = None,
|
|
89
|
+
trace_capture: bool = False,
|
|
90
|
+
trace_visibility: str = "private",
|
|
91
|
+
timeout: int = 60,
|
|
92
|
+
) -> EvalResult:
|
|
93
|
+
if trace_visibility not in {"private", "organization", "fault_atlas"}:
|
|
94
|
+
raise ValueError("trace_visibility must be 'private', 'organization', or 'fault_atlas'")
|
|
95
|
+
base_url = (on_premise_url or "") if on_premise else "https://truvyx.org"
|
|
96
|
+
headers = {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
"X-Truvyx-Key": api_key,
|
|
99
|
+
"User-Agent": f"truvyx-python/{SDK_VERSION}",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# 1. Fetch the scenario directly. This supports private organisation-owned
|
|
103
|
+
# scenarios as well as public Registry scenarios.
|
|
104
|
+
scen_data = _request(f"{base_url}/api/v1/scenarios/{scenario_id}", headers=headers)
|
|
105
|
+
s = scen_data.get("scenario")
|
|
106
|
+
if s is None:
|
|
107
|
+
raise TruvyxError(f"Scenario {scenario_id} not found")
|
|
108
|
+
scenario_input = ScenarioInput(
|
|
109
|
+
id=s["id"],
|
|
110
|
+
name=s["name"],
|
|
111
|
+
description=s.get("description", ""),
|
|
112
|
+
domain=s["domain"],
|
|
113
|
+
difficulty=s["difficulty"],
|
|
114
|
+
agent_roles=s.get("agentRoles", []),
|
|
115
|
+
parameters=s.get("parameters", {}),
|
|
116
|
+
problem_statement=s.get("problemStatement", ""),
|
|
117
|
+
tags=s.get("tags", []),
|
|
118
|
+
constraint_count=len(s.get("constraints", [])),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# 2. Run agent
|
|
122
|
+
start = time.time()
|
|
123
|
+
agent_output = agent_fn(scenario_input)
|
|
124
|
+
execution_time_ms = int((time.time() - start) * 1000)
|
|
125
|
+
execution_trace = extract_trace(agent_fn) if trace_capture else None
|
|
126
|
+
|
|
127
|
+
# 3. Submit
|
|
128
|
+
submit_resp = _request(
|
|
129
|
+
f"{base_url}/api/v1/evaluate",
|
|
130
|
+
headers=headers,
|
|
131
|
+
method="POST",
|
|
132
|
+
body={
|
|
133
|
+
"scenarioId": scenario_id,
|
|
134
|
+
"agentSystemName": agent_system_name,
|
|
135
|
+
"agentOutput": agent_output,
|
|
136
|
+
"executionTrace": execution_trace,
|
|
137
|
+
"traceVisibility": trace_visibility,
|
|
138
|
+
"executionTimeMs": execution_time_ms,
|
|
139
|
+
"environment": {
|
|
140
|
+
"sdk": SDK_VERSION,
|
|
141
|
+
"complianceMode": compliance_mode,
|
|
142
|
+
"traceCapture": trace_capture,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
run_id = submit_resp["runId"]
|
|
147
|
+
|
|
148
|
+
# 4. Poll for result
|
|
149
|
+
deadline = time.time() + timeout
|
|
150
|
+
while time.time() < deadline:
|
|
151
|
+
time.sleep(2)
|
|
152
|
+
try:
|
|
153
|
+
data = _request(f"{base_url}/api/v1/runs/{run_id}/result", headers=headers)
|
|
154
|
+
except TruvyxError:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
if not data.get("ready"):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
violations = [
|
|
161
|
+
Violation(
|
|
162
|
+
constraint_id=v["constraintId"],
|
|
163
|
+
constraint_name=v["constraintName"],
|
|
164
|
+
category=v["category"],
|
|
165
|
+
severity=v["severity"],
|
|
166
|
+
description=v["description"],
|
|
167
|
+
agent_value=v["agentValue"],
|
|
168
|
+
required_bound=v["requiredBound"],
|
|
169
|
+
)
|
|
170
|
+
for v in data.get("violations", [])
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
audit_trail = None
|
|
174
|
+
if compliance_mode and data.get("auditRecordId"):
|
|
175
|
+
audit_status = data.get("auditRecordStatus", "UNSIGNED")
|
|
176
|
+
audit_trail = AuditTrail(
|
|
177
|
+
id=data["auditRecordId"],
|
|
178
|
+
status=audit_status if audit_status in {"UNSIGNED", "SIGNED", "VERIFIED", "INVALID"} else "INVALID",
|
|
179
|
+
verified_at=data.get("auditRecordVerifiedAt"),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
constraint_results = [
|
|
183
|
+
ConstraintResult(
|
|
184
|
+
constraint_id=item.get("constraintId", ""),
|
|
185
|
+
constraint_name=item.get("constraintName", ""),
|
|
186
|
+
status=item.get("status", "NOT_SCORED"),
|
|
187
|
+
score=item.get("score"),
|
|
188
|
+
evidence=item.get("evidence"),
|
|
189
|
+
)
|
|
190
|
+
for item in data.get("constraintResults", [])
|
|
191
|
+
]
|
|
192
|
+
cost_profile = _cost_profile(data.get("costProfile"))
|
|
193
|
+
retry_state = _retry_state(data.get("retryState"))
|
|
194
|
+
|
|
195
|
+
return EvalResult(
|
|
196
|
+
run_id=data.get("runId", run_id),
|
|
197
|
+
scenario_id=data.get("scenarioId", scenario_id),
|
|
198
|
+
status=data.get("status", "ERROR"),
|
|
199
|
+
feasibility_score=data.get("feasibilityScore", 0.0),
|
|
200
|
+
completeness_score=data.get("completenessScore", 0.0),
|
|
201
|
+
optimality_score=data.get("optimalityScore", 0.0),
|
|
202
|
+
overall_score=data.get("overallScore", 0.0),
|
|
203
|
+
passed=data.get("passed", False),
|
|
204
|
+
violations=violations,
|
|
205
|
+
constraint_results=constraint_results,
|
|
206
|
+
completeness_gaps=data.get("completenessGaps", []),
|
|
207
|
+
audit_trail=audit_trail,
|
|
208
|
+
cost_profile=cost_profile,
|
|
209
|
+
retry_state=retry_state,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
empty = EvalResult(
|
|
213
|
+
run_id=run_id, scenario_id=scenario_id, status="ERROR",
|
|
214
|
+
feasibility_score=0, completeness_score=0, optimality_score=0,
|
|
215
|
+
overall_score=0, passed=False,
|
|
216
|
+
)
|
|
217
|
+
raise EvaluationError(f"Evaluation timed out after {timeout}s. Run ID: {run_id}", empty, "TIMEOUT")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def assert_feasibility(
|
|
221
|
+
result: EvalResult,
|
|
222
|
+
*,
|
|
223
|
+
threshold: float = 0.7,
|
|
224
|
+
fail_on_regulatory: bool = False,
|
|
225
|
+
) -> None:
|
|
226
|
+
if fail_on_regulatory:
|
|
227
|
+
regulatory = [
|
|
228
|
+
v for v in result.violations
|
|
229
|
+
if v.category == "REGULATORY" and v.severity in ("CRITICAL", "HIGH")
|
|
230
|
+
]
|
|
231
|
+
if regulatory:
|
|
232
|
+
names = ", ".join(v.constraint_name for v in regulatory)
|
|
233
|
+
raise EvaluationError(
|
|
234
|
+
f"Regulatory violation(s): {names}",
|
|
235
|
+
result,
|
|
236
|
+
"REGULATORY_VIOLATION",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if result.overall_score < threshold:
|
|
240
|
+
raise EvaluationError(
|
|
241
|
+
f"Score {result.overall_score:.3f} < threshold {threshold}. "
|
|
242
|
+
f"Feasibility={result.feasibility_score:.3f} "
|
|
243
|
+
f"Completeness={result.completeness_score:.3f} "
|
|
244
|
+
f"Optimality={result.optimality_score:.3f} "
|
|
245
|
+
f"Violations={len(result.violations)}",
|
|
246
|
+
result,
|
|
247
|
+
"THRESHOLD_NOT_MET",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _base_url(on_premise: bool, on_premise_url: str | None) -> str:
|
|
252
|
+
if on_premise:
|
|
253
|
+
if not on_premise_url:
|
|
254
|
+
raise ValueError("on_premise_url is required when on_premise=True")
|
|
255
|
+
return on_premise_url.rstrip("/")
|
|
256
|
+
if on_premise_url is not None:
|
|
257
|
+
raise ValueError("on_premise_url requires on_premise=True")
|
|
258
|
+
return "https://truvyx.org"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _suite_headers(api_key: str, idempotency_key: str | None = None) -> dict[str, str]:
|
|
262
|
+
if not api_key.strip():
|
|
263
|
+
raise ValueError("api_key is required")
|
|
264
|
+
headers = {"Content-Type": "application/json", "X-Truvyx-Key": api_key, "User-Agent": f"truvyx-python/{SDK_VERSION}"}
|
|
265
|
+
if idempotency_key is not None:
|
|
266
|
+
if not idempotency_key.strip() or len(idempotency_key) > 200:
|
|
267
|
+
raise ValueError("idempotency_key must contain 1-200 characters")
|
|
268
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
269
|
+
return headers
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _scenario_input(data: dict[str, Any]) -> ScenarioInput:
|
|
273
|
+
return ScenarioInput(
|
|
274
|
+
id=data["id"], name=data["name"], description=data.get("description", ""),
|
|
275
|
+
domain=data["domain"], difficulty=data["difficulty"], agent_roles=data.get("agentRoles", []),
|
|
276
|
+
parameters=data.get("parameters", {}), problem_statement=data.get("problemStatement", ""),
|
|
277
|
+
tags=data.get("tags", []), constraint_count=data.get("constraintCount", data.get("_count", {}).get("constraints", len(data.get("constraints", [])))),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _parse_suite(data: dict[str, Any]) -> ScenarioSuite:
|
|
282
|
+
return ScenarioSuite(
|
|
283
|
+
id=data["id"], name=data["name"], description=data.get("description", ""), version=data["version"],
|
|
284
|
+
scenarios=[ScenarioSuiteMember(
|
|
285
|
+
scenario_id=item["scenarioId"], scenario_version=item["scenarioVersion"], position=item["position"],
|
|
286
|
+
critical=item.get("critical", False), scenario=_scenario_input(item["scenario"]),
|
|
287
|
+
) for item in data.get("scenarios", [])],
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def create_scenario_suite(
|
|
292
|
+
*, api_key: str, name: str, scenarios: list[dict[str, Any]], description: str = "",
|
|
293
|
+
on_premise: bool = False, on_premise_url: str | None = None,
|
|
294
|
+
) -> ScenarioSuite:
|
|
295
|
+
data = _request(f"{_base_url(on_premise, on_premise_url)}/api/v1/scenario-suites", headers=_suite_headers(api_key), method="POST", body={"name": name, "description": description, "scenarios": scenarios})
|
|
296
|
+
return _parse_suite(data["suite"])
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_scenario_suite(
|
|
300
|
+
*, api_key: str, suite_id: str, on_premise: bool = False, on_premise_url: str | None = None,
|
|
301
|
+
) -> ScenarioSuite:
|
|
302
|
+
if not suite_id.strip():
|
|
303
|
+
raise ValueError("suite_id is required")
|
|
304
|
+
data = _request(f"{_base_url(on_premise, on_premise_url)}/api/v1/scenario-suites/{suite_id}", headers=_suite_headers(api_key))
|
|
305
|
+
return _parse_suite(data["suite"])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def update_scenario_suite(
|
|
309
|
+
*, api_key: str, suite_id: str, update: dict[str, Any], on_premise: bool = False, on_premise_url: str | None = None,
|
|
310
|
+
) -> ScenarioSuite:
|
|
311
|
+
if not update:
|
|
312
|
+
raise ValueError("at least one suite field is required")
|
|
313
|
+
data = _request(f"{_base_url(on_premise, on_premise_url)}/api/v1/scenario-suites/{suite_id}", headers=_suite_headers(api_key), method="PATCH", body=update)
|
|
314
|
+
return _parse_suite(data["suite"])
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _parse_suite_result(data: dict[str, Any]) -> SuiteEvalResult:
|
|
318
|
+
suite = data.get("suite", {})
|
|
319
|
+
return SuiteEvalResult(
|
|
320
|
+
ready=data.get("ready", False), suite_run_id=data["suiteRunId"], suite_id=suite.get("id", ""), suite_name=suite.get("name", ""),
|
|
321
|
+
suite_version=data.get("suiteVersion", 0), status=data.get("status", "ERROR"), passed=data.get("passed", False),
|
|
322
|
+
overall_score=data.get("overallScore"), threshold=data.get("threshold", 0.7), block_critical=data.get("blockCritical", True),
|
|
323
|
+
summary=data.get("summary", {}), runs=[SuiteRunItem(
|
|
324
|
+
run_id=item["runId"], scenario_id=item["scenarioId"], scenario_name=item.get("scenarioName", ""),
|
|
325
|
+
scenario_version=item.get("scenarioVersion", 0), critical=item.get("critical", False), status=item.get("status", "ERROR"),
|
|
326
|
+
overall_score=item.get("overallScore"), violation_count=item.get("violationCount", 0), completed_at=item.get("completedAt"), error=item.get("error"),
|
|
327
|
+
) for item in data.get("runs", [])], created_at=data.get("createdAt"), completed_at=data.get("completedAt"),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def evaluate_suite(
|
|
332
|
+
agent_fn: Callable[[ScenarioInput], Any], *, api_key: str, suite_id: str, agent_system_name: str = "sdk-agent",
|
|
333
|
+
fail_threshold: float = 0.7, block_critical: bool = True, max_concurrency: int = 5, timeout: int = 60,
|
|
334
|
+
on_premise: bool = False, on_premise_url: str | None = None, trace_capture: bool = False,
|
|
335
|
+
trace_visibility: str = "private", environment: dict[str, Any] | None = None, idempotency_key: str | None = None,
|
|
336
|
+
) -> SuiteEvalResult:
|
|
337
|
+
if not 0 <= fail_threshold <= 1:
|
|
338
|
+
raise ValueError("fail_threshold must be between 0 and 1")
|
|
339
|
+
if not 1 <= max_concurrency <= 10:
|
|
340
|
+
raise ValueError("max_concurrency must be between 1 and 10")
|
|
341
|
+
if timeout < 1:
|
|
342
|
+
raise ValueError("timeout must be at least 1 second")
|
|
343
|
+
if trace_visibility not in {"private", "organization", "fault_atlas"}:
|
|
344
|
+
raise ValueError("trace_visibility is invalid")
|
|
345
|
+
base_url = _base_url(on_premise, on_premise_url)
|
|
346
|
+
suite = get_scenario_suite(api_key=api_key, suite_id=suite_id, on_premise=on_premise, on_premise_url=on_premise_url)
|
|
347
|
+
|
|
348
|
+
def execute(member: ScenarioSuiteMember) -> dict[str, Any]:
|
|
349
|
+
target = trace(emit_to=trace_visibility, agent_id=agent_system_name)(agent_fn) if trace_capture else agent_fn
|
|
350
|
+
started = time.time()
|
|
351
|
+
output = target(member.scenario)
|
|
352
|
+
return {"scenarioId": member.scenario_id, "agentOutput": output, "executionTimeMs": int((time.time() - started) * 1000), "executionTrace": extract_trace(target) if trace_capture else None, "traceVisibility": trace_visibility}
|
|
353
|
+
|
|
354
|
+
with ThreadPoolExecutor(max_workers=max_concurrency) as executor:
|
|
355
|
+
outputs = list(executor.map(execute, suite.scenarios))
|
|
356
|
+
submission = _request(
|
|
357
|
+
f"{base_url}/api/v1/scenario-suites/{suite_id}/evaluate", headers=_suite_headers(api_key, idempotency_key), method="POST",
|
|
358
|
+
body={"agentSystemName": agent_system_name, "outputs": outputs, "failThreshold": fail_threshold, "blockCritical": block_critical, "maxConcurrency": max_concurrency, "environment": {**(environment or {}), "sdk": SDK_VERSION}},
|
|
359
|
+
)
|
|
360
|
+
deadline = time.time() + timeout
|
|
361
|
+
latest: SuiteEvalResult | None = None
|
|
362
|
+
while time.time() < deadline:
|
|
363
|
+
data = _request(f"{base_url}/api/v1/scenario-suite-runs/{submission['suiteRunId']}", headers=_suite_headers(api_key))
|
|
364
|
+
latest = _parse_suite_result(data)
|
|
365
|
+
if latest.ready:
|
|
366
|
+
if not latest.passed:
|
|
367
|
+
raise ScenarioSuiteEvaluationError(f"Scenario suite {suite_id} finished with status {latest.status}", latest, "SUITE_FAILED")
|
|
368
|
+
return latest
|
|
369
|
+
time.sleep(2)
|
|
370
|
+
raise ScenarioSuiteEvaluationError(f"Scenario suite evaluation timed out after {timeout}s. Suite run ID: {submission['suiteRunId']}", latest, "TIMEOUT")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
import time
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any, Callable, Optional
|
|
8
|
+
|
|
9
|
+
_trace_contexts: dict[int, ContextVar[list[dict[str, Any]] | None]] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def trace(
|
|
13
|
+
fn: Optional[Callable] = None,
|
|
14
|
+
*,
|
|
15
|
+
emit_to: str = "private",
|
|
16
|
+
agent_id: Optional[str] = None,
|
|
17
|
+
) -> Callable:
|
|
18
|
+
"""Decorator that captures execution trace without changing function signature.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
@trace
|
|
22
|
+
def my_agent(input_data): ...
|
|
23
|
+
|
|
24
|
+
@trace(emit_to="private", agent_id="planner")
|
|
25
|
+
def my_agent(input_data): ...
|
|
26
|
+
"""
|
|
27
|
+
if emit_to not in {"private", "organization", "fault_atlas"}:
|
|
28
|
+
raise ValueError("emit_to must be 'private', 'organization', or 'fault_atlas'")
|
|
29
|
+
|
|
30
|
+
def decorator(func: Callable) -> Callable:
|
|
31
|
+
fn_id = id(func)
|
|
32
|
+
effective_agent_id = agent_id or func.__name__
|
|
33
|
+
|
|
34
|
+
trace_context: ContextVar[list[dict[str, Any]] | None] = ContextVar(
|
|
35
|
+
f"truvyx_trace_{fn_id}", default=None
|
|
36
|
+
)
|
|
37
|
+
_trace_contexts[fn_id] = trace_context
|
|
38
|
+
|
|
39
|
+
def begin() -> tuple[list[dict[str, Any]], float]:
|
|
40
|
+
steps = trace_context.get()
|
|
41
|
+
if steps is None:
|
|
42
|
+
steps = []
|
|
43
|
+
trace_context.set(steps)
|
|
44
|
+
return steps, time.perf_counter()
|
|
45
|
+
|
|
46
|
+
def finish(steps: list[dict[str, Any]], start: float, args: tuple, kwargs: dict, output: Any) -> None:
|
|
47
|
+
steps.append({
|
|
48
|
+
"agentId": effective_agent_id,
|
|
49
|
+
"step": len(steps),
|
|
50
|
+
"action": "execute",
|
|
51
|
+
"input": args[0] if args else kwargs,
|
|
52
|
+
"output": output,
|
|
53
|
+
"durationMs": max(0, int((time.perf_counter() - start) * 1000)),
|
|
54
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
55
|
+
"emitTo": emit_to,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if inspect.iscoroutinefunction(func):
|
|
59
|
+
@functools.wraps(func)
|
|
60
|
+
async def async_wrapper(*args, **kwargs):
|
|
61
|
+
steps, start = begin()
|
|
62
|
+
result: Any = None
|
|
63
|
+
try:
|
|
64
|
+
result = await func(*args, **kwargs)
|
|
65
|
+
return result
|
|
66
|
+
finally:
|
|
67
|
+
finish(steps, start, args, kwargs, result)
|
|
68
|
+
|
|
69
|
+
setattr(async_wrapper, "__trace_fn_id__", fn_id)
|
|
70
|
+
return async_wrapper
|
|
71
|
+
|
|
72
|
+
@functools.wraps(func)
|
|
73
|
+
def wrapper(*args, **kwargs):
|
|
74
|
+
steps, start = begin()
|
|
75
|
+
result: Any = None
|
|
76
|
+
try:
|
|
77
|
+
result = func(*args, **kwargs)
|
|
78
|
+
return result
|
|
79
|
+
finally:
|
|
80
|
+
finish(steps, start, args, kwargs, result)
|
|
81
|
+
|
|
82
|
+
setattr(wrapper, "__wrapped__", func)
|
|
83
|
+
setattr(wrapper, "__trace_fn_id__", fn_id)
|
|
84
|
+
return wrapper
|
|
85
|
+
|
|
86
|
+
if fn is not None:
|
|
87
|
+
return decorator(fn)
|
|
88
|
+
return decorator
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def extract_trace(fn: Callable) -> list[dict[str, Any]]:
|
|
92
|
+
fn_id = getattr(fn, "__trace_fn_id__", None) or id(fn)
|
|
93
|
+
context = _trace_contexts.get(fn_id)
|
|
94
|
+
if context is None:
|
|
95
|
+
return []
|
|
96
|
+
steps = context.get() or []
|
|
97
|
+
context.set(None)
|
|
98
|
+
return list(steps)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class EvalConfig:
|
|
8
|
+
api_key: str
|
|
9
|
+
scenario_id: Optional[str] = None
|
|
10
|
+
compliance_mode: bool = False
|
|
11
|
+
on_premise: bool = False
|
|
12
|
+
on_premise_url: Optional[str] = None
|
|
13
|
+
trace_capture: bool = False
|
|
14
|
+
trace_visibility: str = "private"
|
|
15
|
+
agent_system_name: str = "sdk-agent"
|
|
16
|
+
timeout: int = 60
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Violation:
|
|
21
|
+
constraint_id: str
|
|
22
|
+
constraint_name: str
|
|
23
|
+
category: str
|
|
24
|
+
severity: str
|
|
25
|
+
description: str
|
|
26
|
+
agent_value: Any
|
|
27
|
+
required_bound: Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ConstraintResult:
|
|
32
|
+
constraint_id: str
|
|
33
|
+
constraint_name: str
|
|
34
|
+
status: str
|
|
35
|
+
score: Optional[float] = None
|
|
36
|
+
evidence: Any = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class CostProfile:
|
|
41
|
+
total_input_tokens: int
|
|
42
|
+
total_output_tokens: int
|
|
43
|
+
total_tokens: int
|
|
44
|
+
estimated_cost_usd: float
|
|
45
|
+
api_call_count: int
|
|
46
|
+
tool_call_count: int
|
|
47
|
+
redundant_call_count: int = 0
|
|
48
|
+
avg_call_latency_ms: Optional[int] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class RetryState:
|
|
53
|
+
attempts: int
|
|
54
|
+
cold_start_retry: bool
|
|
55
|
+
worker_available: bool
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class AuditTrail:
|
|
60
|
+
id: str
|
|
61
|
+
status: str
|
|
62
|
+
verified_at: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class EvalResult:
|
|
67
|
+
run_id: str
|
|
68
|
+
scenario_id: str
|
|
69
|
+
status: str
|
|
70
|
+
feasibility_score: float
|
|
71
|
+
completeness_score: float
|
|
72
|
+
optimality_score: float
|
|
73
|
+
overall_score: float
|
|
74
|
+
passed: bool
|
|
75
|
+
violations: list[Violation] = field(default_factory=list)
|
|
76
|
+
constraint_results: list[ConstraintResult] = field(default_factory=list)
|
|
77
|
+
completeness_gaps: list[str] = field(default_factory=list)
|
|
78
|
+
cost_profile: Optional[CostProfile] = None
|
|
79
|
+
retry_state: Optional[RetryState] = None
|
|
80
|
+
audit_trail: Optional[AuditTrail] = None
|
|
81
|
+
rca_report_id: Optional[str] = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class ScenarioInput:
|
|
86
|
+
id: str
|
|
87
|
+
name: str
|
|
88
|
+
description: str
|
|
89
|
+
domain: str
|
|
90
|
+
difficulty: str
|
|
91
|
+
agent_roles: list[Any]
|
|
92
|
+
parameters: dict[str, Any]
|
|
93
|
+
problem_statement: str
|
|
94
|
+
tags: list[str]
|
|
95
|
+
constraint_count: int
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class ScenarioSuiteMember:
|
|
100
|
+
scenario_id: str
|
|
101
|
+
scenario_version: int
|
|
102
|
+
position: int
|
|
103
|
+
critical: bool
|
|
104
|
+
scenario: ScenarioInput
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class ScenarioSuite:
|
|
109
|
+
id: str
|
|
110
|
+
name: str
|
|
111
|
+
description: str
|
|
112
|
+
version: int
|
|
113
|
+
scenarios: list[ScenarioSuiteMember] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class SuiteRunItem:
|
|
118
|
+
run_id: str
|
|
119
|
+
scenario_id: str
|
|
120
|
+
scenario_name: str
|
|
121
|
+
scenario_version: int
|
|
122
|
+
critical: bool
|
|
123
|
+
status: str
|
|
124
|
+
overall_score: Optional[float]
|
|
125
|
+
violation_count: int
|
|
126
|
+
completed_at: Optional[str]
|
|
127
|
+
error: Optional[str]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class SuiteEvalResult:
|
|
132
|
+
ready: bool
|
|
133
|
+
suite_run_id: str
|
|
134
|
+
suite_id: str
|
|
135
|
+
suite_name: str
|
|
136
|
+
suite_version: int
|
|
137
|
+
status: str
|
|
138
|
+
passed: bool
|
|
139
|
+
overall_score: Optional[float]
|
|
140
|
+
threshold: float
|
|
141
|
+
block_critical: bool
|
|
142
|
+
summary: dict[str, int]
|
|
143
|
+
runs: list[SuiteRunItem] = field(default_factory=list)
|
|
144
|
+
created_at: Optional[str] = None
|
|
145
|
+
completed_at: Optional[str] = None
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: truvyx
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Truvyx evaluation SDK — embed evaluation into any agent pipeline
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://truvyx.org
|
|
7
|
+
Project-URL: Documentation, https://truvyx.org/docs/sdk
|
|
8
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
tests/test_evaluate.py
|
|
3
|
+
tests/test_suites.py
|
|
4
|
+
tests/test_trace.py
|
|
5
|
+
truvyx/__init__.py
|
|
6
|
+
truvyx/client.py
|
|
7
|
+
truvyx/trace.py
|
|
8
|
+
truvyx/types.py
|
|
9
|
+
truvyx.egg-info/PKG-INFO
|
|
10
|
+
truvyx.egg-info/SOURCES.txt
|
|
11
|
+
truvyx.egg-info/dependency_links.txt
|
|
12
|
+
truvyx.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
truvyx
|