applexus-iris-evals 1.0.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.
- applexus_iris_evals-1.0.0.dist-info/METADATA +185 -0
- applexus_iris_evals-1.0.0.dist-info/RECORD +19 -0
- applexus_iris_evals-1.0.0.dist-info/WHEEL +5 -0
- applexus_iris_evals-1.0.0.dist-info/top_level.txt +1 -0
- iris_evals/__init__.py +64 -0
- iris_evals/_transport.py +80 -0
- iris_evals/aicore.py +118 -0
- iris_evals/evaluators/__init__.py +17 -0
- iris_evals/evaluators/deterministic.py +66 -0
- iris_evals/evaluators/llm_judge.py +145 -0
- iris_evals/evaluators/performance.py +91 -0
- iris_evals/evaluators/probabilistic.py +109 -0
- iris_evals/evaluators/safety.py +111 -0
- iris_evals/evaluators/trace.py +108 -0
- iris_evals/registry.py +281 -0
- iris_evals/results.py +82 -0
- iris_evals/runner.py +111 -0
- iris_evals/schemas.py +134 -0
- iris_evals/self_eval.py +285 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: applexus-iris-evals
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Reusable evaluation framework for IRIS SAP Joule A2A agents
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.27.0
|
|
8
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
9
|
+
Requires-Dist: rich>=13.0.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: anthropic>=0.30.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# iris-evals
|
|
17
|
+
|
|
18
|
+
Self-evaluation library for Applexus IRIS SAP Joule agents.
|
|
19
|
+
|
|
20
|
+
Install once — every agent evaluates itself on startup, runs a background loop every 5 minutes, and exposes a `/health/evals` CI gate endpoint. All golden datasets, rubrics, and thresholds are bundled inside the library. Zero eval data files needed per agent.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install git+https://github.com/applexus/iris-evals.git@v1.0.0
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add to your agent's `requirements.txt`:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
iris-evals @ git+https://github.com/applexus/iris-evals.git@v1.0.0
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
Add three blocks to your agent's `app.py`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio, os
|
|
44
|
+
from iris_evals import run_self_eval, self_eval_loop
|
|
45
|
+
|
|
46
|
+
AGENT_URL = os.getenv("AGENT_URL", "http://localhost:8080")
|
|
47
|
+
|
|
48
|
+
@app.on_event("startup")
|
|
49
|
+
async def startup_self_eval():
|
|
50
|
+
report = await run_self_eval(agent_url=AGENT_URL)
|
|
51
|
+
if not report.overall_passed:
|
|
52
|
+
logger.warning("[IRIS EVALS] Self-eval FAILED — %s", report.summary)
|
|
53
|
+
|
|
54
|
+
@app.on_event("startup")
|
|
55
|
+
async def start_eval_loop():
|
|
56
|
+
asyncio.create_task(
|
|
57
|
+
self_eval_loop(agent_url=AGENT_URL, interval_seconds=300)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@app.route("/health/evals", methods=["GET"])
|
|
61
|
+
async def eval_health(request):
|
|
62
|
+
from starlette.responses import JSONResponse
|
|
63
|
+
report = await run_self_eval(agent_url=AGENT_URL)
|
|
64
|
+
return JSONResponse({"passed": report.overall_passed, "summary": report.summary})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Add to your agent's `agent-card.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
"x_evals": {
|
|
71
|
+
"evaluators": ["deterministic", "safety"],
|
|
72
|
+
"dataset": "builtin",
|
|
73
|
+
"threshold_profile": "deterministic_strict"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 7 Eval Types
|
|
80
|
+
|
|
81
|
+
| # | Type | Needs AI | Use when |
|
|
82
|
+
|---|------|----------|----------|
|
|
83
|
+
| 1 | `deterministic` | No | Rule-based agents — exact verdict match |
|
|
84
|
+
| 2 | `probabilistic` | No | Classifiers — accuracy / F1 ≥ 85% |
|
|
85
|
+
| 3 | `llm_as_judge` | Yes (AI Core) | Free-text output quality — rubric scored |
|
|
86
|
+
| 4 | `agent_as_judge` | Yes (A2A) | Complex SAP domain — peer agent reviews |
|
|
87
|
+
| 5 | `trace` | No | Multi-step agents — correct tool call sequence |
|
|
88
|
+
| 6 | `safety` | No | Every agent — PII + injection checks |
|
|
89
|
+
| 7 | `performance` | No | SLA-critical — latency p95 ≤ 5000ms |
|
|
90
|
+
|
|
91
|
+
**Quick picks:**
|
|
92
|
+
- Rule-based agent → `1, 6`
|
|
93
|
+
- Triage / classifier → `2, 3, 6`
|
|
94
|
+
- Multi-step → `1, 5, 6`
|
|
95
|
+
- SLA-critical → `1, 5, 6, 7`
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## How it works
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
cf push
|
|
103
|
+
└─ pip installs iris-evals from GitHub
|
|
104
|
+
└─ startup: run_self_eval() calls agent via A2A with golden records
|
|
105
|
+
└─ CI polls GET /health/evals → passed: true → promote
|
|
106
|
+
└─ background: self_eval_loop() re-runs every 5 minutes
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
iris-evals runs **inside your agent's process** as an asyncio task — no separate BTP service, no extra cost.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## LLM Judge (type 3)
|
|
114
|
+
|
|
115
|
+
Uses SAP AI Core orchestration. Requires these env vars in your agent's `manifest.yml` (gitignored):
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
AICORE_CLIENT_ID
|
|
119
|
+
AICORE_CLIENT_SECRET
|
|
120
|
+
AICORE_AUTH_URL
|
|
121
|
+
AICORE_BASE_URL
|
|
122
|
+
AICORE_RESOURCE_GROUP
|
|
123
|
+
ORCHESTRATION_URL
|
|
124
|
+
AICORE_JUDGE_MODEL (default: gpt-4o)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If your agent already has AI Core credentials, iris-evals picks them up automatically — no extra config.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Skill Integration
|
|
132
|
+
|
|
133
|
+
The `iris-sap-agent-skill` asks **Q_EVAL** as its last interview question:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
Which evaluation types does this agent need?
|
|
137
|
+
→ deterministic / probabilistic / llm_as_judge / agent_as_judge / trace / safety / performance
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Answering generates all wiring automatically — `x_evals` block, `app.py` startup/loop/health, and `requirements.txt` entry.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Running Tests
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install -e ".[dev]"
|
|
148
|
+
pytest
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Project Structure
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
src/iris_evals/
|
|
157
|
+
├── aicore.py SAP AI Core orchestration client
|
|
158
|
+
├── registry.py Built-in golden datasets, rubrics, thresholds
|
|
159
|
+
├── schemas.py EvalType, GoldenRecord, EvalConfig, EvalReport
|
|
160
|
+
├── self_eval.py run_self_eval() and self_eval_loop()
|
|
161
|
+
├── runner.py EvalSuite base class
|
|
162
|
+
├── _transport.py A2A JSON-RPC call helper
|
|
163
|
+
└── evaluators/
|
|
164
|
+
├── deterministic.py
|
|
165
|
+
├── probabilistic.py
|
|
166
|
+
├── llm_judge.py
|
|
167
|
+
├── safety.py
|
|
168
|
+
├── trace.py
|
|
169
|
+
└── performance.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Versioning
|
|
175
|
+
|
|
176
|
+
| Version | Notes |
|
|
177
|
+
|---------|-------|
|
|
178
|
+
| v1.0.0 | Initial release — 7 eval types, built-in registry, AI Core LLM judge |
|
|
179
|
+
|
|
180
|
+
Tag a new version and update `requirements.txt` in each agent to upgrade:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
git tag v1.0.1
|
|
184
|
+
git push --tags
|
|
185
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
iris_evals/__init__.py,sha256=yAhT45gnSJdlLz-m4qrFLd_u5BMJzf5LlirWVoI2d14,2402
|
|
2
|
+
iris_evals/_transport.py,sha256=vhSDUylNLEa-UlRxyfZlUdHhPW7Pzj6OZ0Nj8FtA4Tw,2391
|
|
3
|
+
iris_evals/aicore.py,sha256=GcSYkrTcVhEqKVNgn_xWnP1PHZY1IIhOZWxcluUUxPI,3568
|
|
4
|
+
iris_evals/registry.py,sha256=ELxjug3HyRoTzDvbT0x6p3BvhCL8TqCzPI66JoRauk8,12032
|
|
5
|
+
iris_evals/results.py,sha256=52jKjKpvb3FB-gX0pDp7WoUkSO1fE26kC074yeXJ6Jg,2706
|
|
6
|
+
iris_evals/runner.py,sha256=9tYwSdo2G5SjHlpEFRzmWAtZY0SnFKdzYlBw5_XAV_M,3917
|
|
7
|
+
iris_evals/schemas.py,sha256=Xj8yQWwmRG5QlqpKDPkDEA5oHInGs79yfWBHUy_3tyU,5340
|
|
8
|
+
iris_evals/self_eval.py,sha256=R9q1Dmvu-UGexXoCZpI5BlFGN5yPoylTl9qSUEp-v-g,10870
|
|
9
|
+
iris_evals/evaluators/__init__.py,sha256=rF3sJzuHKdm9hYhsPynllsgNYs9b2pYVJ6bRWb-TLHE,251
|
|
10
|
+
iris_evals/evaluators/deterministic.py,sha256=Yj_ZmCq5OPlTjODHRQXgSkBghhUdC3NFCh02C-L9ZnA,2281
|
|
11
|
+
iris_evals/evaluators/llm_judge.py,sha256=_d4rFJH1HU8ycGC8z_QQbwtrmHtTaQB7mujWLWz3rwI,4512
|
|
12
|
+
iris_evals/evaluators/performance.py,sha256=CU-vopv8AneuSPrs6LBjNpHe47kHlxakhv2aVIYTdgg,3043
|
|
13
|
+
iris_evals/evaluators/probabilistic.py,sha256=p1MVuKXWguS0LQpR-vlUaC6tx5R8_OMZqzzeC-J0bgM,4135
|
|
14
|
+
iris_evals/evaluators/safety.py,sha256=wYdcAf6GXBR1pcTcDViSzrfaO9J6iCbBjRnmLFaJjFU,3776
|
|
15
|
+
iris_evals/evaluators/trace.py,sha256=OQ-pFRNSdZ2j4DvvAcc477JFJdhE_5vAZTj5uJN1sbw,3383
|
|
16
|
+
applexus_iris_evals-1.0.0.dist-info/METADATA,sha256=Ex2pLM4e1plPPr1uGTgIZtq6w--KxJvmpIf9kEz7ZHY,5158
|
|
17
|
+
applexus_iris_evals-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
18
|
+
applexus_iris_evals-1.0.0.dist-info/top_level.txt,sha256=fly3XfJNLI8pg1in9fHDx3JCol1Zj7_O72ObWcqtEl4,11
|
|
19
|
+
applexus_iris_evals-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
iris_evals
|
iris_evals/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
iris-evals — Reusable evaluation framework for IRIS SAP Joule A2A agents.
|
|
3
|
+
|
|
4
|
+
── Startup self-eval (simplest usage) ──────────────────────────────────────────
|
|
5
|
+
from iris_evals import run_self_eval
|
|
6
|
+
|
|
7
|
+
@app.on_event("startup")
|
|
8
|
+
async def startup():
|
|
9
|
+
report = await run_self_eval(agent_url="http://localhost:8080")
|
|
10
|
+
if not report.overall_passed:
|
|
11
|
+
logger.warning(f"[IRIS EVALS] {report.summary}")
|
|
12
|
+
|
|
13
|
+
── Background loop ──────────────────────────────────────────────────────────────
|
|
14
|
+
from iris_evals import self_eval_loop
|
|
15
|
+
|
|
16
|
+
@app.on_event("startup")
|
|
17
|
+
async def startup():
|
|
18
|
+
asyncio.create_task(self_eval_loop(agent_url="http://localhost:8080", interval_seconds=300))
|
|
19
|
+
|
|
20
|
+
── Health endpoint ───────────────────────────────────────────────────────────────
|
|
21
|
+
@app.get("/health/evals")
|
|
22
|
+
async def eval_health():
|
|
23
|
+
report = await run_self_eval(agent_url="http://localhost:8080")
|
|
24
|
+
return {"passed": report.overall_passed, "summary": report.summary}
|
|
25
|
+
|
|
26
|
+
── Manual / CLI ─────────────────────────────────────────────────────────────────
|
|
27
|
+
iris-evals run --agent http://localhost:8080
|
|
28
|
+
iris-evals run-pipeline --pipeline pipeline.json
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from iris_evals.runner import EvalSuite
|
|
32
|
+
from iris_evals.self_eval import run_self_eval, self_eval_loop
|
|
33
|
+
from iris_evals.schemas import (
|
|
34
|
+
EvalConfig,
|
|
35
|
+
EvalType,
|
|
36
|
+
EvalReport,
|
|
37
|
+
EvalResult,
|
|
38
|
+
GoldenRecord,
|
|
39
|
+
JudgeRubric,
|
|
40
|
+
JudgeCriterion,
|
|
41
|
+
)
|
|
42
|
+
from iris_evals.results import print_report, save_json
|
|
43
|
+
from iris_evals.registry import register_agent
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
# self-eval (primary API)
|
|
47
|
+
"run_self_eval",
|
|
48
|
+
"self_eval_loop",
|
|
49
|
+
# manual suite
|
|
50
|
+
"EvalSuite",
|
|
51
|
+
# schemas
|
|
52
|
+
"EvalConfig",
|
|
53
|
+
"EvalType",
|
|
54
|
+
"EvalReport",
|
|
55
|
+
"EvalResult",
|
|
56
|
+
"GoldenRecord",
|
|
57
|
+
"JudgeRubric",
|
|
58
|
+
"JudgeCriterion",
|
|
59
|
+
# output
|
|
60
|
+
"print_report",
|
|
61
|
+
"save_json",
|
|
62
|
+
# registry extension
|
|
63
|
+
"register_agent",
|
|
64
|
+
]
|
iris_evals/_transport.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_transport.py — Agent call transport layer.
|
|
3
|
+
|
|
4
|
+
All evaluators call the agent through this single function.
|
|
5
|
+
Supports:
|
|
6
|
+
- A2A JSON-RPC (message/send) — default for IRIS agents
|
|
7
|
+
- Plain HTTP POST (for non-A2A agents or direct endpoint testing)
|
|
8
|
+
|
|
9
|
+
The agent URL and transport mode come from EvalConfig.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import uuid
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from iris_evals.schemas import EvalConfig
|
|
21
|
+
|
|
22
|
+
_TIMEOUT = 60.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def call_agent(config: EvalConfig, input_data: str | dict) -> dict:
|
|
26
|
+
"""Send input to the agent and return the parsed response dict."""
|
|
27
|
+
payload = input_data if isinstance(input_data, str) else json.dumps(input_data)
|
|
28
|
+
|
|
29
|
+
if config.use_a2a:
|
|
30
|
+
return await _call_a2a(config.agent_url, payload)
|
|
31
|
+
else:
|
|
32
|
+
return await _call_http(config.agent_url, payload)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def _call_a2a(base_url: str, text: str) -> dict:
|
|
36
|
+
"""
|
|
37
|
+
SAP Joule A2A JSON-RPC 2.0 transport.
|
|
38
|
+
Mirrors the pattern used by a2a_client.py in every IRIS agent.
|
|
39
|
+
"""
|
|
40
|
+
request_id = str(uuid.uuid4())
|
|
41
|
+
body = {
|
|
42
|
+
"jsonrpc": "2.0",
|
|
43
|
+
"id": request_id,
|
|
44
|
+
"method": "message/send",
|
|
45
|
+
"params": {
|
|
46
|
+
"message": {
|
|
47
|
+
"messageId": str(uuid.uuid4()),
|
|
48
|
+
"contextId": str(uuid.uuid4()),
|
|
49
|
+
"role": "user",
|
|
50
|
+
"parts": [{"kind": "text", "text": text}],
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
|
56
|
+
resp = await client.post(base_url, json=body)
|
|
57
|
+
resp.raise_for_status()
|
|
58
|
+
rpc = resp.json()
|
|
59
|
+
|
|
60
|
+
result = rpc.get("result") or {}
|
|
61
|
+
artifacts = result.get("artifacts", [])
|
|
62
|
+
if not artifacts:
|
|
63
|
+
return {"raw_rpc": rpc}
|
|
64
|
+
|
|
65
|
+
parts = artifacts[0].get("parts", [{}])
|
|
66
|
+
text_out = parts[0].get("text", "") if parts else ""
|
|
67
|
+
try:
|
|
68
|
+
parsed = json.loads(text_out)
|
|
69
|
+
parsed["result"] = result # keep A2A envelope for trace evaluator
|
|
70
|
+
return parsed
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
return {"result": result, "text": text_out}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _call_http(url: str, text: str) -> dict[str, Any]:
|
|
76
|
+
"""Plain HTTP POST — for non-A2A endpoints."""
|
|
77
|
+
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
|
78
|
+
resp = await client.post(url, json={"input": text})
|
|
79
|
+
resp.raise_for_status()
|
|
80
|
+
return resp.json()
|
iris_evals/aicore.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aicore.py — SAP AI Core orchestration client for iris-evals LLM judge.
|
|
3
|
+
|
|
4
|
+
Handles OAuth2 token fetch (client credentials) and calls the
|
|
5
|
+
AI Core orchestration completion endpoint.
|
|
6
|
+
|
|
7
|
+
Required env vars:
|
|
8
|
+
AICORE_CLIENT_ID
|
|
9
|
+
AICORE_CLIENT_SECRET
|
|
10
|
+
AICORE_AUTH_URL
|
|
11
|
+
ORCHESTRATION_URL
|
|
12
|
+
AICORE_RESOURCE_GROUP (default: "default")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── Token cache ───────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
_token: str | None = None
|
|
28
|
+
_token_expiry: float = 0.0 # epoch seconds
|
|
29
|
+
_TOKEN_BUFFER_SECS = 60 # refresh 60 s before actual expiry
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fetch_token() -> str:
|
|
33
|
+
client_id = os.environ["AICORE_CLIENT_ID"]
|
|
34
|
+
client_secret = os.environ["AICORE_CLIENT_SECRET"]
|
|
35
|
+
auth_url = os.environ["AICORE_AUTH_URL"]
|
|
36
|
+
|
|
37
|
+
creds = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
|
|
38
|
+
|
|
39
|
+
resp = httpx.post(
|
|
40
|
+
auth_url,
|
|
41
|
+
headers={
|
|
42
|
+
"Authorization": f"Basic {creds}",
|
|
43
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
44
|
+
},
|
|
45
|
+
data={"grant_type": "client_credentials"},
|
|
46
|
+
timeout=15,
|
|
47
|
+
)
|
|
48
|
+
resp.raise_for_status()
|
|
49
|
+
body = resp.json()
|
|
50
|
+
return body["access_token"], int(body.get("expires_in", 3600))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_token() -> str:
|
|
54
|
+
global _token, _token_expiry
|
|
55
|
+
now = time.monotonic()
|
|
56
|
+
if _token is None or now >= _token_expiry:
|
|
57
|
+
token, expires_in = _fetch_token()
|
|
58
|
+
_token = token
|
|
59
|
+
_token_expiry = now + expires_in - _TOKEN_BUFFER_SECS
|
|
60
|
+
return _token
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── Completion call ───────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
def chat_complete(
|
|
66
|
+
system: str,
|
|
67
|
+
user: str,
|
|
68
|
+
*,
|
|
69
|
+
model_name: str = "gpt-4o",
|
|
70
|
+
max_tokens: int = 512,
|
|
71
|
+
temperature: float = 0.0,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Call the SAP AI Core orchestration service and return the assistant text.
|
|
75
|
+
|
|
76
|
+
Uses the deployment at ORCHESTRATION_URL env var.
|
|
77
|
+
The model_name must be deployed in the AI Core resource group.
|
|
78
|
+
"""
|
|
79
|
+
url = os.environ["ORCHESTRATION_URL"].rstrip("/") + "/completion"
|
|
80
|
+
resource_group = os.environ.get("AICORE_RESOURCE_GROUP", "default")
|
|
81
|
+
|
|
82
|
+
payload: dict[str, Any] = {
|
|
83
|
+
"orchestration_config": {
|
|
84
|
+
"module_configurations": {
|
|
85
|
+
"llm_module_config": {
|
|
86
|
+
"model_name": model_name,
|
|
87
|
+
"model_params": {
|
|
88
|
+
"max_tokens": max_tokens,
|
|
89
|
+
"temperature": temperature,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"messages_history": [
|
|
95
|
+
{"role": "system", "content": system},
|
|
96
|
+
{"role": "user", "content": user},
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
resp = httpx.post(
|
|
101
|
+
url,
|
|
102
|
+
headers={
|
|
103
|
+
"Authorization": f"Bearer {_get_token()}",
|
|
104
|
+
"AI-Resource-Group": resource_group,
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
json=payload,
|
|
108
|
+
timeout=60,
|
|
109
|
+
)
|
|
110
|
+
resp.raise_for_status()
|
|
111
|
+
data = resp.json()
|
|
112
|
+
|
|
113
|
+
# SAP AI Core orchestration wraps the OpenAI-style response
|
|
114
|
+
choices = (
|
|
115
|
+
data.get("orchestration_result", data)
|
|
116
|
+
.get("choices", [{}])
|
|
117
|
+
)
|
|
118
|
+
return choices[0].get("message", {}).get("content", "")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
deterministic.py — Exact-match evaluation against a golden dataset.
|
|
3
|
+
|
|
4
|
+
Sends each golden record to the agent, compares actual output to
|
|
5
|
+
expected_output / expected_verdict. No LLM involved.
|
|
6
|
+
|
|
7
|
+
Suitable for: P1.11 Duplicate Detection, P2.5 Tolerance Resolution,
|
|
8
|
+
and any fully deterministic rule-based agent.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from iris_evals.schemas import EvalConfig, EvalResult, EvalType, GoldenRecord
|
|
15
|
+
from iris_evals._transport import call_agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def run(config: EvalConfig, records: list[GoldenRecord]) -> list[EvalResult]:
|
|
19
|
+
results: list[EvalResult] = []
|
|
20
|
+
|
|
21
|
+
for rec in records:
|
|
22
|
+
t0 = time.monotonic()
|
|
23
|
+
try:
|
|
24
|
+
raw = await call_agent(config, rec.input)
|
|
25
|
+
latency_ms = (time.monotonic() - t0) * 1000
|
|
26
|
+
|
|
27
|
+
actual = _extract_verdict_or_text(raw)
|
|
28
|
+
expected = rec.expected_verdict or rec.expected_output or ""
|
|
29
|
+
|
|
30
|
+
passed = _matches(actual, expected)
|
|
31
|
+
results.append(EvalResult(
|
|
32
|
+
record_id=rec.id,
|
|
33
|
+
eval_type=EvalType.DETERMINISTIC,
|
|
34
|
+
passed=passed,
|
|
35
|
+
score=1.0 if passed else 0.0,
|
|
36
|
+
actual_output=actual,
|
|
37
|
+
expected_output=expected,
|
|
38
|
+
reason="" if passed else f"Expected '{expected}', got '{actual}'",
|
|
39
|
+
latency_ms=latency_ms,
|
|
40
|
+
))
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
results.append(EvalResult(
|
|
43
|
+
record_id=rec.id,
|
|
44
|
+
eval_type=EvalType.DETERMINISTIC,
|
|
45
|
+
passed=False,
|
|
46
|
+
reason=f"Agent call failed: {exc}",
|
|
47
|
+
))
|
|
48
|
+
|
|
49
|
+
return results
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _extract_verdict_or_text(raw: dict) -> str:
|
|
53
|
+
"""Pull verdict / invoice_resolution / text from the agent response."""
|
|
54
|
+
# structured SAP agent response shapes
|
|
55
|
+
for key in ("verdict", "invoice_resolution", "resolution", "status"):
|
|
56
|
+
if key in raw:
|
|
57
|
+
return str(raw[key])
|
|
58
|
+
# A2A artifact text fallback
|
|
59
|
+
if "result" in raw:
|
|
60
|
+
parts = raw["result"].get("artifacts", [{}])[0].get("parts", [{}])
|
|
61
|
+
return parts[0].get("text", str(raw)) if parts else str(raw)
|
|
62
|
+
return str(raw)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _matches(actual: str, expected: str) -> bool:
|
|
66
|
+
return actual.strip().upper() == expected.strip().upper()
|