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,347 @@
|
|
|
1
|
+
"""Shared judge-model client and disk cache for LLM-assisted checks.
|
|
2
|
+
|
|
3
|
+
The client is intentionally minimal:
|
|
4
|
+
|
|
5
|
+
* Lazy-imports ``azure.ai.projects`` and ``openai`` so the base CLI does
|
|
6
|
+
not require Azure SDKs.
|
|
7
|
+
* One :func:`LLMJudge.call` per rule. The caller supplies a Pydantic
|
|
8
|
+
schema; we ask the judge for a JSON response and validate against it.
|
|
9
|
+
* A disk cache at ``.agentops/cache/llm/<hash>.json`` keeps repeated
|
|
10
|
+
Doctor runs at zero token cost while inputs are unchanged.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, Optional, Type, TypeVar
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel, ValidationError
|
|
24
|
+
|
|
25
|
+
from agentops.agent.config import LLMAssistCheckConfig
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
T = TypeVar("T", bound=BaseModel)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class JudgementMeta:
|
|
34
|
+
"""Bookkeeping returned alongside a judge model's verdict."""
|
|
35
|
+
|
|
36
|
+
cache_hit: bool = False
|
|
37
|
+
input_tokens: int = 0
|
|
38
|
+
output_tokens: int = 0
|
|
39
|
+
model_deployment: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LLMJudge:
|
|
43
|
+
"""Thin wrapper around the Foundry project's OpenAI client."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
config: LLMAssistCheckConfig,
|
|
48
|
+
workspace: Path,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.config = config
|
|
51
|
+
self.workspace = workspace
|
|
52
|
+
self._client: Any = None
|
|
53
|
+
self._deployment: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# Resolution helpers
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def resolve_deployment(self) -> Optional[str]:
|
|
60
|
+
"""Find a deployment to use as the judge model.
|
|
61
|
+
|
|
62
|
+
Order of resolution:
|
|
63
|
+
|
|
64
|
+
1. Explicit ``deployment_name`` in ``LLMAssistCheckConfig``.
|
|
65
|
+
2. ``deployment_name_env`` env var (default
|
|
66
|
+
``AZURE_AI_MODEL_DEPLOYMENT_NAME``).
|
|
67
|
+
3. **Auto-discovery**: list deployments on the Foundry project
|
|
68
|
+
and pick a chat-capable one, preferring smaller / cheaper
|
|
69
|
+
models so judge calls do not blow up the project's token
|
|
70
|
+
bill.
|
|
71
|
+
|
|
72
|
+
Returns ``None`` only when none of the three paths yields a
|
|
73
|
+
deployment.
|
|
74
|
+
"""
|
|
75
|
+
if self._deployment is not None:
|
|
76
|
+
return self._deployment
|
|
77
|
+
|
|
78
|
+
deployment = self.config.deployment_name
|
|
79
|
+
if not deployment and self.config.deployment_name_env:
|
|
80
|
+
deployment = os.environ.get(self.config.deployment_name_env)
|
|
81
|
+
|
|
82
|
+
if not deployment:
|
|
83
|
+
deployment = self._auto_discover_deployment()
|
|
84
|
+
|
|
85
|
+
self._deployment = deployment or None
|
|
86
|
+
return self._deployment
|
|
87
|
+
|
|
88
|
+
# Preference ranking for auto-discovery. Cheaper / smaller chat
|
|
89
|
+
# models come first so the Doctor stays light on quota by default.
|
|
90
|
+
_DEPLOYMENT_PREFERENCE = (
|
|
91
|
+
"gpt-5.4-mini",
|
|
92
|
+
"gpt-5-mini",
|
|
93
|
+
"gpt-4o-mini",
|
|
94
|
+
"gpt-4.1-mini",
|
|
95
|
+
"gpt-5.4",
|
|
96
|
+
"gpt-5",
|
|
97
|
+
"gpt-4o",
|
|
98
|
+
"gpt-4.1",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _auto_discover_deployment(self) -> Optional[str]:
|
|
102
|
+
"""List Foundry project deployments and pick a judge-suitable one."""
|
|
103
|
+
endpoint = self._resolve_endpoint()
|
|
104
|
+
if not endpoint:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
from azure.ai.projects import AIProjectClient # type: ignore
|
|
108
|
+
from azure.identity import DefaultAzureCredential # type: ignore
|
|
109
|
+
except ImportError: # pragma: no cover
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
project_client = AIProjectClient(
|
|
114
|
+
endpoint=endpoint,
|
|
115
|
+
credential=DefaultAzureCredential(exclude_developer_cli_credential=True, process_timeout=30),
|
|
116
|
+
)
|
|
117
|
+
accessor = getattr(project_client, "deployments", None) or getattr(
|
|
118
|
+
project_client, "models", None
|
|
119
|
+
)
|
|
120
|
+
if accessor is None:
|
|
121
|
+
return None
|
|
122
|
+
list_fn = getattr(accessor, "list", None)
|
|
123
|
+
if list_fn is None:
|
|
124
|
+
return None
|
|
125
|
+
names = []
|
|
126
|
+
for raw in list_fn():
|
|
127
|
+
name = (
|
|
128
|
+
getattr(raw, "name", None)
|
|
129
|
+
or getattr(raw, "deployment_name", None)
|
|
130
|
+
or getattr(raw, "id", None)
|
|
131
|
+
)
|
|
132
|
+
if name:
|
|
133
|
+
names.append(str(name))
|
|
134
|
+
except Exception as exc: # pragma: no cover
|
|
135
|
+
log.info("llm_assist: deployment auto-discovery failed: %s", exc)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
if not names:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
# 1. Prefer known good models from the ranked list.
|
|
142
|
+
for preferred in self._DEPLOYMENT_PREFERENCE:
|
|
143
|
+
for name in names:
|
|
144
|
+
if name.lower() == preferred or preferred in name.lower():
|
|
145
|
+
log.info(
|
|
146
|
+
"llm_assist: auto-selected deployment %s (preferred)",
|
|
147
|
+
name,
|
|
148
|
+
)
|
|
149
|
+
return name
|
|
150
|
+
|
|
151
|
+
# 2. Otherwise prefer anything with "mini" in the name (smaller
|
|
152
|
+
# = cheaper judge calls).
|
|
153
|
+
for name in names:
|
|
154
|
+
if "mini" in name.lower():
|
|
155
|
+
log.info(
|
|
156
|
+
"llm_assist: auto-selected deployment %s (mini fallback)",
|
|
157
|
+
name,
|
|
158
|
+
)
|
|
159
|
+
return name
|
|
160
|
+
|
|
161
|
+
# 3. Last resort - first non-embedding deployment.
|
|
162
|
+
for name in names:
|
|
163
|
+
if "embedding" not in name.lower():
|
|
164
|
+
log.info(
|
|
165
|
+
"llm_assist: auto-selected deployment %s (first non-embedding)",
|
|
166
|
+
name,
|
|
167
|
+
)
|
|
168
|
+
return name
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def _resolve_endpoint(self) -> Optional[str]:
|
|
173
|
+
if self.config.project_endpoint:
|
|
174
|
+
return self.config.project_endpoint
|
|
175
|
+
if self.config.project_endpoint_env:
|
|
176
|
+
return os.environ.get(self.config.project_endpoint_env)
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
# Cache helpers
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def _cache_path(self, inputs_hash: str) -> Path:
|
|
184
|
+
return (
|
|
185
|
+
self.workspace
|
|
186
|
+
/ ".agentops"
|
|
187
|
+
/ "cache"
|
|
188
|
+
/ "llm"
|
|
189
|
+
/ f"{inputs_hash}.json"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _read_cache(self, inputs_hash: str) -> Optional[Dict[str, Any]]:
|
|
193
|
+
path = self._cache_path(inputs_hash)
|
|
194
|
+
if not path.is_file():
|
|
195
|
+
return None
|
|
196
|
+
try:
|
|
197
|
+
stat = path.stat()
|
|
198
|
+
except OSError:
|
|
199
|
+
return None
|
|
200
|
+
age_days = (time.time() - stat.st_mtime) / 86400.0
|
|
201
|
+
if (
|
|
202
|
+
self.config.cache_ttl_days > 0
|
|
203
|
+
and age_days > self.config.cache_ttl_days
|
|
204
|
+
):
|
|
205
|
+
return None
|
|
206
|
+
try:
|
|
207
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
208
|
+
except (OSError, json.JSONDecodeError):
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def _write_cache(self, inputs_hash: str, payload: Dict[str, Any]) -> None:
|
|
212
|
+
path = self._cache_path(inputs_hash)
|
|
213
|
+
try:
|
|
214
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
215
|
+
path.write_text(
|
|
216
|
+
json.dumps(payload, indent=2, default=str),
|
|
217
|
+
encoding="utf-8",
|
|
218
|
+
)
|
|
219
|
+
except OSError as exc:
|
|
220
|
+
log.warning("LLM cache write failed at %s: %s", path, exc)
|
|
221
|
+
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
# Client init
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def _get_client(self) -> Optional[Any]:
|
|
227
|
+
if self._client is not None:
|
|
228
|
+
return self._client
|
|
229
|
+
endpoint = self._resolve_endpoint()
|
|
230
|
+
if not endpoint:
|
|
231
|
+
log.info("llm_assist: project endpoint not configured")
|
|
232
|
+
return None
|
|
233
|
+
try:
|
|
234
|
+
from azure.ai.projects import AIProjectClient # type: ignore
|
|
235
|
+
from azure.identity import DefaultAzureCredential # type: ignore
|
|
236
|
+
except ImportError as exc: # pragma: no cover
|
|
237
|
+
log.info("llm_assist: azure-ai-projects unavailable: %s", exc)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
project_client = AIProjectClient(
|
|
242
|
+
endpoint=endpoint,
|
|
243
|
+
credential=DefaultAzureCredential(exclude_developer_cli_credential=True, process_timeout=30),
|
|
244
|
+
)
|
|
245
|
+
# Foundry exposes get_openai_client without an api_version arg;
|
|
246
|
+
# never pass one (the SDK picks the right version).
|
|
247
|
+
get_openai_client = getattr(project_client, "get_openai_client", None)
|
|
248
|
+
if get_openai_client is None:
|
|
249
|
+
inference = getattr(project_client, "inference", None)
|
|
250
|
+
get_openai_client = getattr(inference, "get_openai_client", None)
|
|
251
|
+
if get_openai_client is None:
|
|
252
|
+
log.info("llm_assist: AIProjectClient has no OpenAI client helper")
|
|
253
|
+
return None
|
|
254
|
+
self._client = get_openai_client()
|
|
255
|
+
except Exception as exc: # pragma: no cover
|
|
256
|
+
log.warning("llm_assist: openai client init failed: %s", exc)
|
|
257
|
+
return None
|
|
258
|
+
return self._client
|
|
259
|
+
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
# Public call
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
def call(
|
|
265
|
+
self,
|
|
266
|
+
*,
|
|
267
|
+
system: str,
|
|
268
|
+
user: str,
|
|
269
|
+
schema: Type[T],
|
|
270
|
+
inputs_hash: str,
|
|
271
|
+
) -> Optional[tuple[T, JudgementMeta]]:
|
|
272
|
+
"""Run the judge model and return a parsed ``schema`` instance.
|
|
273
|
+
|
|
274
|
+
Returns ``None`` when the call cannot be made (missing endpoint,
|
|
275
|
+
SDK not installed, judge raised) or when validation fails. The
|
|
276
|
+
Doctor stays silent in those cases - LLM checks are advisory.
|
|
277
|
+
"""
|
|
278
|
+
cached = self._read_cache(inputs_hash)
|
|
279
|
+
if cached is not None:
|
|
280
|
+
try:
|
|
281
|
+
verdict = schema.model_validate(cached["verdict"])
|
|
282
|
+
meta = JudgementMeta(
|
|
283
|
+
cache_hit=True,
|
|
284
|
+
model_deployment=cached.get("model_deployment"),
|
|
285
|
+
)
|
|
286
|
+
return verdict, meta
|
|
287
|
+
except (ValidationError, KeyError, TypeError):
|
|
288
|
+
log.info(
|
|
289
|
+
"llm_assist: cached entry %s invalid; ignoring", inputs_hash
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
client = self._get_client()
|
|
293
|
+
deployment = self.resolve_deployment()
|
|
294
|
+
if client is None or not deployment:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
response = client.chat.completions.create(
|
|
299
|
+
model=deployment,
|
|
300
|
+
temperature=0.0,
|
|
301
|
+
response_format={"type": "json_object"},
|
|
302
|
+
messages=[
|
|
303
|
+
{"role": "system", "content": system},
|
|
304
|
+
{"role": "user", "content": user},
|
|
305
|
+
],
|
|
306
|
+
)
|
|
307
|
+
except Exception as exc: # pragma: no cover
|
|
308
|
+
log.warning("llm_assist: judge call failed: %s", exc)
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
raw = ""
|
|
312
|
+
usage = None
|
|
313
|
+
try:
|
|
314
|
+
raw = response.choices[0].message.content or ""
|
|
315
|
+
usage = getattr(response, "usage", None)
|
|
316
|
+
except (AttributeError, IndexError) as exc: # pragma: no cover
|
|
317
|
+
log.warning("llm_assist: judge response malformed: %s", exc)
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
payload = json.loads(raw)
|
|
322
|
+
verdict = schema.model_validate(payload)
|
|
323
|
+
except (json.JSONDecodeError, ValidationError) as exc:
|
|
324
|
+
log.warning("llm_assist: judge JSON invalid: %s", exc)
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
meta = JudgementMeta(
|
|
328
|
+
cache_hit=False,
|
|
329
|
+
input_tokens=int(getattr(usage, "prompt_tokens", 0) or 0)
|
|
330
|
+
if usage
|
|
331
|
+
else 0,
|
|
332
|
+
output_tokens=int(getattr(usage, "completion_tokens", 0) or 0)
|
|
333
|
+
if usage
|
|
334
|
+
else 0,
|
|
335
|
+
model_deployment=deployment,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
self._write_cache(
|
|
339
|
+
inputs_hash,
|
|
340
|
+
{
|
|
341
|
+
"verdict": verdict.model_dump(),
|
|
342
|
+
"model_deployment": deployment,
|
|
343
|
+
"input_tokens": meta.input_tokens,
|
|
344
|
+
"output_tokens": meta.output_tokens,
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
return verdict, meta
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""LLM-judged Responsible-AI checks against the evaluation dataset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from agentops.agent.findings import Category, Finding
|
|
9
|
+
from agentops.agent.llm_assist._base import (
|
|
10
|
+
BaseVerdict,
|
|
11
|
+
FindingBuilderArgs,
|
|
12
|
+
build_llm_finding,
|
|
13
|
+
hash_text,
|
|
14
|
+
)
|
|
15
|
+
from agentops.agent.llm_assist._client import LLMJudge
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_PII_SYSTEM = """You audit a sample of eval dataset rows for the
|
|
19
|
+
Microsoft Responsible AI Standard's Privacy & Security principle.
|
|
20
|
+
The rows are JSONL records. You decide whether the sample contains
|
|
21
|
+
personally identifiable information (PII).
|
|
22
|
+
|
|
23
|
+
Categories that count: personal names tied to context, email
|
|
24
|
+
addresses, phone numbers, government / employee / student ids,
|
|
25
|
+
financial account numbers, residential addresses, dates of birth tied
|
|
26
|
+
to a real-looking identity, IP addresses tied to a specific person.
|
|
27
|
+
|
|
28
|
+
Public-figure examples or obvious placeholder data (e.g. "John Doe",
|
|
29
|
+
"test@example.com") do NOT count.
|
|
30
|
+
|
|
31
|
+
Respond as compact JSON. Do NOT echo the offending row text back -
|
|
32
|
+
just describe categories and a count.
|
|
33
|
+
|
|
34
|
+
{"risk": "low|medium|high", "confidence": <0.0-1.0>,
|
|
35
|
+
"reasoning": "<one short paragraph>",
|
|
36
|
+
"suggestions": ["<fix 1>", "<fix 2>", "<fix 3>"],
|
|
37
|
+
"categories": ["name" | "email" | "phone" | "id" | "address" |
|
|
38
|
+
"dob" | "ip", ...],
|
|
39
|
+
"row_count": <int>}
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PIIVerdict(BaseVerdict):
|
|
44
|
+
categories: List[str] = []
|
|
45
|
+
row_count: int = 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_BIAS_SYSTEM = """You audit a sample of eval dataset rows for the
|
|
49
|
+
Microsoft Responsible AI Standard's Fairness principle. You decide
|
|
50
|
+
whether the row sample shows demographic / role / domain skew that
|
|
51
|
+
could bias the agent's evaluated quality.
|
|
52
|
+
|
|
53
|
+
Examples of signal:
|
|
54
|
+
|
|
55
|
+
* All examples reference a single gender, age group, or ethnicity.
|
|
56
|
+
* All scenarios target one industry / domain / region.
|
|
57
|
+
* Tone or register is uniform (only formal, or only casual).
|
|
58
|
+
* All examples represent the "happy path"; edge cases are absent.
|
|
59
|
+
|
|
60
|
+
Respond as compact JSON. Do not echo dataset rows.
|
|
61
|
+
|
|
62
|
+
{"risk": "low|medium|high", "confidence": <0.0-1.0>,
|
|
63
|
+
"reasoning": "<one short paragraph>",
|
|
64
|
+
"suggestions": ["<fix 1>", "<fix 2>", "<fix 3>"],
|
|
65
|
+
"skew_axes": ["gender" | "age" | "domain" | "tone" | "happy_path" |
|
|
66
|
+
"geography" | "other", ...]}
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BiasVerdict(BaseVerdict):
|
|
71
|
+
skew_axes: List[str] = []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _load_dataset_sample(workspace: Path, max_rows: int) -> Optional[str]:
|
|
75
|
+
data_dir = workspace / ".agentops" / "data"
|
|
76
|
+
if not data_dir.is_dir():
|
|
77
|
+
return None
|
|
78
|
+
rows: List[str] = []
|
|
79
|
+
for path in sorted(data_dir.glob("*.jsonl")):
|
|
80
|
+
try:
|
|
81
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
82
|
+
for line in handle:
|
|
83
|
+
line = line.strip()
|
|
84
|
+
if not line:
|
|
85
|
+
continue
|
|
86
|
+
rows.append(line)
|
|
87
|
+
if len(rows) >= max_rows:
|
|
88
|
+
break
|
|
89
|
+
except OSError:
|
|
90
|
+
continue
|
|
91
|
+
if len(rows) >= max_rows:
|
|
92
|
+
break
|
|
93
|
+
if not rows:
|
|
94
|
+
return None
|
|
95
|
+
return "\n".join(rows)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_dataset_pii_risk(
|
|
99
|
+
judge: LLMJudge,
|
|
100
|
+
workspace: Path,
|
|
101
|
+
max_rows: int,
|
|
102
|
+
min_confidence: float,
|
|
103
|
+
) -> List[Finding]:
|
|
104
|
+
sample = _load_dataset_sample(workspace, max_rows)
|
|
105
|
+
if sample is None:
|
|
106
|
+
return []
|
|
107
|
+
ih = hash_text("dataset_pii", str(max_rows), sample)
|
|
108
|
+
result = judge.call(
|
|
109
|
+
system=_PII_SYSTEM,
|
|
110
|
+
user=f"Dataset rows (one JSON per line):\n```\n{sample}\n```",
|
|
111
|
+
schema=PIIVerdict,
|
|
112
|
+
inputs_hash=ih,
|
|
113
|
+
)
|
|
114
|
+
if result is None:
|
|
115
|
+
return []
|
|
116
|
+
verdict, meta = result
|
|
117
|
+
if verdict.confidence < min_confidence:
|
|
118
|
+
return []
|
|
119
|
+
finding = build_llm_finding(
|
|
120
|
+
FindingBuilderArgs(
|
|
121
|
+
rule_id="responsible_ai.llm.dataset_pii_risk",
|
|
122
|
+
title="Possible PII detected in evaluation dataset",
|
|
123
|
+
category=Category.RESPONSIBLE_AI,
|
|
124
|
+
summary_template=(
|
|
125
|
+
"The judge model flagged possible PII in the dataset "
|
|
126
|
+
"sample (risk={risk}): {reasoning}"
|
|
127
|
+
),
|
|
128
|
+
recommendation=(
|
|
129
|
+
"Audit `.agentops/data/*.jsonl` for personally "
|
|
130
|
+
"identifiable information. Replace real names, email "
|
|
131
|
+
"addresses, phone numbers, and ids with placeholders "
|
|
132
|
+
"(`john.doe@example.com`, `+1-555-0100`). Bump the "
|
|
133
|
+
"dataset `version:` field after the scrub so historical "
|
|
134
|
+
"runs remain comparable."
|
|
135
|
+
),
|
|
136
|
+
verdict=verdict,
|
|
137
|
+
meta=meta,
|
|
138
|
+
extra_evidence={
|
|
139
|
+
"categories": getattr(verdict, "categories", []),
|
|
140
|
+
"row_count_judged": int(getattr(verdict, "row_count", 0)),
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
return [finding] if finding is not None else []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def check_dataset_bias_signals(
|
|
148
|
+
judge: LLMJudge,
|
|
149
|
+
workspace: Path,
|
|
150
|
+
max_rows: int,
|
|
151
|
+
min_confidence: float,
|
|
152
|
+
) -> List[Finding]:
|
|
153
|
+
sample = _load_dataset_sample(workspace, max_rows)
|
|
154
|
+
if sample is None:
|
|
155
|
+
return []
|
|
156
|
+
ih = hash_text("dataset_bias", str(max_rows), sample)
|
|
157
|
+
result = judge.call(
|
|
158
|
+
system=_BIAS_SYSTEM,
|
|
159
|
+
user=f"Dataset rows (one JSON per line):\n```\n{sample}\n```",
|
|
160
|
+
schema=BiasVerdict,
|
|
161
|
+
inputs_hash=ih,
|
|
162
|
+
)
|
|
163
|
+
if result is None:
|
|
164
|
+
return []
|
|
165
|
+
verdict, meta = result
|
|
166
|
+
if verdict.confidence < min_confidence:
|
|
167
|
+
return []
|
|
168
|
+
finding = build_llm_finding(
|
|
169
|
+
FindingBuilderArgs(
|
|
170
|
+
rule_id="responsible_ai.llm.dataset_bias_signals",
|
|
171
|
+
title="Evaluation dataset shows distribution skew",
|
|
172
|
+
category=Category.RESPONSIBLE_AI,
|
|
173
|
+
summary_template=(
|
|
174
|
+
"The judge model identified distribution skew in the "
|
|
175
|
+
"dataset sample (risk={risk}): {reasoning}"
|
|
176
|
+
),
|
|
177
|
+
recommendation=(
|
|
178
|
+
"Diversify the dataset along the flagged axes (gender, "
|
|
179
|
+
"age, domain, tone, geography, happy/sad paths). The "
|
|
180
|
+
"Microsoft Responsible AI Standard's Fairness principle "
|
|
181
|
+
"asks for representative test data; uniform samples "
|
|
182
|
+
"underestimate the agent's real-world failure rate."
|
|
183
|
+
),
|
|
184
|
+
verdict=verdict,
|
|
185
|
+
meta=meta,
|
|
186
|
+
extra_evidence={
|
|
187
|
+
"skew_axes": getattr(verdict, "skew_axes", []),
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
return [finding] if finding is not None else []
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Top-level entry point for the LLM-assisted check suite."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional, Set
|
|
7
|
+
|
|
8
|
+
from agentops.agent.config import LLMAssistCheckConfig
|
|
9
|
+
from agentops.agent.findings import Finding
|
|
10
|
+
from agentops.agent.llm_assist._bundle_rule import check_bundle_coverage
|
|
11
|
+
from agentops.agent.llm_assist._client import LLMJudge
|
|
12
|
+
from agentops.agent.llm_assist._dataset_rules import (
|
|
13
|
+
check_dataset_bias_signals,
|
|
14
|
+
check_dataset_pii_risk,
|
|
15
|
+
)
|
|
16
|
+
from agentops.agent.llm_assist._prompt_rules import (
|
|
17
|
+
check_prompt_jailbreak_surface,
|
|
18
|
+
check_prompt_safety_guardrails,
|
|
19
|
+
check_prompt_transparency,
|
|
20
|
+
)
|
|
21
|
+
from agentops.agent.sources.foundry_control import FoundryControlPayload
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_ALL_RULES = (
|
|
25
|
+
"rai.prompt_transparency",
|
|
26
|
+
"rai.prompt_safety_guardrails",
|
|
27
|
+
"rai.prompt_jailbreak_surface",
|
|
28
|
+
"rai.dataset_pii_risk",
|
|
29
|
+
"rai.dataset_bias_signals",
|
|
30
|
+
"opex.bundle_coverage",
|
|
31
|
+
"opex.spec_conformance.llm.implementation_gap",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _enabled_rules(config: LLMAssistCheckConfig) -> Set[str]:
|
|
36
|
+
if not config.rules:
|
|
37
|
+
return set(_ALL_RULES)
|
|
38
|
+
return {r.strip() for r in config.rules if r and r.strip()}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_llm_assist_check(
|
|
42
|
+
workspace: Path,
|
|
43
|
+
config: LLMAssistCheckConfig,
|
|
44
|
+
foundry: Optional[FoundryControlPayload],
|
|
45
|
+
) -> List[Finding]:
|
|
46
|
+
"""Run all enabled LLM-judged rules and merge their findings.
|
|
47
|
+
|
|
48
|
+
Short-circuits cleanly when the suite is disabled, the judge model
|
|
49
|
+
cannot be reached, or there is no Foundry agent / dataset to read.
|
|
50
|
+
"""
|
|
51
|
+
if not config.enabled:
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
judge = LLMJudge(config=config, workspace=workspace)
|
|
55
|
+
# Skip the whole suite when no deployment is resolvable - the judge
|
|
56
|
+
# cannot be called and we would just log warnings repeatedly.
|
|
57
|
+
if not judge.resolve_deployment():
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
enabled = _enabled_rules(config)
|
|
61
|
+
findings: List[Finding] = []
|
|
62
|
+
|
|
63
|
+
agents = list(foundry.agents) if foundry and foundry.agents else []
|
|
64
|
+
if agents:
|
|
65
|
+
if "rai.prompt_transparency" in enabled:
|
|
66
|
+
findings.extend(
|
|
67
|
+
check_prompt_transparency(judge, agents, config.min_confidence)
|
|
68
|
+
)
|
|
69
|
+
if "rai.prompt_safety_guardrails" in enabled:
|
|
70
|
+
findings.extend(
|
|
71
|
+
check_prompt_safety_guardrails(
|
|
72
|
+
judge, agents, config.min_confidence
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
if "rai.prompt_jailbreak_surface" in enabled:
|
|
76
|
+
findings.extend(
|
|
77
|
+
check_prompt_jailbreak_surface(
|
|
78
|
+
judge, agents, config.min_confidence
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if "rai.dataset_pii_risk" in enabled:
|
|
83
|
+
findings.extend(
|
|
84
|
+
check_dataset_pii_risk(
|
|
85
|
+
judge,
|
|
86
|
+
workspace,
|
|
87
|
+
config.max_dataset_rows,
|
|
88
|
+
config.min_confidence,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
if "rai.dataset_bias_signals" in enabled:
|
|
92
|
+
findings.extend(
|
|
93
|
+
check_dataset_bias_signals(
|
|
94
|
+
judge,
|
|
95
|
+
workspace,
|
|
96
|
+
config.max_dataset_rows,
|
|
97
|
+
config.min_confidence,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if agents and "opex.bundle_coverage" in enabled:
|
|
102
|
+
findings.extend(
|
|
103
|
+
check_bundle_coverage(judge, workspace, agents, config.min_confidence)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return findings
|