agentops-accelerator 0.3.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.
- agentops/__init__.py +10 -0
- agentops/__main__.py +6 -0
- agentops/agent/__init__.py +12 -0
- agentops/agent/_legacy_ids.py +92 -0
- agentops/agent/analyzer.py +207 -0
- agentops/agent/checks/__init__.py +1 -0
- agentops/agent/checks/catalog.py +880 -0
- agentops/agent/checks/errors.py +279 -0
- agentops/agent/checks/foundry_config.py +75 -0
- agentops/agent/checks/latency.py +84 -0
- agentops/agent/checks/opex.py +157 -0
- agentops/agent/checks/opex_workspace.py +874 -0
- agentops/agent/checks/posture.py +36 -0
- agentops/agent/checks/posture_rules/__init__.py +53 -0
- agentops/agent/checks/posture_rules/content_filter.py +59 -0
- agentops/agent/checks/posture_rules/diagnostics.py +74 -0
- agentops/agent/checks/posture_rules/local_auth.py +55 -0
- agentops/agent/checks/posture_rules/managed_identity.py +59 -0
- agentops/agent/checks/posture_rules/network.py +68 -0
- agentops/agent/checks/regression.py +78 -0
- agentops/agent/checks/release_readiness.py +182 -0
- agentops/agent/checks/safety.py +247 -0
- agentops/agent/checks/spec_conformance.py +375 -0
- agentops/agent/cockpit.py +5159 -0
- agentops/agent/config.py +240 -0
- agentops/agent/findings.py +113 -0
- agentops/agent/history.py +142 -0
- agentops/agent/knowledge/__init__.py +182 -0
- agentops/agent/knowledge/waf-checklist.csv +39 -0
- agentops/agent/llm_assist/__init__.py +16 -0
- agentops/agent/llm_assist/_base.py +124 -0
- agentops/agent/llm_assist/_bundle_rule.py +154 -0
- agentops/agent/llm_assist/_client.py +347 -0
- agentops/agent/llm_assist/_dataset_rules.py +191 -0
- agentops/agent/llm_assist/_engine.py +106 -0
- agentops/agent/llm_assist/_prompt_rules.py +291 -0
- agentops/agent/llm_assist/_spec_rules.py +235 -0
- agentops/agent/production_telemetry.py +430 -0
- agentops/agent/report.py +207 -0
- agentops/agent/server/__init__.py +1 -0
- agentops/agent/server/app.py +84 -0
- agentops/agent/server/auth.py +94 -0
- agentops/agent/server/chat.py +44 -0
- agentops/agent/server/protocol.py +72 -0
- agentops/agent/sources/__init__.py +1 -0
- agentops/agent/sources/azure_monitor.py +523 -0
- agentops/agent/sources/azure_resources.py +602 -0
- agentops/agent/sources/foundry_control.py +174 -0
- agentops/agent/sources/results_history.py +494 -0
- agentops/agent/sources/spec_detectors/__init__.py +42 -0
- agentops/agent/sources/spec_detectors/_base.py +58 -0
- agentops/agent/sources/spec_detectors/agents_md.py +75 -0
- agentops/agent/sources/spec_detectors/spec_kit.py +172 -0
- agentops/agent/time_range.py +117 -0
- agentops/cli/__init__.py +1 -0
- agentops/cli/app.py +4823 -0
- agentops/core/__init__.py +1 -0
- agentops/core/agentops_config.py +592 -0
- agentops/core/config_loader.py +22 -0
- agentops/core/evaluators.py +480 -0
- agentops/core/release_evidence.py +56 -0
- agentops/core/results.py +117 -0
- agentops/mcp/__init__.py +10 -0
- agentops/mcp/server.py +232 -0
- agentops/pipeline/__init__.py +8 -0
- agentops/pipeline/cloud_results.py +189 -0
- agentops/pipeline/cloud_runner.py +901 -0
- agentops/pipeline/comparison.py +108 -0
- agentops/pipeline/diagnostics.py +51 -0
- agentops/pipeline/invocations.py +535 -0
- agentops/pipeline/official_eval.py +414 -0
- agentops/pipeline/orchestrator.py +775 -0
- agentops/pipeline/prompt_deploy.py +377 -0
- agentops/pipeline/publisher.py +121 -0
- agentops/pipeline/reporter.py +202 -0
- agentops/pipeline/runtime.py +409 -0
- agentops/pipeline/thresholds.py +84 -0
- agentops/services/__init__.py +1 -0
- agentops/services/cicd.py +720 -0
- agentops/services/eval_analysis.py +848 -0
- agentops/services/evidence_pack.py +757 -0
- agentops/services/initializer.py +86 -0
- agentops/services/preflight.py +470 -0
- agentops/services/setup_wizard.py +709 -0
- agentops/services/skills.py +643 -0
- agentops/services/trace_promotion.py +300 -0
- agentops/services/workflow_analysis.py +1129 -0
- agentops/templates/.gitignore +15 -0
- agentops/templates/__init__.py +1 -0
- agentops/templates/agent-server/Dockerfile +23 -0
- agentops/templates/agent-server/README.md +61 -0
- agentops/templates/agent-server/main.bicep +94 -0
- agentops/templates/agent.yaml +87 -0
- agentops/templates/agentops.yaml +58 -0
- agentops/templates/foundry.svg +71 -0
- agentops/templates/icon.png +0 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-dev-azd.yml +118 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-dev.yml +73 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prod-azd.yml +141 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prod.yml +94 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prompt-agent.yml +167 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-qa-azd.yml +118 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-qa.yml +68 -0
- agentops/templates/pipelines/azuredevops/agentops-pr-prompt-agent.yml +210 -0
- agentops/templates/pipelines/azuredevops/agentops-pr.yml +155 -0
- agentops/templates/pipelines/azuredevops/agentops-watchdog.yml +106 -0
- agentops/templates/project.gitignore +36 -0
- agentops/templates/sample-traces.jsonl +3 -0
- agentops/templates/skills/agentops-agent/SKILL.md +137 -0
- agentops/templates/skills/agentops-config/SKILL.md +113 -0
- agentops/templates/skills/agentops-dataset/SKILL.md +84 -0
- agentops/templates/skills/agentops-eval/SKILL.md +189 -0
- agentops/templates/skills/agentops-report/SKILL.md +71 -0
- agentops/templates/skills/agentops-workflow/SKILL.md +471 -0
- agentops/templates/smoke.jsonl +3 -0
- agentops/templates/waf-checklist.README.md +84 -0
- agentops/templates/waf-checklist.csv +22 -0
- agentops/templates/workflows/agentops-deploy-dev-azd.yml +166 -0
- agentops/templates/workflows/agentops-deploy-dev.yml +187 -0
- agentops/templates/workflows/agentops-deploy-prod-azd.yml +183 -0
- agentops/templates/workflows/agentops-deploy-prod.yml +171 -0
- agentops/templates/workflows/agentops-deploy-prompt-agent.yml +197 -0
- agentops/templates/workflows/agentops-deploy-qa-azd.yml +156 -0
- agentops/templates/workflows/agentops-deploy-qa.yml +145 -0
- agentops/templates/workflows/agentops-pr-prompt-agent.yml +210 -0
- agentops/templates/workflows/agentops-pr.yml +148 -0
- agentops/templates/workflows/agentops-watchdog.yml +122 -0
- agentops/utils/__init__.py +1 -0
- agentops/utils/azd_env.py +435 -0
- agentops/utils/azure_endpoints.py +62 -0
- agentops/utils/colors.py +47 -0
- agentops/utils/dotenv_loader.py +105 -0
- agentops/utils/foundry_discovery.py +229 -0
- agentops/utils/logging.py +59 -0
- agentops/utils/telemetry.py +554 -0
- agentops/utils/yaml.py +36 -0
- agentops_accelerator-0.3.0.dist-info/METADATA +278 -0
- agentops_accelerator-0.3.0.dist-info/RECORD +142 -0
- agentops_accelerator-0.3.0.dist-info/WHEEL +5 -0
- agentops_accelerator-0.3.0.dist-info/entry_points.txt +2 -0
- agentops_accelerator-0.3.0.dist-info/licenses/LICENSE +21 -0
- agentops_accelerator-0.3.0.dist-info/top_level.txt +1 -0
agentops/mcp/server.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""AgentOps MCP server (stdio transport).
|
|
2
|
+
|
|
3
|
+
This module is imported lazily - the ``mcp`` extra is optional. Importing
|
|
4
|
+
this file triggers ``import mcp.server.fastmcp`` which fails with a clear
|
|
5
|
+
message if the extra is not installed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _build_server() -> Any:
|
|
16
|
+
"""Construct and return the FastMCP server.
|
|
17
|
+
|
|
18
|
+
Imports of ``mcp`` happen inside the function so that ``agentops --help``
|
|
19
|
+
keeps working when the optional extra is not installed.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
from mcp.server.fastmcp import FastMCP
|
|
23
|
+
except ImportError as exc: # pragma: no cover - exercised only without extra
|
|
24
|
+
raise RuntimeError(
|
|
25
|
+
"The MCP server requires the 'mcp' extra. "
|
|
26
|
+
"Install it with: pip install agentops-accelerator[mcp]"
|
|
27
|
+
) from exc
|
|
28
|
+
|
|
29
|
+
server = FastMCP("agentops")
|
|
30
|
+
|
|
31
|
+
@server.tool()
|
|
32
|
+
def agentops_init(directory: str = ".", force: bool = False) -> Dict[str, Any]:
|
|
33
|
+
"""Initialise an AgentOps workspace at ``directory``.
|
|
34
|
+
|
|
35
|
+
Creates ``agentops.yaml`` at the project root and a tiny seed
|
|
36
|
+
dataset under ``.agentops/data/smoke.jsonl``. Set ``force=True``
|
|
37
|
+
to overwrite existing files.
|
|
38
|
+
"""
|
|
39
|
+
from agentops.services.initializer import initialize_flat_workspace
|
|
40
|
+
|
|
41
|
+
result = initialize_flat_workspace(Path(directory), force=force)
|
|
42
|
+
return {
|
|
43
|
+
"workspace_dir": str(result.workspace_dir),
|
|
44
|
+
"created_files": [str(p) for p in result.created_files],
|
|
45
|
+
"overwritten_files": [str(p) for p in result.overwritten_files],
|
|
46
|
+
"skipped_files": [str(p) for p in result.skipped_files],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@server.tool()
|
|
50
|
+
def agentops_eval_run(
|
|
51
|
+
config_path: Optional[str] = None,
|
|
52
|
+
output_dir: Optional[str] = None,
|
|
53
|
+
baseline: Optional[str] = None,
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
"""Run an AgentOps evaluation and return a summary.
|
|
56
|
+
|
|
57
|
+
``config_path`` defaults to ``agentops.yaml`` in the current
|
|
58
|
+
directory. ``output_dir`` defaults to
|
|
59
|
+
``.agentops/results/latest``. ``baseline`` is an optional path to
|
|
60
|
+
a previous ``results.json`` for comparison.
|
|
61
|
+
"""
|
|
62
|
+
from agentops.core.config_loader import load_agentops_config
|
|
63
|
+
from agentops.pipeline.orchestrator import (
|
|
64
|
+
RunOptions,
|
|
65
|
+
exit_code_from,
|
|
66
|
+
run_evaluation,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
config = Path(config_path) if config_path else Path("agentops.yaml")
|
|
70
|
+
if not config.exists():
|
|
71
|
+
return {
|
|
72
|
+
"ok": False,
|
|
73
|
+
"exit_code": 1,
|
|
74
|
+
"error": f"config not found at {config}",
|
|
75
|
+
}
|
|
76
|
+
try:
|
|
77
|
+
config_obj = load_agentops_config(config)
|
|
78
|
+
except Exception as exc: # noqa: BLE001
|
|
79
|
+
return {
|
|
80
|
+
"ok": False,
|
|
81
|
+
"exit_code": 1,
|
|
82
|
+
"error": f"failed to load config: {exc}",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
out_dir = (
|
|
86
|
+
Path(output_dir)
|
|
87
|
+
if output_dir
|
|
88
|
+
else config.parent / ".agentops" / "results" / "latest"
|
|
89
|
+
)
|
|
90
|
+
options = RunOptions(
|
|
91
|
+
config_path=config.resolve(),
|
|
92
|
+
output_dir=out_dir,
|
|
93
|
+
baseline_path=Path(baseline).resolve() if baseline else None,
|
|
94
|
+
)
|
|
95
|
+
try:
|
|
96
|
+
run = run_evaluation(config_obj, options=options)
|
|
97
|
+
except Exception as exc: # noqa: BLE001
|
|
98
|
+
return {"ok": False, "exit_code": 1, "error": f"evaluation failed: {exc}"}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"ok": True,
|
|
102
|
+
"exit_code": exit_code_from(run),
|
|
103
|
+
"output_dir": str(out_dir),
|
|
104
|
+
"results_json": str(out_dir / "results.json"),
|
|
105
|
+
"report_md": str(out_dir / "report.md"),
|
|
106
|
+
"passed": bool(run.summary.overall_passed),
|
|
107
|
+
"metrics": dict(run.aggregate_metrics or {}),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@server.tool()
|
|
111
|
+
def agentops_report_show(report_path: Optional[str] = None) -> Dict[str, Any]:
|
|
112
|
+
"""Return the contents of ``report.md`` for a finished run.
|
|
113
|
+
|
|
114
|
+
Defaults to ``.agentops/results/latest/report.md``.
|
|
115
|
+
"""
|
|
116
|
+
path = (
|
|
117
|
+
Path(report_path)
|
|
118
|
+
if report_path
|
|
119
|
+
else Path(".agentops/results/latest/report.md")
|
|
120
|
+
)
|
|
121
|
+
if not path.exists():
|
|
122
|
+
return {"ok": False, "error": f"report not found at {path}"}
|
|
123
|
+
return {
|
|
124
|
+
"ok": True,
|
|
125
|
+
"path": str(path),
|
|
126
|
+
"markdown": path.read_text(encoding="utf-8"),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@server.tool()
|
|
130
|
+
def agentops_results_summary(results_path: Optional[str] = None) -> Dict[str, Any]:
|
|
131
|
+
"""Return a compact JSON summary extracted from ``results.json``.
|
|
132
|
+
|
|
133
|
+
Defaults to ``.agentops/results/latest/results.json``.
|
|
134
|
+
"""
|
|
135
|
+
path = (
|
|
136
|
+
Path(results_path)
|
|
137
|
+
if results_path
|
|
138
|
+
else Path(".agentops/results/latest/results.json")
|
|
139
|
+
)
|
|
140
|
+
if not path.exists():
|
|
141
|
+
return {"ok": False, "error": f"results not found at {path}"}
|
|
142
|
+
try:
|
|
143
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
144
|
+
except Exception as exc: # noqa: BLE001
|
|
145
|
+
return {"ok": False, "error": f"failed to parse results.json: {exc}"}
|
|
146
|
+
|
|
147
|
+
summary = data.get("summary") or {}
|
|
148
|
+
return {
|
|
149
|
+
"ok": True,
|
|
150
|
+
"path": str(path),
|
|
151
|
+
"version": data.get("version"),
|
|
152
|
+
"target": data.get("target"),
|
|
153
|
+
"rows": len(data.get("rows") or []),
|
|
154
|
+
"metrics": summary.get("metrics") or {},
|
|
155
|
+
"thresholds": summary.get("thresholds") or {},
|
|
156
|
+
"overall_passed": summary.get("overall_passed"),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@server.tool()
|
|
160
|
+
def agentops_dataset_add(
|
|
161
|
+
dataset_path: str,
|
|
162
|
+
rows: List[Dict[str, Any]],
|
|
163
|
+
) -> Dict[str, Any]:
|
|
164
|
+
"""Append JSONL rows to ``dataset_path``.
|
|
165
|
+
|
|
166
|
+
Creates the parent directory if needed. Each row must be a JSON
|
|
167
|
+
object - typical keys are ``input``, ``expected``, ``context``,
|
|
168
|
+
and ``tool_calls`` depending on the agent type.
|
|
169
|
+
"""
|
|
170
|
+
if not isinstance(rows, list) or not all(isinstance(r, dict) for r in rows):
|
|
171
|
+
return {"ok": False, "error": "rows must be a list of JSON objects"}
|
|
172
|
+
path = Path(dataset_path)
|
|
173
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
175
|
+
for row in rows:
|
|
176
|
+
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
177
|
+
return {"ok": True, "path": str(path), "appended": len(rows)}
|
|
178
|
+
|
|
179
|
+
@server.tool()
|
|
180
|
+
def agentops_list_runs(workspace_dir: str = ".") -> Dict[str, Any]:
|
|
181
|
+
"""List historical runs under ``<workspace_dir>/.agentops/results/``."""
|
|
182
|
+
results_dir = Path(workspace_dir) / ".agentops" / "results"
|
|
183
|
+
if not results_dir.is_dir():
|
|
184
|
+
return {"ok": True, "runs": []}
|
|
185
|
+
runs: List[Dict[str, Any]] = []
|
|
186
|
+
for entry in sorted(results_dir.iterdir(), key=lambda p: p.name):
|
|
187
|
+
if not entry.is_dir():
|
|
188
|
+
continue
|
|
189
|
+
results_json = entry / "results.json"
|
|
190
|
+
run_info: Dict[str, Any] = {
|
|
191
|
+
"name": entry.name,
|
|
192
|
+
"path": str(entry),
|
|
193
|
+
"has_results": results_json.exists(),
|
|
194
|
+
}
|
|
195
|
+
if results_json.exists():
|
|
196
|
+
try:
|
|
197
|
+
payload = json.loads(results_json.read_text(encoding="utf-8"))
|
|
198
|
+
summary = payload.get("summary") or {}
|
|
199
|
+
run_info["overall_passed"] = summary.get("overall_passed")
|
|
200
|
+
run_info["metrics"] = summary.get("metrics") or {}
|
|
201
|
+
except Exception: # noqa: BLE001
|
|
202
|
+
pass
|
|
203
|
+
runs.append(run_info)
|
|
204
|
+
return {"ok": True, "runs": runs}
|
|
205
|
+
|
|
206
|
+
@server.tool()
|
|
207
|
+
def agentops_workflow_init(
|
|
208
|
+
directory: str = ".",
|
|
209
|
+
force: bool = False,
|
|
210
|
+
) -> Dict[str, Any]:
|
|
211
|
+
"""Generate GitHub Actions workflows for AgentOps evaluation."""
|
|
212
|
+
from agentops.services.cicd import generate_cicd_workflows
|
|
213
|
+
|
|
214
|
+
result = generate_cicd_workflows(directory=Path(directory), force=force)
|
|
215
|
+
return {
|
|
216
|
+
"ok": True,
|
|
217
|
+
"created_files": [str(p) for p in result.created_files],
|
|
218
|
+
"overwritten_files": [str(p) for p in result.overwritten_files],
|
|
219
|
+
"skipped_files": [str(p) for p in result.skipped_files],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return server
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def serve_stdio() -> None:
|
|
226
|
+
"""Entry point for ``agentops mcp serve``.
|
|
227
|
+
|
|
228
|
+
Builds the FastMCP server and runs it on the stdio transport. This
|
|
229
|
+
function blocks until the client disconnects.
|
|
230
|
+
"""
|
|
231
|
+
server = _build_server()
|
|
232
|
+
server.run()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Map Foundry cloud eval output items into AgentOps result shapes.
|
|
2
|
+
|
|
3
|
+
When ``execution: cloud`` is used in ``agentops.yaml``, the agent and
|
|
4
|
+
evaluators run server-side via the Foundry / OpenAI Evals API. We then
|
|
5
|
+
download per-row ``output_items`` from Foundry and reshape them into the
|
|
6
|
+
same :class:`RowResult` / :class:`RunResult` schema that local execution
|
|
7
|
+
produces, so downstream consumers (``report.md``, ``--baseline`` diffing,
|
|
8
|
+
CI gates) behave identically regardless of where the run executed.
|
|
9
|
+
|
|
10
|
+
The cloud output schema is intentionally loose: we accept multiple field
|
|
11
|
+
spellings (``output_text`` / ``output`` / ``message``; ``score`` /
|
|
12
|
+
``value`` / ``passed``) and fall back gracefully when a field is absent.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from agentops.core.results import RowMetric, RowResult
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def rows_from_cloud_output_items(
|
|
24
|
+
output_items: List[Dict[str, Any]],
|
|
25
|
+
) -> List[RowResult]:
|
|
26
|
+
"""Build a list of :class:`RowResult` from raw Foundry output items.
|
|
27
|
+
|
|
28
|
+
``output_items`` is the list returned by
|
|
29
|
+
``cloud_runner._list_output_items``. Each item is a dict with at
|
|
30
|
+
least ``datasource_item``, ``sample`` and ``results``; missing keys
|
|
31
|
+
yield blank fields rather than raising.
|
|
32
|
+
"""
|
|
33
|
+
rows: List[RowResult] = []
|
|
34
|
+
for index, item in enumerate(output_items):
|
|
35
|
+
rows.append(_row_from_item(index, item))
|
|
36
|
+
return rows
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _row_from_item(index: int, item: Dict[str, Any]) -> RowResult:
|
|
40
|
+
datasource = _as_dict(item.get("datasource_item")) or {}
|
|
41
|
+
sample = _as_dict(item.get("sample")) or {}
|
|
42
|
+
results = item.get("results") or []
|
|
43
|
+
|
|
44
|
+
metrics: List[RowMetric] = []
|
|
45
|
+
if isinstance(results, list):
|
|
46
|
+
for entry in results:
|
|
47
|
+
metric = _metric_from_result(entry)
|
|
48
|
+
if metric is not None:
|
|
49
|
+
metrics.append(metric)
|
|
50
|
+
|
|
51
|
+
return RowResult(
|
|
52
|
+
row_index=index,
|
|
53
|
+
input=_as_str(datasource.get("input")),
|
|
54
|
+
expected=_optional_str(datasource.get("expected")),
|
|
55
|
+
response=_extract_response_text(sample),
|
|
56
|
+
context=_optional_str(datasource.get("context")),
|
|
57
|
+
latency_seconds=None, # Foundry-side latency is not client-perceived.
|
|
58
|
+
tool_calls=datasource.get("tool_calls") if isinstance(datasource.get("tool_calls"), list) else None,
|
|
59
|
+
metrics=metrics,
|
|
60
|
+
error=_extract_item_error(item),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _metric_from_result(entry: Any) -> Optional[RowMetric]:
|
|
65
|
+
if not isinstance(entry, dict):
|
|
66
|
+
return None
|
|
67
|
+
name = entry.get("name") or entry.get("metric")
|
|
68
|
+
if not isinstance(name, str) or not name:
|
|
69
|
+
return None
|
|
70
|
+
score = _coerce_float(
|
|
71
|
+
entry.get("score"),
|
|
72
|
+
entry.get("value"),
|
|
73
|
+
entry.get("result"),
|
|
74
|
+
)
|
|
75
|
+
if score is None and isinstance(entry.get("passed"), bool):
|
|
76
|
+
score = 1.0 if entry["passed"] else 0.0
|
|
77
|
+
reason = entry.get("reason") if isinstance(entry.get("reason"), str) else None
|
|
78
|
+
err = entry.get("error") if isinstance(entry.get("error"), str) else None
|
|
79
|
+
return RowMetric(name=name, value=score, error=err, reason=reason)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _extract_response_text(sample: Dict[str, Any]) -> str:
|
|
83
|
+
"""Reach into a Foundry sample payload and pull a plain text response.
|
|
84
|
+
|
|
85
|
+
Foundry's sample shape varies: sometimes the response is a clean string
|
|
86
|
+
under ``output_text``, sometimes it's a list of output items under
|
|
87
|
+
``output`` / ``output_items``, and occasionally ``output_text`` is set
|
|
88
|
+
to a JSON-encoded version of the structured output. Try structured
|
|
89
|
+
fields first (they're authoritative), and recurse into JSON-encoded
|
|
90
|
+
strings rather than passing them through as the response.
|
|
91
|
+
"""
|
|
92
|
+
# 1. Structured fields are authoritative. Walk them first.
|
|
93
|
+
for key in ("output", "messages", "output_items"):
|
|
94
|
+
text = _text_from_structured(sample.get(key))
|
|
95
|
+
if text:
|
|
96
|
+
return text
|
|
97
|
+
|
|
98
|
+
# 2. Flat string fields. If the value looks like JSON, parse and recurse.
|
|
99
|
+
for key in ("output_text", "text", "content"):
|
|
100
|
+
value = sample.get(key)
|
|
101
|
+
if isinstance(value, str) and value:
|
|
102
|
+
stripped = value.strip()
|
|
103
|
+
if stripped.startswith("[") or stripped.startswith("{"):
|
|
104
|
+
try:
|
|
105
|
+
parsed = json.loads(stripped)
|
|
106
|
+
except (ValueError, TypeError):
|
|
107
|
+
return value
|
|
108
|
+
if isinstance(parsed, list):
|
|
109
|
+
text = _text_from_structured(parsed)
|
|
110
|
+
if text:
|
|
111
|
+
return text
|
|
112
|
+
elif isinstance(parsed, dict):
|
|
113
|
+
return _extract_response_text(parsed)
|
|
114
|
+
# Fall through to raw value if we couldn't extract.
|
|
115
|
+
return value
|
|
116
|
+
if isinstance(value, list):
|
|
117
|
+
text = _text_from_structured(value)
|
|
118
|
+
if text:
|
|
119
|
+
return text
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _text_from_structured(value: Any) -> str:
|
|
124
|
+
"""Walk a list-of-dicts (output / messages / output_items shape) and
|
|
125
|
+
return the first textual payload encountered, or ``""`` when none is
|
|
126
|
+
found. Iterates in reverse so the assistant's final message wins.
|
|
127
|
+
"""
|
|
128
|
+
if not isinstance(value, list):
|
|
129
|
+
return ""
|
|
130
|
+
for entry in reversed(value):
|
|
131
|
+
if not isinstance(entry, dict):
|
|
132
|
+
continue
|
|
133
|
+
# Try flat text fields first.
|
|
134
|
+
for field in ("output_text", "text"):
|
|
135
|
+
candidate = entry.get(field)
|
|
136
|
+
if isinstance(candidate, str) and candidate:
|
|
137
|
+
return candidate
|
|
138
|
+
# Some shapes nest under "content" as either a string or a list of
|
|
139
|
+
# content blocks (OpenAI Responses API: content: [{type, text}, ...]).
|
|
140
|
+
nested = entry.get("content")
|
|
141
|
+
if isinstance(nested, str) and nested:
|
|
142
|
+
return nested
|
|
143
|
+
if isinstance(nested, list):
|
|
144
|
+
text = _text_from_structured(nested)
|
|
145
|
+
if text:
|
|
146
|
+
return text
|
|
147
|
+
return ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _extract_item_error(item: Dict[str, Any]) -> Optional[str]:
|
|
151
|
+
err = item.get("error")
|
|
152
|
+
if isinstance(err, str) and err:
|
|
153
|
+
return err
|
|
154
|
+
if isinstance(err, dict):
|
|
155
|
+
msg = err.get("message") or err.get("error")
|
|
156
|
+
if isinstance(msg, str) and msg:
|
|
157
|
+
return msg
|
|
158
|
+
status = item.get("status")
|
|
159
|
+
if isinstance(status, str) and status.lower() in {"failed", "error"}:
|
|
160
|
+
return f"output item status: {status}"
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _as_dict(value: Any) -> Optional[Dict[str, Any]]:
|
|
165
|
+
return value if isinstance(value, dict) else None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _as_str(value: Any) -> str:
|
|
169
|
+
return value if isinstance(value, str) else ""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _optional_str(value: Any) -> Optional[str]:
|
|
173
|
+
return value if isinstance(value, str) and value else None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _coerce_float(*candidates: Any) -> Optional[float]:
|
|
177
|
+
for value in candidates:
|
|
178
|
+
if value is None:
|
|
179
|
+
continue
|
|
180
|
+
if isinstance(value, bool):
|
|
181
|
+
return 1.0 if value else 0.0
|
|
182
|
+
if isinstance(value, (int, float)):
|
|
183
|
+
return float(value)
|
|
184
|
+
if isinstance(value, str):
|
|
185
|
+
try:
|
|
186
|
+
return float(value)
|
|
187
|
+
except ValueError:
|
|
188
|
+
continue
|
|
189
|
+
return None
|