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.
Files changed (142) hide show
  1. agentops/__init__.py +10 -0
  2. agentops/__main__.py +6 -0
  3. agentops/agent/__init__.py +12 -0
  4. agentops/agent/_legacy_ids.py +92 -0
  5. agentops/agent/analyzer.py +207 -0
  6. agentops/agent/checks/__init__.py +1 -0
  7. agentops/agent/checks/catalog.py +880 -0
  8. agentops/agent/checks/errors.py +279 -0
  9. agentops/agent/checks/foundry_config.py +75 -0
  10. agentops/agent/checks/latency.py +84 -0
  11. agentops/agent/checks/opex.py +157 -0
  12. agentops/agent/checks/opex_workspace.py +874 -0
  13. agentops/agent/checks/posture.py +36 -0
  14. agentops/agent/checks/posture_rules/__init__.py +53 -0
  15. agentops/agent/checks/posture_rules/content_filter.py +59 -0
  16. agentops/agent/checks/posture_rules/diagnostics.py +74 -0
  17. agentops/agent/checks/posture_rules/local_auth.py +55 -0
  18. agentops/agent/checks/posture_rules/managed_identity.py +59 -0
  19. agentops/agent/checks/posture_rules/network.py +68 -0
  20. agentops/agent/checks/regression.py +78 -0
  21. agentops/agent/checks/release_readiness.py +182 -0
  22. agentops/agent/checks/safety.py +247 -0
  23. agentops/agent/checks/spec_conformance.py +375 -0
  24. agentops/agent/cockpit.py +5159 -0
  25. agentops/agent/config.py +240 -0
  26. agentops/agent/findings.py +113 -0
  27. agentops/agent/history.py +142 -0
  28. agentops/agent/knowledge/__init__.py +182 -0
  29. agentops/agent/knowledge/waf-checklist.csv +39 -0
  30. agentops/agent/llm_assist/__init__.py +16 -0
  31. agentops/agent/llm_assist/_base.py +124 -0
  32. agentops/agent/llm_assist/_bundle_rule.py +154 -0
  33. agentops/agent/llm_assist/_client.py +347 -0
  34. agentops/agent/llm_assist/_dataset_rules.py +191 -0
  35. agentops/agent/llm_assist/_engine.py +106 -0
  36. agentops/agent/llm_assist/_prompt_rules.py +291 -0
  37. agentops/agent/llm_assist/_spec_rules.py +235 -0
  38. agentops/agent/production_telemetry.py +430 -0
  39. agentops/agent/report.py +207 -0
  40. agentops/agent/server/__init__.py +1 -0
  41. agentops/agent/server/app.py +84 -0
  42. agentops/agent/server/auth.py +94 -0
  43. agentops/agent/server/chat.py +44 -0
  44. agentops/agent/server/protocol.py +72 -0
  45. agentops/agent/sources/__init__.py +1 -0
  46. agentops/agent/sources/azure_monitor.py +523 -0
  47. agentops/agent/sources/azure_resources.py +602 -0
  48. agentops/agent/sources/foundry_control.py +174 -0
  49. agentops/agent/sources/results_history.py +494 -0
  50. agentops/agent/sources/spec_detectors/__init__.py +42 -0
  51. agentops/agent/sources/spec_detectors/_base.py +58 -0
  52. agentops/agent/sources/spec_detectors/agents_md.py +75 -0
  53. agentops/agent/sources/spec_detectors/spec_kit.py +172 -0
  54. agentops/agent/time_range.py +117 -0
  55. agentops/cli/__init__.py +1 -0
  56. agentops/cli/app.py +4823 -0
  57. agentops/core/__init__.py +1 -0
  58. agentops/core/agentops_config.py +592 -0
  59. agentops/core/config_loader.py +22 -0
  60. agentops/core/evaluators.py +480 -0
  61. agentops/core/release_evidence.py +56 -0
  62. agentops/core/results.py +117 -0
  63. agentops/mcp/__init__.py +10 -0
  64. agentops/mcp/server.py +232 -0
  65. agentops/pipeline/__init__.py +8 -0
  66. agentops/pipeline/cloud_results.py +189 -0
  67. agentops/pipeline/cloud_runner.py +901 -0
  68. agentops/pipeline/comparison.py +108 -0
  69. agentops/pipeline/diagnostics.py +51 -0
  70. agentops/pipeline/invocations.py +535 -0
  71. agentops/pipeline/official_eval.py +414 -0
  72. agentops/pipeline/orchestrator.py +775 -0
  73. agentops/pipeline/prompt_deploy.py +377 -0
  74. agentops/pipeline/publisher.py +121 -0
  75. agentops/pipeline/reporter.py +202 -0
  76. agentops/pipeline/runtime.py +409 -0
  77. agentops/pipeline/thresholds.py +84 -0
  78. agentops/services/__init__.py +1 -0
  79. agentops/services/cicd.py +720 -0
  80. agentops/services/eval_analysis.py +848 -0
  81. agentops/services/evidence_pack.py +757 -0
  82. agentops/services/initializer.py +86 -0
  83. agentops/services/preflight.py +470 -0
  84. agentops/services/setup_wizard.py +709 -0
  85. agentops/services/skills.py +643 -0
  86. agentops/services/trace_promotion.py +300 -0
  87. agentops/services/workflow_analysis.py +1129 -0
  88. agentops/templates/.gitignore +15 -0
  89. agentops/templates/__init__.py +1 -0
  90. agentops/templates/agent-server/Dockerfile +23 -0
  91. agentops/templates/agent-server/README.md +61 -0
  92. agentops/templates/agent-server/main.bicep +94 -0
  93. agentops/templates/agent.yaml +87 -0
  94. agentops/templates/agentops.yaml +58 -0
  95. agentops/templates/foundry.svg +71 -0
  96. agentops/templates/icon.png +0 -0
  97. agentops/templates/pipelines/azuredevops/agentops-deploy-dev-azd.yml +118 -0
  98. agentops/templates/pipelines/azuredevops/agentops-deploy-dev.yml +73 -0
  99. agentops/templates/pipelines/azuredevops/agentops-deploy-prod-azd.yml +141 -0
  100. agentops/templates/pipelines/azuredevops/agentops-deploy-prod.yml +94 -0
  101. agentops/templates/pipelines/azuredevops/agentops-deploy-prompt-agent.yml +167 -0
  102. agentops/templates/pipelines/azuredevops/agentops-deploy-qa-azd.yml +118 -0
  103. agentops/templates/pipelines/azuredevops/agentops-deploy-qa.yml +68 -0
  104. agentops/templates/pipelines/azuredevops/agentops-pr-prompt-agent.yml +210 -0
  105. agentops/templates/pipelines/azuredevops/agentops-pr.yml +155 -0
  106. agentops/templates/pipelines/azuredevops/agentops-watchdog.yml +106 -0
  107. agentops/templates/project.gitignore +36 -0
  108. agentops/templates/sample-traces.jsonl +3 -0
  109. agentops/templates/skills/agentops-agent/SKILL.md +137 -0
  110. agentops/templates/skills/agentops-config/SKILL.md +113 -0
  111. agentops/templates/skills/agentops-dataset/SKILL.md +84 -0
  112. agentops/templates/skills/agentops-eval/SKILL.md +189 -0
  113. agentops/templates/skills/agentops-report/SKILL.md +71 -0
  114. agentops/templates/skills/agentops-workflow/SKILL.md +471 -0
  115. agentops/templates/smoke.jsonl +3 -0
  116. agentops/templates/waf-checklist.README.md +84 -0
  117. agentops/templates/waf-checklist.csv +22 -0
  118. agentops/templates/workflows/agentops-deploy-dev-azd.yml +166 -0
  119. agentops/templates/workflows/agentops-deploy-dev.yml +187 -0
  120. agentops/templates/workflows/agentops-deploy-prod-azd.yml +183 -0
  121. agentops/templates/workflows/agentops-deploy-prod.yml +171 -0
  122. agentops/templates/workflows/agentops-deploy-prompt-agent.yml +197 -0
  123. agentops/templates/workflows/agentops-deploy-qa-azd.yml +156 -0
  124. agentops/templates/workflows/agentops-deploy-qa.yml +145 -0
  125. agentops/templates/workflows/agentops-pr-prompt-agent.yml +210 -0
  126. agentops/templates/workflows/agentops-pr.yml +148 -0
  127. agentops/templates/workflows/agentops-watchdog.yml +122 -0
  128. agentops/utils/__init__.py +1 -0
  129. agentops/utils/azd_env.py +435 -0
  130. agentops/utils/azure_endpoints.py +62 -0
  131. agentops/utils/colors.py +47 -0
  132. agentops/utils/dotenv_loader.py +105 -0
  133. agentops/utils/foundry_discovery.py +229 -0
  134. agentops/utils/logging.py +59 -0
  135. agentops/utils/telemetry.py +554 -0
  136. agentops/utils/yaml.py +36 -0
  137. agentops_accelerator-0.3.0.dist-info/METADATA +278 -0
  138. agentops_accelerator-0.3.0.dist-info/RECORD +142 -0
  139. agentops_accelerator-0.3.0.dist-info/WHEEL +5 -0
  140. agentops_accelerator-0.3.0.dist-info/entry_points.txt +2 -0
  141. agentops_accelerator-0.3.0.dist-info/licenses/LICENSE +21 -0
  142. 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."""