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
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Evaluator runtime for AgentOps 1.0.
|
|
2
|
+
|
|
3
|
+
Each :class:`EvaluatorPreset` from the catalog is instantiated lazily from
|
|
4
|
+
``azure.ai.evaluation`` and run against one dataset row. The runtime hides
|
|
5
|
+
SDK details (``model_config`` for AI-assisted evaluators, ``azure_ai_project``
|
|
6
|
+
for safety evaluators, kwarg mapping, score extraction).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
import inspect
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from agentops.core.evaluators import EvaluatorPreset
|
|
20
|
+
from agentops.core.results import RowMetric
|
|
21
|
+
|
|
22
|
+
# Evaluator classes that require an evaluator model via ``model_config``.
|
|
23
|
+
_AI_ASSISTED = {
|
|
24
|
+
"GroundednessEvaluator",
|
|
25
|
+
"RelevanceEvaluator",
|
|
26
|
+
"CoherenceEvaluator",
|
|
27
|
+
"FluencyEvaluator",
|
|
28
|
+
"SimilarityEvaluator",
|
|
29
|
+
"RetrievalEvaluator",
|
|
30
|
+
"ResponseCompletenessEvaluator",
|
|
31
|
+
"QAEvaluator",
|
|
32
|
+
"IntentResolutionEvaluator",
|
|
33
|
+
"TaskAdherenceEvaluator",
|
|
34
|
+
"ToolCallAccuracyEvaluator",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Evaluator classes that require ``azure_ai_project``.
|
|
38
|
+
_SAFETY = {
|
|
39
|
+
"ViolenceEvaluator",
|
|
40
|
+
"SexualEvaluator",
|
|
41
|
+
"SelfHarmEvaluator",
|
|
42
|
+
"HateUnfairnessEvaluator",
|
|
43
|
+
"ContentSafetyEvaluator",
|
|
44
|
+
"ProtectedMaterialEvaluator",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class EvaluatorRuntime:
|
|
50
|
+
"""A loaded, ready-to-call evaluator."""
|
|
51
|
+
|
|
52
|
+
preset: EvaluatorPreset
|
|
53
|
+
callable: Any # evaluator instance or sentinel for "latency"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Loading
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _credential() -> Any:
|
|
62
|
+
from azure.identity import DefaultAzureCredential # noqa: WPS433
|
|
63
|
+
|
|
64
|
+
return DefaultAzureCredential(exclude_developer_cli_credential=True, process_timeout=30)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _model_config() -> Dict[str, str]:
|
|
68
|
+
from agentops.utils.azure_endpoints import normalize_azure_openai_endpoint
|
|
69
|
+
|
|
70
|
+
raw_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
|
|
71
|
+
endpoint = normalize_azure_openai_endpoint(raw_endpoint)
|
|
72
|
+
deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT") or os.getenv(
|
|
73
|
+
"AZURE_AI_MODEL_DEPLOYMENT_NAME"
|
|
74
|
+
)
|
|
75
|
+
# The New Foundry "AI Services" inference endpoint rejects the
|
|
76
|
+
# azure-ai-evaluation SDK's stock api-version with
|
|
77
|
+
# ``BadRequest: API version not supported``. Default to a version
|
|
78
|
+
# known to work against both the New Foundry proxy and classic
|
|
79
|
+
# Azure OpenAI; allow override via AZURE_OPENAI_API_VERSION.
|
|
80
|
+
api_version = os.getenv("AZURE_OPENAI_API_VERSION") or "2025-04-01-preview"
|
|
81
|
+
|
|
82
|
+
missing = []
|
|
83
|
+
if not endpoint:
|
|
84
|
+
missing.append("AZURE_OPENAI_ENDPOINT")
|
|
85
|
+
if not deployment:
|
|
86
|
+
missing.append("AZURE_OPENAI_DEPLOYMENT")
|
|
87
|
+
if missing:
|
|
88
|
+
raise RuntimeError(
|
|
89
|
+
"AI-assisted evaluators require an evaluator model. "
|
|
90
|
+
"Missing environment variables: " + ", ".join(missing)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
config: Dict[str, str] = {
|
|
94
|
+
"azure_endpoint": endpoint, # type: ignore[dict-item]
|
|
95
|
+
"azure_deployment": deployment, # type: ignore[dict-item]
|
|
96
|
+
"api_version": api_version,
|
|
97
|
+
}
|
|
98
|
+
return config
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _project_endpoint() -> str:
|
|
102
|
+
endpoint = os.getenv("AZURE_AI_FOUNDRY_PROJECT_ENDPOINT")
|
|
103
|
+
if not endpoint:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
"Safety evaluators require AZURE_AI_FOUNDRY_PROJECT_ENDPOINT."
|
|
106
|
+
)
|
|
107
|
+
return endpoint
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_LATENCY_SENTINEL = object()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_evaluator(preset: EvaluatorPreset) -> EvaluatorRuntime:
|
|
114
|
+
"""Instantiate one evaluator. Raises a clear error if the SDK is missing."""
|
|
115
|
+
if preset.class_name == "_latency":
|
|
116
|
+
return EvaluatorRuntime(preset=preset, callable=_LATENCY_SENTINEL)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
module = importlib.import_module("azure.ai.evaluation")
|
|
120
|
+
except ImportError as exc:
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
"Evaluators require the 'azure-ai-evaluation' package. "
|
|
123
|
+
"Install the Foundry extra in this virtual environment. "
|
|
124
|
+
"Run: python -m pip install --upgrade 'agentops-accelerator[foundry]'"
|
|
125
|
+
) from exc
|
|
126
|
+
|
|
127
|
+
cls = getattr(module, preset.class_name, None)
|
|
128
|
+
if cls is None:
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
f"Evaluator class {preset.class_name!r} not found in azure.ai.evaluation"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
init_kwargs: Dict[str, Any] = {}
|
|
134
|
+
if preset.class_name in _AI_ASSISTED:
|
|
135
|
+
init_kwargs["model_config"] = _model_config()
|
|
136
|
+
if preset.class_name in _SAFETY:
|
|
137
|
+
init_kwargs["azure_ai_project"] = _project_endpoint()
|
|
138
|
+
init_kwargs["credential"] = _credential()
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
instance = cls(**init_kwargs) if inspect.isclass(cls) else cls
|
|
142
|
+
except TypeError:
|
|
143
|
+
# Some evaluators reject unexpected kwargs (e.g. F1ScoreEvaluator).
|
|
144
|
+
instance = cls() if inspect.isclass(cls) else cls
|
|
145
|
+
|
|
146
|
+
return EvaluatorRuntime(preset=preset, callable=instance)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def load_evaluators(presets: List[EvaluatorPreset]) -> List[EvaluatorRuntime]:
|
|
150
|
+
return [load_evaluator(preset) for preset in presets]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Execution
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
_PLACEHOLDERS = {
|
|
159
|
+
"$prompt": "input",
|
|
160
|
+
"$prediction": "response",
|
|
161
|
+
"$expected": "expected",
|
|
162
|
+
"$context": "context",
|
|
163
|
+
"$tool_calls": "tool_calls",
|
|
164
|
+
"$tool_definitions": "tool_definitions",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_conversation_messages(
|
|
169
|
+
*,
|
|
170
|
+
input_text: Optional[str],
|
|
171
|
+
response_text: str,
|
|
172
|
+
tool_calls: Any,
|
|
173
|
+
) -> Optional[Dict[str, List[Dict[str, Any]]]]:
|
|
174
|
+
"""Build conversation-style ``query`` and ``response`` for agent evaluators.
|
|
175
|
+
|
|
176
|
+
When the agent invoked tools, returning only the final answer text to
|
|
177
|
+
evaluators like ``IntentResolutionEvaluator`` and ``TaskAdherenceEvaluator``
|
|
178
|
+
leaves them blind to *how* the agent arrived at that answer. They then
|
|
179
|
+
consistently score it as 1/5 even when the agent did the right thing.
|
|
180
|
+
|
|
181
|
+
This helper returns a structured payload compatible with the
|
|
182
|
+
``azure.ai.evaluation`` conversational schema:
|
|
183
|
+
|
|
184
|
+
* ``query`` -> a single user message with the original input text
|
|
185
|
+
* ``response`` -> a sequence of assistant tool_call messages, optional
|
|
186
|
+
tool result messages (when each captured call has a ``result``
|
|
187
|
+
string), and a final assistant text message with the natural-language
|
|
188
|
+
answer.
|
|
189
|
+
|
|
190
|
+
Returns ``None`` when there are no tool calls to include - callers
|
|
191
|
+
should fall back to plain string kwargs in that case.
|
|
192
|
+
"""
|
|
193
|
+
has_tool_calls = isinstance(tool_calls, list) and len(tool_calls) > 0
|
|
194
|
+
|
|
195
|
+
query_messages: List[Dict[str, Any]] = [
|
|
196
|
+
{
|
|
197
|
+
"role": "user",
|
|
198
|
+
"content": [{"type": "text", "text": input_text or ""}],
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
response_messages: List[Dict[str, Any]] = []
|
|
203
|
+
if has_tool_calls:
|
|
204
|
+
for index, call in enumerate(tool_calls):
|
|
205
|
+
if not isinstance(call, dict):
|
|
206
|
+
continue
|
|
207
|
+
# Normalise across the OpenAI ``function_call`` shape and the
|
|
208
|
+
# nested ``function`` envelope produced by some Foundry payloads.
|
|
209
|
+
raw_function = call.get("function")
|
|
210
|
+
function: Dict[str, Any] = raw_function if isinstance(raw_function, dict) else {}
|
|
211
|
+
name = call.get("name") or function.get("name")
|
|
212
|
+
if not name:
|
|
213
|
+
continue
|
|
214
|
+
arguments = call.get("arguments")
|
|
215
|
+
if arguments is None:
|
|
216
|
+
arguments = function.get("arguments")
|
|
217
|
+
if isinstance(arguments, str):
|
|
218
|
+
try:
|
|
219
|
+
arguments = json.loads(arguments)
|
|
220
|
+
except json.JSONDecodeError:
|
|
221
|
+
# leave as raw string - evaluators tolerate either form
|
|
222
|
+
pass
|
|
223
|
+
tool_call_id = call.get("tool_call_id") or call.get("id") or f"call_{index}"
|
|
224
|
+
|
|
225
|
+
response_messages.append({
|
|
226
|
+
"role": "assistant",
|
|
227
|
+
"content": [{
|
|
228
|
+
"type": "tool_call",
|
|
229
|
+
"tool_call_id": tool_call_id,
|
|
230
|
+
"name": name,
|
|
231
|
+
"arguments": arguments if arguments is not None else {},
|
|
232
|
+
}],
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
result = call.get("result")
|
|
236
|
+
if isinstance(result, str) and result:
|
|
237
|
+
response_messages.append({
|
|
238
|
+
"role": "tool",
|
|
239
|
+
"tool_call_id": tool_call_id,
|
|
240
|
+
"content": [{"type": "tool_result", "tool_result": result}],
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
if response_text:
|
|
244
|
+
response_messages.append({
|
|
245
|
+
"role": "assistant",
|
|
246
|
+
"content": [{"type": "text", "text": response_text}],
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
if not response_messages:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
return {"query": query_messages, "response": response_messages}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _resolve_kwargs(
|
|
256
|
+
mapping: Dict[str, str],
|
|
257
|
+
*,
|
|
258
|
+
row: Dict[str, Any],
|
|
259
|
+
response: str,
|
|
260
|
+
) -> Dict[str, Any]:
|
|
261
|
+
resolved: Dict[str, Any] = {}
|
|
262
|
+
merged = {**row, "response": response, "input": row.get("input")}
|
|
263
|
+
for kwarg, placeholder in mapping.items():
|
|
264
|
+
if not isinstance(placeholder, str) or not placeholder.startswith("$"):
|
|
265
|
+
resolved[kwarg] = placeholder
|
|
266
|
+
continue
|
|
267
|
+
source_key = _PLACEHOLDERS.get(placeholder)
|
|
268
|
+
if source_key is None:
|
|
269
|
+
raise ValueError(f"unknown evaluator placeholder {placeholder!r}")
|
|
270
|
+
value = merged.get(source_key)
|
|
271
|
+
if value is None:
|
|
272
|
+
continue
|
|
273
|
+
resolved[kwarg] = value
|
|
274
|
+
return resolved
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _extract_score(payload: Any, score_key: str) -> Optional[float]:
|
|
278
|
+
if payload is None:
|
|
279
|
+
return None
|
|
280
|
+
if isinstance(payload, (int, float)):
|
|
281
|
+
return float(payload)
|
|
282
|
+
if not isinstance(payload, dict):
|
|
283
|
+
return None
|
|
284
|
+
for candidate in (
|
|
285
|
+
score_key,
|
|
286
|
+
f"{score_key}_score",
|
|
287
|
+
f"gpt_{score_key}",
|
|
288
|
+
"score",
|
|
289
|
+
):
|
|
290
|
+
value = payload.get(candidate)
|
|
291
|
+
if isinstance(value, bool):
|
|
292
|
+
return 1.0 if value else 0.0
|
|
293
|
+
if isinstance(value, (int, float)):
|
|
294
|
+
return float(value)
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _extract_reason(payload: Any, score_key: str) -> Optional[str]:
|
|
299
|
+
if not isinstance(payload, dict):
|
|
300
|
+
return None
|
|
301
|
+
for candidate in (
|
|
302
|
+
f"{score_key}_reason",
|
|
303
|
+
f"{score_key}_reasoning",
|
|
304
|
+
f"gpt_{score_key}_reason",
|
|
305
|
+
"reason",
|
|
306
|
+
"reasoning",
|
|
307
|
+
):
|
|
308
|
+
value = payload.get(candidate)
|
|
309
|
+
if isinstance(value, str) and value.strip():
|
|
310
|
+
return value
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def run_evaluator(
|
|
315
|
+
runtime: EvaluatorRuntime,
|
|
316
|
+
*,
|
|
317
|
+
row: Dict[str, Any],
|
|
318
|
+
response: str,
|
|
319
|
+
latency_seconds: float,
|
|
320
|
+
actual_tool_calls: Optional[List[Any]] = None,
|
|
321
|
+
) -> RowMetric:
|
|
322
|
+
"""Execute one evaluator on one row. Captures errors so the run continues."""
|
|
323
|
+
preset = runtime.preset
|
|
324
|
+
if runtime.callable is _LATENCY_SENTINEL:
|
|
325
|
+
return RowMetric(name=preset.score_key, value=float(latency_seconds))
|
|
326
|
+
|
|
327
|
+
# ToolCallAccuracyEvaluator: special handling when the agent made no
|
|
328
|
+
# tool calls. The Azure SDK evaluator raises ("No tool calls found in
|
|
329
|
+
# response...") which would surface as ERR. Translate that into a
|
|
330
|
+
# meaningful score:
|
|
331
|
+
# * dataset has no tool_calls either -> not applicable (n/a).
|
|
332
|
+
# * dataset expected tool_calls -> the agent failed to call them, so
|
|
333
|
+
# score it as 0.0 instead of crashing the row.
|
|
334
|
+
if preset.class_name == "ToolCallAccuracyEvaluator":
|
|
335
|
+
has_actual = isinstance(actual_tool_calls, list) and len(actual_tool_calls) > 0
|
|
336
|
+
has_dataset = isinstance(row.get("tool_calls"), list) and len(row["tool_calls"]) > 0
|
|
337
|
+
if not has_actual:
|
|
338
|
+
if has_dataset:
|
|
339
|
+
return RowMetric(
|
|
340
|
+
name=preset.score_key,
|
|
341
|
+
value=0.0,
|
|
342
|
+
reason="agent made no tool calls but the dataset expected some",
|
|
343
|
+
)
|
|
344
|
+
return RowMetric(
|
|
345
|
+
name=preset.score_key,
|
|
346
|
+
value=None,
|
|
347
|
+
reason="not applicable: agent made no tool calls",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
kwargs = _resolve_kwargs(preset.input_mapping, row=row, response=response)
|
|
352
|
+
if preset.needs_conversation:
|
|
353
|
+
# Prefer the actual calls made by the agent during invocation;
|
|
354
|
+
# fall back to the dataset's expected calls if the runner did
|
|
355
|
+
# not provide any (e.g. unit tests).
|
|
356
|
+
tool_calls_for_convo = (
|
|
357
|
+
actual_tool_calls
|
|
358
|
+
if actual_tool_calls is not None
|
|
359
|
+
else row.get("tool_calls")
|
|
360
|
+
)
|
|
361
|
+
conversation = _build_conversation_messages(
|
|
362
|
+
input_text=row.get("input"),
|
|
363
|
+
response_text=response,
|
|
364
|
+
tool_calls=tool_calls_for_convo,
|
|
365
|
+
)
|
|
366
|
+
if conversation is not None:
|
|
367
|
+
# Upgrade query/response from plain strings to the
|
|
368
|
+
# conversational schema. Both kwargs are guaranteed to be
|
|
369
|
+
# in input_mapping for evaluators that opt into this.
|
|
370
|
+
if "query" in kwargs:
|
|
371
|
+
kwargs["query"] = conversation["query"]
|
|
372
|
+
if "response" in kwargs:
|
|
373
|
+
kwargs["response"] = conversation["response"]
|
|
374
|
+
|
|
375
|
+
# Retry once on transient Azure CLI credential failures. The
|
|
376
|
+
# az CLI occasionally fails to launch on Windows under heavy
|
|
377
|
+
# I/O; DefaultAzureCredential's other sources usually succeed
|
|
378
|
+
# on the second attempt because the token has been cached.
|
|
379
|
+
last_exc: Optional[Exception] = None
|
|
380
|
+
for attempt in range(2):
|
|
381
|
+
try:
|
|
382
|
+
result = runtime.callable(**kwargs)
|
|
383
|
+
last_exc = None
|
|
384
|
+
break
|
|
385
|
+
except Exception as exc: # noqa: BLE001
|
|
386
|
+
last_exc = exc
|
|
387
|
+
if attempt == 0 and _is_transient_credential_error(exc):
|
|
388
|
+
time.sleep(0.5)
|
|
389
|
+
continue
|
|
390
|
+
raise
|
|
391
|
+
if last_exc is not None: # pragma: no cover - defensive
|
|
392
|
+
raise last_exc
|
|
393
|
+
score = _extract_score(result, preset.score_key)
|
|
394
|
+
reason = _extract_reason(result, preset.score_key)
|
|
395
|
+
return RowMetric(name=preset.score_key, value=score, reason=reason)
|
|
396
|
+
except Exception as exc: # noqa: BLE001
|
|
397
|
+
return RowMetric(name=preset.score_key, error=str(exc))
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
_TRANSIENT_CRED_MARKERS = (
|
|
401
|
+
"failed to invoke the azure cli",
|
|
402
|
+
"azureclicredential",
|
|
403
|
+
"credentialunavailableerror",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _is_transient_credential_error(exc: Exception) -> bool:
|
|
408
|
+
msg = str(exc).lower()
|
|
409
|
+
return any(marker in msg for marker in _TRANSIENT_CRED_MARKERS)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Threshold evaluation against parsed :class:`Threshold` rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
from agentops.core.agentops_config import Threshold
|
|
8
|
+
from agentops.core.results import ThresholdEvaluation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def evaluate(
|
|
12
|
+
rules: List[Threshold],
|
|
13
|
+
metrics: Dict[str, float],
|
|
14
|
+
) -> List[ThresholdEvaluation]:
|
|
15
|
+
"""Apply each rule against the aggregate metric value.
|
|
16
|
+
|
|
17
|
+
Missing metrics produce a failed evaluation with ``actual="missing"`` so
|
|
18
|
+
the report can show the gap clearly rather than crashing the run.
|
|
19
|
+
"""
|
|
20
|
+
results: List[ThresholdEvaluation] = []
|
|
21
|
+
for rule in rules:
|
|
22
|
+
actual_value = metrics.get(rule.metric)
|
|
23
|
+
|
|
24
|
+
if rule.criteria in {"true", "false"}:
|
|
25
|
+
expected = rule.criteria
|
|
26
|
+
actual = "missing"
|
|
27
|
+
passed = False
|
|
28
|
+
if actual_value is not None:
|
|
29
|
+
actual_bool = actual_value == 1.0
|
|
30
|
+
actual = "true" if actual_bool else "false"
|
|
31
|
+
passed = actual == expected
|
|
32
|
+
results.append(
|
|
33
|
+
ThresholdEvaluation(
|
|
34
|
+
metric=rule.metric,
|
|
35
|
+
criteria=rule.criteria,
|
|
36
|
+
expected=expected,
|
|
37
|
+
actual=actual,
|
|
38
|
+
passed=passed,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
if rule.value is None:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"threshold for {rule.metric!r} requires a numeric value"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
target = float(rule.value)
|
|
49
|
+
expected_str = f"{rule.criteria}{target:g}"
|
|
50
|
+
if actual_value is None:
|
|
51
|
+
results.append(
|
|
52
|
+
ThresholdEvaluation(
|
|
53
|
+
metric=rule.metric,
|
|
54
|
+
criteria=rule.criteria,
|
|
55
|
+
expected=expected_str,
|
|
56
|
+
actual="missing",
|
|
57
|
+
passed=False,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if rule.criteria == ">=":
|
|
63
|
+
passed = actual_value >= target
|
|
64
|
+
elif rule.criteria == ">":
|
|
65
|
+
passed = actual_value > target
|
|
66
|
+
elif rule.criteria == "<=":
|
|
67
|
+
passed = actual_value <= target
|
|
68
|
+
elif rule.criteria == "<":
|
|
69
|
+
passed = actual_value < target
|
|
70
|
+
elif rule.criteria == "==":
|
|
71
|
+
passed = actual_value == target
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"unsupported criteria {rule.criteria!r}")
|
|
74
|
+
|
|
75
|
+
results.append(
|
|
76
|
+
ThresholdEvaluation(
|
|
77
|
+
metric=rule.metric,
|
|
78
|
+
criteria=rule.criteria,
|
|
79
|
+
expected=expected_str,
|
|
80
|
+
actual=f"{actual_value:g}",
|
|
81
|
+
passed=passed,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Application services package."""
|