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,300 @@
|
|
|
1
|
+
"""Promote production trace exports into reviewable regression datasets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import textwrap
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Iterable, Literal, Optional
|
|
11
|
+
|
|
12
|
+
_TEXT_WRAP_WIDTH = 92
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
LabelMode = Literal["self-similarity", "pending"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class TracePromotionPreview:
|
|
20
|
+
"""Result of transforming traces into candidate dataset rows."""
|
|
21
|
+
|
|
22
|
+
source: Path
|
|
23
|
+
output_path: Path
|
|
24
|
+
manifest_path: Path
|
|
25
|
+
rows: list[dict[str, Any]]
|
|
26
|
+
skipped: int
|
|
27
|
+
label_mode: LabelMode
|
|
28
|
+
warnings: list[str] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def promote_traces(
|
|
32
|
+
*,
|
|
33
|
+
source: Path,
|
|
34
|
+
output_path: Path,
|
|
35
|
+
max_rows: int = 50,
|
|
36
|
+
label_mode: LabelMode = "self-similarity",
|
|
37
|
+
apply: bool = False,
|
|
38
|
+
) -> TracePromotionPreview:
|
|
39
|
+
"""Convert a JSON/JSONL trace export into AgentOps dataset rows.
|
|
40
|
+
|
|
41
|
+
``self-similarity`` stores the production response as ``expected`` so future
|
|
42
|
+
evals catch behavior drift against a known production answer. ``pending``
|
|
43
|
+
leaves ``expected`` empty and marks every row for human completion.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if max_rows <= 0:
|
|
47
|
+
raise ValueError("max_rows must be greater than zero")
|
|
48
|
+
if label_mode not in {"self-similarity", "pending"}:
|
|
49
|
+
raise ValueError("label_mode must be self-similarity or pending")
|
|
50
|
+
if not source.exists():
|
|
51
|
+
raise FileNotFoundError(
|
|
52
|
+
f"trace source not found: {source}. Export Foundry/App Insights traces "
|
|
53
|
+
"first, or use the sample created by `agentops init` at "
|
|
54
|
+
".agentops/traces/sample-traces.jsonl."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
traces = _load_trace_export(source)
|
|
58
|
+
rows: list[dict[str, Any]] = []
|
|
59
|
+
warnings: list[str] = []
|
|
60
|
+
skipped = 0
|
|
61
|
+
seen: set[tuple[str, str]] = set()
|
|
62
|
+
|
|
63
|
+
for trace in traces:
|
|
64
|
+
if len(rows) >= max_rows:
|
|
65
|
+
break
|
|
66
|
+
candidate = _trace_to_row(trace, label_mode)
|
|
67
|
+
if candidate is None:
|
|
68
|
+
skipped += 1
|
|
69
|
+
continue
|
|
70
|
+
key = (candidate["input"], candidate.get("expected", ""))
|
|
71
|
+
if key in seen:
|
|
72
|
+
skipped += 1
|
|
73
|
+
continue
|
|
74
|
+
seen.add(key)
|
|
75
|
+
rows.append(candidate)
|
|
76
|
+
|
|
77
|
+
if not rows:
|
|
78
|
+
warnings.append("No usable input/response pairs were found in the trace export.")
|
|
79
|
+
if label_mode == "self-similarity":
|
|
80
|
+
warnings.append(
|
|
81
|
+
"Rows use production responses as expected values. Treat this as drift detection, not human-verified ground truth."
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
warnings.append(
|
|
85
|
+
"Rows are pending human labels. Fill expected/context/tool fields before using this file as a blocking gate."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
manifest_path = output_path.with_name("trace-regression-manifest.json")
|
|
89
|
+
preview = TracePromotionPreview(
|
|
90
|
+
source=source,
|
|
91
|
+
output_path=output_path,
|
|
92
|
+
manifest_path=manifest_path,
|
|
93
|
+
rows=rows,
|
|
94
|
+
skipped=skipped,
|
|
95
|
+
label_mode=label_mode,
|
|
96
|
+
warnings=warnings,
|
|
97
|
+
)
|
|
98
|
+
if apply:
|
|
99
|
+
_write_trace_dataset(preview)
|
|
100
|
+
return preview
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def render_trace_promotion_preview(preview: TracePromotionPreview) -> str:
|
|
104
|
+
"""Render a concise terminal-friendly summary."""
|
|
105
|
+
|
|
106
|
+
lines = [
|
|
107
|
+
"AgentOps trace-to-dataset preview",
|
|
108
|
+
f"Source: {preview.source}",
|
|
109
|
+
f"Output: {preview.output_path}",
|
|
110
|
+
"",
|
|
111
|
+
"Summary",
|
|
112
|
+
]
|
|
113
|
+
lines.extend(
|
|
114
|
+
_render_text_fields(
|
|
115
|
+
[
|
|
116
|
+
("rows", str(len(preview.rows))),
|
|
117
|
+
("skipped", str(preview.skipped)),
|
|
118
|
+
("label mode", preview.label_mode),
|
|
119
|
+
]
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
if preview.warnings:
|
|
123
|
+
lines.append("")
|
|
124
|
+
lines.append("Warnings")
|
|
125
|
+
for warning in preview.warnings:
|
|
126
|
+
lines.extend(_wrapped_status_line("warn", "warning", warning))
|
|
127
|
+
if preview.rows:
|
|
128
|
+
lines.append("")
|
|
129
|
+
lines.append("Sample rows")
|
|
130
|
+
for index, row in enumerate(preview.rows[:3], start=1):
|
|
131
|
+
lines.extend(_wrapped_numbered_line(index, str(row["input"])[:100]))
|
|
132
|
+
return "\n".join(lines) + "\n"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _render_text_fields(rows: list[tuple[str, str]]) -> list[str]:
|
|
136
|
+
width = max(len(label) for label, _ in rows)
|
|
137
|
+
lines: list[str] = []
|
|
138
|
+
for label, value in rows:
|
|
139
|
+
lines.extend(_wrap_text(value, indent=f" {label.ljust(width)} "))
|
|
140
|
+
return lines
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _wrapped_status_line(status: str, label: str, text: str) -> list[str]:
|
|
144
|
+
prefix = f" {status.ljust(4)} {label.ljust(10)} "
|
|
145
|
+
wrapped = textwrap.wrap(
|
|
146
|
+
text,
|
|
147
|
+
width=_TEXT_WRAP_WIDTH,
|
|
148
|
+
initial_indent=prefix,
|
|
149
|
+
subsequent_indent=" " * len(prefix),
|
|
150
|
+
break_long_words=False,
|
|
151
|
+
break_on_hyphens=False,
|
|
152
|
+
)
|
|
153
|
+
return wrapped or [prefix.rstrip()]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _wrapped_numbered_line(index: int, text: str) -> list[str]:
|
|
157
|
+
prefix = f" {index}. "
|
|
158
|
+
wrapped = textwrap.wrap(
|
|
159
|
+
text,
|
|
160
|
+
width=_TEXT_WRAP_WIDTH,
|
|
161
|
+
initial_indent=prefix,
|
|
162
|
+
subsequent_indent=" " * len(prefix),
|
|
163
|
+
break_long_words=False,
|
|
164
|
+
break_on_hyphens=False,
|
|
165
|
+
)
|
|
166
|
+
return wrapped or [prefix.rstrip()]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _wrap_text(text: str, *, indent: str) -> list[str]:
|
|
170
|
+
return textwrap.wrap(
|
|
171
|
+
text,
|
|
172
|
+
width=_TEXT_WRAP_WIDTH,
|
|
173
|
+
initial_indent=indent,
|
|
174
|
+
subsequent_indent=indent,
|
|
175
|
+
break_long_words=False,
|
|
176
|
+
break_on_hyphens=False,
|
|
177
|
+
) or [indent.rstrip()]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _load_trace_export(source: Path) -> Iterable[dict[str, Any]]:
|
|
181
|
+
text = source.read_text(encoding="utf-8")
|
|
182
|
+
stripped = text.strip()
|
|
183
|
+
if not stripped:
|
|
184
|
+
return []
|
|
185
|
+
if stripped.startswith("["):
|
|
186
|
+
payload = json.loads(stripped)
|
|
187
|
+
if not isinstance(payload, list):
|
|
188
|
+
raise ValueError("JSON trace export must be an array of objects")
|
|
189
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
190
|
+
|
|
191
|
+
rows: list[dict[str, Any]] = []
|
|
192
|
+
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
193
|
+
item = line.strip()
|
|
194
|
+
if not item:
|
|
195
|
+
continue
|
|
196
|
+
try:
|
|
197
|
+
payload = json.loads(item)
|
|
198
|
+
except json.JSONDecodeError as exc:
|
|
199
|
+
raise ValueError(f"{source}: invalid JSON on line {line_number}: {exc}") from exc
|
|
200
|
+
if isinstance(payload, dict):
|
|
201
|
+
rows.append(payload)
|
|
202
|
+
return rows
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _trace_to_row(trace: dict[str, Any], label_mode: LabelMode) -> Optional[dict[str, Any]]:
|
|
206
|
+
input_text = _first_text(
|
|
207
|
+
trace,
|
|
208
|
+
"input",
|
|
209
|
+
"query",
|
|
210
|
+
"prompt",
|
|
211
|
+
"message",
|
|
212
|
+
"user_message",
|
|
213
|
+
"request",
|
|
214
|
+
"customDimensions.input",
|
|
215
|
+
"customDimensions.query",
|
|
216
|
+
"customDimensions.gen_ai.prompt",
|
|
217
|
+
)
|
|
218
|
+
response_text = _first_text(
|
|
219
|
+
trace,
|
|
220
|
+
"response",
|
|
221
|
+
"output",
|
|
222
|
+
"answer",
|
|
223
|
+
"completion",
|
|
224
|
+
"assistant_message",
|
|
225
|
+
"customDimensions.response",
|
|
226
|
+
"customDimensions.output",
|
|
227
|
+
"customDimensions.gen_ai.completion",
|
|
228
|
+
)
|
|
229
|
+
if not input_text or not response_text:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
metadata = {
|
|
233
|
+
"source": "production_trace",
|
|
234
|
+
"trace_id": _first_text(trace, "trace_id", "operation_Id", "operationId", "id"),
|
|
235
|
+
"timestamp": _first_text(trace, "timestamp", "time", "TimeGenerated"),
|
|
236
|
+
"label_mode": label_mode,
|
|
237
|
+
"needs_review": True,
|
|
238
|
+
}
|
|
239
|
+
row: dict[str, Any] = {
|
|
240
|
+
"input": input_text,
|
|
241
|
+
"expected": response_text if label_mode == "self-similarity" else "",
|
|
242
|
+
"metadata": {k: v for k, v in metadata.items() if v not in (None, "")},
|
|
243
|
+
}
|
|
244
|
+
context = _first_text(trace, "context", "grounding", "retrieved_context", "customDimensions.context")
|
|
245
|
+
if context:
|
|
246
|
+
row["context"] = context
|
|
247
|
+
tool_calls = _first_value(trace, "tool_calls", "customDimensions.tool_calls")
|
|
248
|
+
if tool_calls:
|
|
249
|
+
row["tool_calls"] = tool_calls
|
|
250
|
+
return row
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _write_trace_dataset(preview: TracePromotionPreview) -> None:
|
|
254
|
+
preview.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
255
|
+
with preview.output_path.open("w", encoding="utf-8") as handle:
|
|
256
|
+
for row in preview.rows:
|
|
257
|
+
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
258
|
+
|
|
259
|
+
manifest = {
|
|
260
|
+
"version": 1,
|
|
261
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
262
|
+
"source": str(preview.source),
|
|
263
|
+
"dataset_path": str(preview.output_path),
|
|
264
|
+
"rows": len(preview.rows),
|
|
265
|
+
"skipped": preview.skipped,
|
|
266
|
+
"label_mode": preview.label_mode,
|
|
267
|
+
"human_review_required": True,
|
|
268
|
+
}
|
|
269
|
+
preview.manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _first_text(data: dict[str, Any], *keys: str) -> Optional[str]:
|
|
273
|
+
value = _first_value(data, *keys)
|
|
274
|
+
if value is None:
|
|
275
|
+
return None
|
|
276
|
+
if isinstance(value, str):
|
|
277
|
+
text = value.strip()
|
|
278
|
+
return text or None
|
|
279
|
+
if isinstance(value, (dict, list)):
|
|
280
|
+
text = json.dumps(value, ensure_ascii=False)
|
|
281
|
+
return text if text not in ("{}", "[]") else None
|
|
282
|
+
text = str(value).strip()
|
|
283
|
+
return text or None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _first_value(data: dict[str, Any], *keys: str) -> Any:
|
|
287
|
+
for key in keys:
|
|
288
|
+
value = _lookup(data, key)
|
|
289
|
+
if value not in (None, "", [], {}):
|
|
290
|
+
return value
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _lookup(data: dict[str, Any], key: str) -> Any:
|
|
295
|
+
current: Any = data
|
|
296
|
+
for part in key.split("."):
|
|
297
|
+
if not isinstance(current, dict):
|
|
298
|
+
return None
|
|
299
|
+
current = current.get(part)
|
|
300
|
+
return current
|