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,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