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,1129 @@
|
|
|
1
|
+
"""Read-only CI/CD analysis for Azure AI accelerator repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import textwrap
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
from agentops.core.agentops_config import classify_agent
|
|
12
|
+
from agentops.pipeline.official_eval import (
|
|
13
|
+
AGENTOPS_CLOUD_RUNNER,
|
|
14
|
+
AGENTOPS_LOCAL_RUNNER,
|
|
15
|
+
OFFICIAL_EVAL_RUNNER,
|
|
16
|
+
analyze_official_eval_support,
|
|
17
|
+
official_eval_action_ref,
|
|
18
|
+
)
|
|
19
|
+
from agentops.utils.yaml import load_yaml
|
|
20
|
+
|
|
21
|
+
_TEXT_LIMIT = 200_000
|
|
22
|
+
_SCAN_LIMIT = 80
|
|
23
|
+
_TEXT_WRAP_WIDTH = 92
|
|
24
|
+
_IGNORE_PARTS = {
|
|
25
|
+
".agentops",
|
|
26
|
+
".azure",
|
|
27
|
+
".git",
|
|
28
|
+
".github",
|
|
29
|
+
".mypy_cache",
|
|
30
|
+
".pytest_cache",
|
|
31
|
+
".ruff_cache",
|
|
32
|
+
".venv",
|
|
33
|
+
"__pycache__",
|
|
34
|
+
"build",
|
|
35
|
+
"dist",
|
|
36
|
+
"node_modules",
|
|
37
|
+
"site-packages",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class WorkflowSignal:
|
|
43
|
+
"""A local file-system signal used to classify CI/CD shape."""
|
|
44
|
+
|
|
45
|
+
key: str
|
|
46
|
+
label: str
|
|
47
|
+
detail: str
|
|
48
|
+
path: Optional[str] = None
|
|
49
|
+
confidence: str = "high"
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> Dict[str, str]:
|
|
52
|
+
data = {
|
|
53
|
+
"key": self.key,
|
|
54
|
+
"label": self.label,
|
|
55
|
+
"detail": self.detail,
|
|
56
|
+
"confidence": self.confidence,
|
|
57
|
+
}
|
|
58
|
+
if self.path:
|
|
59
|
+
data["path"] = self.path
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class WorkflowStage:
|
|
65
|
+
"""Recommended pipeline stage."""
|
|
66
|
+
|
|
67
|
+
name: str
|
|
68
|
+
owner: str
|
|
69
|
+
purpose: str
|
|
70
|
+
commands: List[str] = field(default_factory=list)
|
|
71
|
+
notes: List[str] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
74
|
+
return {
|
|
75
|
+
"name": self.name,
|
|
76
|
+
"owner": self.owner,
|
|
77
|
+
"purpose": self.purpose,
|
|
78
|
+
"commands": list(self.commands),
|
|
79
|
+
"notes": list(self.notes),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class WorkflowAnalysis:
|
|
85
|
+
"""Stable result contract for `agentops workflow analyze`."""
|
|
86
|
+
|
|
87
|
+
version: int
|
|
88
|
+
directory: str
|
|
89
|
+
classification: str
|
|
90
|
+
recommended_deploy_mode: str
|
|
91
|
+
recommended_eval_runner: str
|
|
92
|
+
deployment_strategy: str
|
|
93
|
+
eval_strategy: str
|
|
94
|
+
complexity: str
|
|
95
|
+
requires_copilot_adaptation: bool
|
|
96
|
+
copilot_skills_installed: bool
|
|
97
|
+
copilot_prompt: Optional[str] = None
|
|
98
|
+
official_eval_reasons: List[str] = field(default_factory=list)
|
|
99
|
+
official_evaluators: List[str] = field(default_factory=list)
|
|
100
|
+
signals: List[WorkflowSignal] = field(default_factory=list)
|
|
101
|
+
warnings: List[str] = field(default_factory=list)
|
|
102
|
+
recommended_commands: List[str] = field(default_factory=list)
|
|
103
|
+
stages: List[WorkflowStage] = field(default_factory=list)
|
|
104
|
+
next_steps: List[str] = field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
107
|
+
return {
|
|
108
|
+
"version": self.version,
|
|
109
|
+
"directory": self.directory,
|
|
110
|
+
"classification": self.classification,
|
|
111
|
+
"recommended_deploy_mode": self.recommended_deploy_mode,
|
|
112
|
+
"recommended_eval_runner": self.recommended_eval_runner,
|
|
113
|
+
"deployment_strategy": self.deployment_strategy,
|
|
114
|
+
"eval_strategy": self.eval_strategy,
|
|
115
|
+
"complexity": self.complexity,
|
|
116
|
+
"requires_copilot_adaptation": self.requires_copilot_adaptation,
|
|
117
|
+
"copilot_skills_installed": self.copilot_skills_installed,
|
|
118
|
+
"copilot_prompt": self.copilot_prompt,
|
|
119
|
+
"official_eval_reasons": list(self.official_eval_reasons),
|
|
120
|
+
"official_evaluators": list(self.official_evaluators),
|
|
121
|
+
"signals": [signal.to_dict() for signal in self.signals],
|
|
122
|
+
"warnings": list(self.warnings),
|
|
123
|
+
"recommended_commands": list(self.recommended_commands),
|
|
124
|
+
"stages": [stage.to_dict() for stage in self.stages],
|
|
125
|
+
"next_steps": list(self.next_steps),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def analyze_workflow_project(directory: Path) -> WorkflowAnalysis:
|
|
130
|
+
"""Analyze a copied accelerator/app repo and recommend CI/CD shape.
|
|
131
|
+
|
|
132
|
+
This is intentionally local-only: it does not call Azure, `azd`, GitHub, or
|
|
133
|
+
Foundry. The goal is to give a coding agent and the user a grounded plan
|
|
134
|
+
before generating or adapting workflow files.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
root = directory.resolve()
|
|
138
|
+
signals: List[WorkflowSignal] = []
|
|
139
|
+
warnings: List[str] = []
|
|
140
|
+
|
|
141
|
+
azure_yaml = root / "azure.yaml"
|
|
142
|
+
has_azd = azure_yaml.exists()
|
|
143
|
+
if has_azd:
|
|
144
|
+
signals.append(
|
|
145
|
+
WorkflowSignal(
|
|
146
|
+
"azd_project",
|
|
147
|
+
"Azure Developer CLI project",
|
|
148
|
+
"azure.yaml found; prefer azd for provision/deploy lifecycle.",
|
|
149
|
+
_rel(root, azure_yaml),
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
agentops = _agentops_signal(root)
|
|
154
|
+
prompt_agent = bool(agentops.get("prompt_agent"))
|
|
155
|
+
if agentops:
|
|
156
|
+
signals.append(agentops["signal"])
|
|
157
|
+
if agentops.get("prompt_file"):
|
|
158
|
+
signals.append(
|
|
159
|
+
WorkflowSignal(
|
|
160
|
+
"prompt_file",
|
|
161
|
+
"Source-controlled prompt file",
|
|
162
|
+
"Prompt-agent CI/CD can create a candidate Foundry version from this file.",
|
|
163
|
+
str(agentops["prompt_file"]),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
bicep_files = _find_files(root, "*.bicep")
|
|
168
|
+
if bicep_files:
|
|
169
|
+
signals.append(
|
|
170
|
+
WorkflowSignal(
|
|
171
|
+
"bicep_infra",
|
|
172
|
+
"Bicep infrastructure",
|
|
173
|
+
f"Found {len(bicep_files)} Bicep file(s); keep infra changes behind azd/Bicep review.",
|
|
174
|
+
_rel(root, bicep_files[0]),
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
manifest = _read_json(root / "manifest.json")
|
|
179
|
+
ailz_manifest = isinstance(manifest, dict) and any(
|
|
180
|
+
key in manifest for key in ("ailz_tag", "ailz_version", "components")
|
|
181
|
+
)
|
|
182
|
+
if ailz_manifest:
|
|
183
|
+
signals.append(
|
|
184
|
+
WorkflowSignal(
|
|
185
|
+
"ailz_manifest",
|
|
186
|
+
"AI Landing Zone manifest",
|
|
187
|
+
"manifest.json pins landing-zone/components; treat it as release input.",
|
|
188
|
+
"manifest.json",
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
ailz_preflight = has_ailz_preflight(root)
|
|
193
|
+
if ailz_preflight:
|
|
194
|
+
signals.append(
|
|
195
|
+
WorkflowSignal(
|
|
196
|
+
"ailz_preflight",
|
|
197
|
+
"AI Landing Zone preflight",
|
|
198
|
+
"Invoke-PreflightChecks.ps1 found; run it before azd provision in CI/CD.",
|
|
199
|
+
"scripts/Invoke-PreflightChecks.ps1",
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
infra_text = "\n".join(_read_text(path) for path in _infra_scan_files(root, bicep_files))
|
|
204
|
+
readme_text = _read_text(root / "README.md")
|
|
205
|
+
readme_lower = readme_text.lower()
|
|
206
|
+
|
|
207
|
+
network_isolated = _has_network_isolation(infra_text)
|
|
208
|
+
if network_isolated:
|
|
209
|
+
signals.append(
|
|
210
|
+
WorkflowSignal(
|
|
211
|
+
"network_isolation",
|
|
212
|
+
"Network-isolated Azure AI topology",
|
|
213
|
+
"Structural infra mentions private endpoints, VNet/jumpbox, firewall, or NETWORK_ISOLATION.",
|
|
214
|
+
confidence="high",
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
warnings.append(
|
|
218
|
+
"Network-isolated deployments often cannot complete all data-plane "
|
|
219
|
+
"steps from GitHub-hosted runners. Use azd hooks plus a self-hosted "
|
|
220
|
+
"runner in the VNet, jumpbox handoff, or ACR Tasks agent pool for "
|
|
221
|
+
"private build/deploy/post-provision work."
|
|
222
|
+
)
|
|
223
|
+
elif "network isolation" in readme_lower or "private endpoint" in readme_lower:
|
|
224
|
+
signals.append(
|
|
225
|
+
WorkflowSignal(
|
|
226
|
+
"network_isolation_hint",
|
|
227
|
+
"Network isolation mentioned",
|
|
228
|
+
"README mentions network isolation/private endpoints; verify infra before choosing runner topology.",
|
|
229
|
+
"README.md",
|
|
230
|
+
confidence="medium",
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if _looks_like_container_app(root, infra_text, readme_lower):
|
|
235
|
+
signals.append(
|
|
236
|
+
WorkflowSignal(
|
|
237
|
+
"container_app",
|
|
238
|
+
"Containerized Azure app",
|
|
239
|
+
"Container App/Docker signals found; build and deploy steps are project-specific.",
|
|
240
|
+
confidence="high",
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
accelerator_hint = _accelerator_hint(readme_lower)
|
|
245
|
+
if accelerator_hint:
|
|
246
|
+
signals.append(accelerator_hint)
|
|
247
|
+
|
|
248
|
+
existing_ci = _existing_ci_signal(root)
|
|
249
|
+
if existing_ci:
|
|
250
|
+
signals.append(existing_ci)
|
|
251
|
+
|
|
252
|
+
if not (root / "agentops.yaml").exists():
|
|
253
|
+
warnings.append(
|
|
254
|
+
"No agentops.yaml found. Run `agentops init` and prove `agentops eval run` locally before making the pipeline blocking."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
official_eval_reasons: List[str] = []
|
|
258
|
+
official_evaluators: List[str] = []
|
|
259
|
+
recommended_eval_runner = AGENTOPS_LOCAL_RUNNER
|
|
260
|
+
if (root / "agentops.yaml").exists():
|
|
261
|
+
official_support = analyze_official_eval_support(root / "agentops.yaml")
|
|
262
|
+
official_eval_reasons = list(official_support.reasons)
|
|
263
|
+
official_evaluators = list(official_support.official_evaluators)
|
|
264
|
+
if official_support.eligible:
|
|
265
|
+
recommended_eval_runner = AGENTOPS_CLOUD_RUNNER
|
|
266
|
+
signals.append(
|
|
267
|
+
WorkflowSignal(
|
|
268
|
+
"agentops_cloud_evaluation",
|
|
269
|
+
"AgentOps cloud eval runner",
|
|
270
|
+
"prompt agent and dataset are compatible; CI can run Foundry cloud eval with AgentOps threshold enforcement.",
|
|
271
|
+
"agentops.yaml",
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
warnings.extend(official_support.warnings)
|
|
275
|
+
else:
|
|
276
|
+
warnings.append(
|
|
277
|
+
"Microsoft Foundry AI Agent Evaluation not selected: "
|
|
278
|
+
+ " ".join(official_support.reasons)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
recommended_deploy_mode = _recommended_deploy_mode(has_azd, prompt_agent)
|
|
282
|
+
classification = _classification(has_azd, prompt_agent, network_isolated, ailz_manifest, accelerator_hint)
|
|
283
|
+
complexity = _complexity(network_isolated, has_azd, bicep_files, accelerator_hint, ailz_manifest)
|
|
284
|
+
deployment_strategy = _deployment_strategy(recommended_deploy_mode, network_isolated, ailz_preflight)
|
|
285
|
+
eval_strategy = _eval_strategy(recommended_eval_runner)
|
|
286
|
+
requires_copilot = (
|
|
287
|
+
recommended_deploy_mode == "placeholder"
|
|
288
|
+
or network_isolated
|
|
289
|
+
or bool(accelerator_hint)
|
|
290
|
+
or len(bicep_files) > 5
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
skills_installed = _skills_installed(root)
|
|
294
|
+
copilot_prompt = _copilot_prompt(classification, recommended_deploy_mode, network_isolated) if requires_copilot else None
|
|
295
|
+
recommended_commands = [
|
|
296
|
+
"agentops workflow analyze --format markdown",
|
|
297
|
+
f"agentops workflow generate --kinds pr,dev,qa,prod --deploy-mode {recommended_deploy_mode} --force",
|
|
298
|
+
]
|
|
299
|
+
if ailz_preflight:
|
|
300
|
+
recommended_commands.insert(1, "pwsh ./scripts/Invoke-PreflightChecks.ps1 -Strict")
|
|
301
|
+
if requires_copilot and not skills_installed:
|
|
302
|
+
recommended_commands.insert(1, "agentops skills install --platform copilot")
|
|
303
|
+
if has_azd:
|
|
304
|
+
recommended_commands.append("azd provision")
|
|
305
|
+
recommended_commands.append("azd deploy")
|
|
306
|
+
|
|
307
|
+
stages = _stages(
|
|
308
|
+
recommended_deploy_mode,
|
|
309
|
+
recommended_eval_runner,
|
|
310
|
+
network_isolated,
|
|
311
|
+
prompt_agent,
|
|
312
|
+
ailz_preflight,
|
|
313
|
+
)
|
|
314
|
+
next_steps = _next_steps(
|
|
315
|
+
recommended_deploy_mode,
|
|
316
|
+
recommended_eval_runner,
|
|
317
|
+
requires_copilot,
|
|
318
|
+
network_isolated,
|
|
319
|
+
skills_installed,
|
|
320
|
+
ailz_preflight,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return WorkflowAnalysis(
|
|
324
|
+
version=1,
|
|
325
|
+
directory=str(root),
|
|
326
|
+
classification=classification,
|
|
327
|
+
recommended_deploy_mode=recommended_deploy_mode,
|
|
328
|
+
recommended_eval_runner=recommended_eval_runner,
|
|
329
|
+
deployment_strategy=deployment_strategy,
|
|
330
|
+
eval_strategy=eval_strategy,
|
|
331
|
+
complexity=complexity,
|
|
332
|
+
requires_copilot_adaptation=requires_copilot,
|
|
333
|
+
copilot_skills_installed=skills_installed,
|
|
334
|
+
copilot_prompt=copilot_prompt,
|
|
335
|
+
official_eval_reasons=official_eval_reasons,
|
|
336
|
+
official_evaluators=official_evaluators,
|
|
337
|
+
signals=signals,
|
|
338
|
+
warnings=warnings,
|
|
339
|
+
recommended_commands=recommended_commands,
|
|
340
|
+
stages=stages,
|
|
341
|
+
next_steps=next_steps,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def recommended_deploy_mode(directory: Path) -> str:
|
|
346
|
+
"""Return the same deploy-mode decision used by workflow generation."""
|
|
347
|
+
return analyze_workflow_project(directory).recommended_deploy_mode
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def recommended_eval_runner(directory: Path) -> str:
|
|
351
|
+
"""Return the same eval-runner decision used by workflow generation."""
|
|
352
|
+
return analyze_workflow_project(directory).recommended_eval_runner
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _display_eval_runner(eval_runner: str) -> str:
|
|
356
|
+
if eval_runner == AGENTOPS_CLOUD_RUNNER:
|
|
357
|
+
return "AgentOps cloud eval in Foundry"
|
|
358
|
+
if eval_runner == OFFICIAL_EVAL_RUNNER:
|
|
359
|
+
return "Microsoft Foundry AI Agent Evaluation"
|
|
360
|
+
if eval_runner == AGENTOPS_LOCAL_RUNNER:
|
|
361
|
+
return "AgentOps local eval"
|
|
362
|
+
return eval_runner
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def has_ailz_preflight(directory: Path) -> bool:
|
|
366
|
+
"""Return True when the official AI Landing Zone preflight script exists."""
|
|
367
|
+
root = directory.resolve()
|
|
368
|
+
return (root / "scripts" / "Invoke-PreflightChecks.ps1").exists()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def render_workflow_analysis(analysis: WorkflowAnalysis, output_format: str = "text") -> str:
|
|
372
|
+
"""Render analysis as text, Markdown, or JSON."""
|
|
373
|
+
if output_format == "json":
|
|
374
|
+
return json.dumps(analysis.to_dict(), indent=2) + "\n"
|
|
375
|
+
if output_format == "markdown":
|
|
376
|
+
return _render_markdown(analysis)
|
|
377
|
+
if output_format == "text":
|
|
378
|
+
return _render_text(analysis)
|
|
379
|
+
raise ValueError("output_format must be text, markdown, or json")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _render_text(analysis: WorkflowAnalysis) -> str:
|
|
383
|
+
lines = [
|
|
384
|
+
"AgentOps workflow analysis",
|
|
385
|
+
f"Workspace: {analysis.directory}",
|
|
386
|
+
f"Project: {analysis.classification}",
|
|
387
|
+
"",
|
|
388
|
+
"Recommendation",
|
|
389
|
+
]
|
|
390
|
+
lines.extend(_render_text_recommendation(analysis))
|
|
391
|
+
lines.append("")
|
|
392
|
+
lines.append("Signals")
|
|
393
|
+
lines.extend(_render_text_signal_rows(_signal_rows(analysis)))
|
|
394
|
+
if analysis.warnings:
|
|
395
|
+
lines.append("")
|
|
396
|
+
lines.append("Warnings")
|
|
397
|
+
for warning in analysis.warnings:
|
|
398
|
+
lines.extend(_wrapped_status_line("warn", "warning", warning))
|
|
399
|
+
if analysis.official_eval_reasons:
|
|
400
|
+
lines.append("")
|
|
401
|
+
lines.append("Foundry eval")
|
|
402
|
+
lines.extend(_render_text_foundry_eval_rows(_foundry_eval_rows(analysis)))
|
|
403
|
+
if analysis.copilot_prompt:
|
|
404
|
+
lines.append("")
|
|
405
|
+
lines.append("Copilot handoff")
|
|
406
|
+
lines.extend(_wrapped_status_line("todo", "copy/paste", analysis.copilot_prompt))
|
|
407
|
+
lines.append("")
|
|
408
|
+
lines.append("Pipeline plan")
|
|
409
|
+
for index, stage in enumerate(analysis.stages, start=1):
|
|
410
|
+
lines.append(f" {index}. {stage.name}")
|
|
411
|
+
lines.extend(_wrap_text(stage.purpose, indent=" "))
|
|
412
|
+
commands = _text_commands(analysis.recommended_commands)
|
|
413
|
+
if commands:
|
|
414
|
+
lines.append("")
|
|
415
|
+
lines.append("Commands")
|
|
416
|
+
lines.extend(f" {command}" for command in commands)
|
|
417
|
+
lines.append("")
|
|
418
|
+
lines.append("Next")
|
|
419
|
+
for index, step in enumerate(analysis.next_steps, start=1):
|
|
420
|
+
lines.extend(_wrapped_numbered_step(index, step))
|
|
421
|
+
return "\n".join(lines) + "\n"
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _render_markdown(analysis: WorkflowAnalysis) -> str:
|
|
425
|
+
lines = [
|
|
426
|
+
"# AgentOps workflow analysis",
|
|
427
|
+
"",
|
|
428
|
+
f"- **Directory:** `{analysis.directory}`",
|
|
429
|
+
f"- **Classification:** {analysis.classification}",
|
|
430
|
+
"",
|
|
431
|
+
"## Workflow decision checklist",
|
|
432
|
+
"",
|
|
433
|
+
]
|
|
434
|
+
lines.extend(_render_markdown_table(("Check", "Status", "Explanation"), _decision_checklist_rows(analysis)))
|
|
435
|
+
lines.extend(
|
|
436
|
+
[
|
|
437
|
+
"",
|
|
438
|
+
"## Detected signals",
|
|
439
|
+
"",
|
|
440
|
+
]
|
|
441
|
+
)
|
|
442
|
+
lines.extend(_render_markdown_table(("Status", "Type", "Finding", "Evidence"), _signal_rows(analysis)))
|
|
443
|
+
if analysis.warnings:
|
|
444
|
+
lines.extend(["", "## Warnings", ""])
|
|
445
|
+
lines.extend(f"- {warning}" for warning in analysis.warnings)
|
|
446
|
+
if analysis.official_eval_reasons:
|
|
447
|
+
lines.extend(["", "## Foundry eval checks", ""])
|
|
448
|
+
lines.extend(_render_markdown_table(("Status", "Check", "Explanation"), _foundry_eval_rows(analysis)))
|
|
449
|
+
if analysis.copilot_prompt:
|
|
450
|
+
lines.extend(["", "## Copilot handoff", ""])
|
|
451
|
+
lines.extend(["Copy/paste this into Copilot:", "", "```text", analysis.copilot_prompt, "```"])
|
|
452
|
+
lines.extend(["", "## Recommended commands", ""])
|
|
453
|
+
lines.extend(f"```bash\n{command}\n```" for command in analysis.recommended_commands)
|
|
454
|
+
lines.extend(["", "## Pipeline stages", ""])
|
|
455
|
+
for stage in analysis.stages:
|
|
456
|
+
lines.append(f"### {stage.name}")
|
|
457
|
+
lines.append("")
|
|
458
|
+
lines.append(f"- **Owner:** {stage.owner}")
|
|
459
|
+
lines.append(f"- **Purpose:** {stage.purpose}")
|
|
460
|
+
if stage.commands:
|
|
461
|
+
lines.append("- **Commands:** " + ", ".join(f"`{c}`" for c in stage.commands))
|
|
462
|
+
for note in stage.notes:
|
|
463
|
+
lines.append(f"- {note}")
|
|
464
|
+
lines.append("")
|
|
465
|
+
lines.extend(["## Next steps", ""])
|
|
466
|
+
lines.extend(f"- {step}" for step in analysis.next_steps)
|
|
467
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _render_text_recommendation(analysis: WorkflowAnalysis) -> List[str]:
|
|
471
|
+
adaptation_value = (
|
|
472
|
+
"needed - review project-specific build/deploy steps"
|
|
473
|
+
if analysis.requires_copilot_adaptation
|
|
474
|
+
else "not needed - generated workflow should work as-is"
|
|
475
|
+
)
|
|
476
|
+
skills_value = (
|
|
477
|
+
"installed - available for workflow adaptation handoff"
|
|
478
|
+
if analysis.copilot_skills_installed
|
|
479
|
+
else (
|
|
480
|
+
"missing - run `agentops skills install --platform copilot` for handoff"
|
|
481
|
+
if analysis.requires_copilot_adaptation
|
|
482
|
+
else "not needed - no Copilot handoff for this project shape"
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
return _render_text_fields(
|
|
486
|
+
[
|
|
487
|
+
("deploy", analysis.recommended_deploy_mode),
|
|
488
|
+
("evaluate", _display_eval_runner(analysis.recommended_eval_runner)),
|
|
489
|
+
("workflow edits", adaptation_value),
|
|
490
|
+
("Copilot skills", skills_value),
|
|
491
|
+
]
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _text_commands(commands: Sequence[str]) -> List[str]:
|
|
496
|
+
return [command for command in commands if command != "agentops workflow analyze --format markdown"]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _render_text_fields(rows: Sequence[tuple[str, str]]) -> List[str]:
|
|
500
|
+
width = max(len(label) for label, _ in rows)
|
|
501
|
+
lines: List[str] = []
|
|
502
|
+
for label, value in rows:
|
|
503
|
+
lines.extend(_wrap_text(value, indent=f" {label.ljust(width)} "))
|
|
504
|
+
return lines
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _render_text_signal_rows(rows: Sequence[Sequence[str]]) -> List[str]:
|
|
508
|
+
lines: List[str] = []
|
|
509
|
+
for status, signal_type, _finding, evidence in rows:
|
|
510
|
+
marker, _ = _split_status_value(str(status))
|
|
511
|
+
detail = _soften_text(str(evidence))
|
|
512
|
+
lines.extend(_wrapped_status_line(_status_word(marker), str(signal_type), detail))
|
|
513
|
+
return lines
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _render_text_foundry_eval_rows(rows: Sequence[Sequence[str]]) -> List[str]:
|
|
517
|
+
lines: List[str] = []
|
|
518
|
+
for status, check, explanation in rows:
|
|
519
|
+
marker, _ = _split_status_value(str(status))
|
|
520
|
+
lines.extend(
|
|
521
|
+
_wrapped_status_line(
|
|
522
|
+
_status_word(marker),
|
|
523
|
+
str(check),
|
|
524
|
+
_friendly_foundry_eval_text(str(check), str(explanation)),
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
return lines
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _split_status_value(status: str) -> tuple[str, str]:
|
|
531
|
+
if status.startswith("[") and "]" in status:
|
|
532
|
+
marker, _, value = status.partition(" ")
|
|
533
|
+
return marker, value.strip()
|
|
534
|
+
return status, ""
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _status_word(marker: str) -> str:
|
|
538
|
+
if marker == "[x]":
|
|
539
|
+
return "ok"
|
|
540
|
+
if marker == "[?]":
|
|
541
|
+
return "hint"
|
|
542
|
+
if marker == "[ ]":
|
|
543
|
+
return "todo"
|
|
544
|
+
return marker.strip("[]").lower() or "info"
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _wrapped_status_line(status: str, label: str, text: str) -> List[str]:
|
|
548
|
+
prefix = f" {status.ljust(4)} {label.ljust(13)} "
|
|
549
|
+
wrapped = textwrap.wrap(
|
|
550
|
+
text,
|
|
551
|
+
width=_TEXT_WRAP_WIDTH,
|
|
552
|
+
initial_indent=prefix,
|
|
553
|
+
subsequent_indent=" " * len(prefix),
|
|
554
|
+
break_long_words=False,
|
|
555
|
+
break_on_hyphens=False,
|
|
556
|
+
)
|
|
557
|
+
return wrapped or [prefix.rstrip()]
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _wrapped_numbered_step(index: int, text: str) -> List[str]:
|
|
561
|
+
prefix = f" {index}. "
|
|
562
|
+
wrapped = textwrap.wrap(
|
|
563
|
+
text,
|
|
564
|
+
width=_TEXT_WRAP_WIDTH,
|
|
565
|
+
initial_indent=prefix,
|
|
566
|
+
subsequent_indent=" " * len(prefix),
|
|
567
|
+
break_long_words=False,
|
|
568
|
+
break_on_hyphens=False,
|
|
569
|
+
)
|
|
570
|
+
return wrapped or [prefix.rstrip()]
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _friendly_foundry_eval_text(check: str, text: str) -> str:
|
|
574
|
+
if check == "Agent target":
|
|
575
|
+
return "Foundry prompt agent (`name:version`)."
|
|
576
|
+
if check == "Evaluators":
|
|
577
|
+
return _friendly_evaluator_list(text.split(", "))
|
|
578
|
+
return _soften_text(text)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _friendly_evaluator_list(evaluators: Iterable[str]) -> str:
|
|
582
|
+
return ", ".join(
|
|
583
|
+
evaluator.removeprefix("builtin.").replace("_", " ")
|
|
584
|
+
for evaluator in evaluators
|
|
585
|
+
if evaluator
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _soften_text(text: str) -> str:
|
|
590
|
+
return text.replace("foundry_prompt", "Foundry prompt agent")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _wrap_text(text: str, *, indent: str) -> List[str]:
|
|
594
|
+
return textwrap.wrap(
|
|
595
|
+
text,
|
|
596
|
+
width=_TEXT_WRAP_WIDTH,
|
|
597
|
+
initial_indent=indent,
|
|
598
|
+
subsequent_indent=indent,
|
|
599
|
+
break_long_words=False,
|
|
600
|
+
break_on_hyphens=False,
|
|
601
|
+
) or [indent.rstrip()]
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _render_markdown_table(headers: Sequence[str], rows: Sequence[Sequence[str]]) -> List[str]:
|
|
605
|
+
normalized = [[_escape_markdown_cell(str(cell)) for cell in row] for row in rows]
|
|
606
|
+
if not normalized:
|
|
607
|
+
normalized = [["-" for _ in headers]]
|
|
608
|
+
header_line = "| " + " | ".join(_escape_markdown_cell(str(header)) for header in headers) + " |"
|
|
609
|
+
separator = "| " + " | ".join("---" for _ in headers) + " |"
|
|
610
|
+
body = ["| " + " | ".join(row) + " |" for row in normalized]
|
|
611
|
+
return [header_line, separator, *body]
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _escape_markdown_cell(value: str) -> str:
|
|
615
|
+
return value.replace("|", "\\|")
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _decision_checklist_rows(analysis: WorkflowAnalysis) -> List[tuple[str, str, str]]:
|
|
619
|
+
if analysis.requires_copilot_adaptation:
|
|
620
|
+
adaptation_status = "[ ] needs edits"
|
|
621
|
+
adaptation_detail = (
|
|
622
|
+
"Detected project-specific topology or deploy signals; review generated "
|
|
623
|
+
"workflow before making it blocking."
|
|
624
|
+
)
|
|
625
|
+
else:
|
|
626
|
+
adaptation_status = "[x] not needed"
|
|
627
|
+
adaptation_detail = "Generated workflow should be directly usable for this project shape."
|
|
628
|
+
|
|
629
|
+
if analysis.requires_copilot_adaptation:
|
|
630
|
+
skills_status = "[x] installed" if analysis.copilot_skills_installed else "[ ] missing"
|
|
631
|
+
skills_detail = "Needed only for the Copilot workflow-adaptation handoff."
|
|
632
|
+
else:
|
|
633
|
+
skills_status = "[x] not required"
|
|
634
|
+
skills_detail = "No workflow handoff is required for the current recommendation."
|
|
635
|
+
|
|
636
|
+
return [
|
|
637
|
+
(
|
|
638
|
+
"Deploy mode",
|
|
639
|
+
f"[x] {analysis.recommended_deploy_mode}",
|
|
640
|
+
_deploy_mode_check_detail(analysis.recommended_deploy_mode),
|
|
641
|
+
),
|
|
642
|
+
(
|
|
643
|
+
"Eval runner",
|
|
644
|
+
f"[x] {_display_eval_runner(analysis.recommended_eval_runner)}",
|
|
645
|
+
_eval_runner_check_detail(analysis.recommended_eval_runner),
|
|
646
|
+
),
|
|
647
|
+
("Complexity", "[x] " + analysis.complexity, "Used to decide whether CI needs extra review."),
|
|
648
|
+
("Workflow adaptation", adaptation_status, adaptation_detail),
|
|
649
|
+
("Copilot skills", skills_status, skills_detail),
|
|
650
|
+
]
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _deploy_mode_check_detail(mode: str) -> str:
|
|
654
|
+
if mode == "azd":
|
|
655
|
+
return "Use azd for provision/deploy; AgentOps supplies gates and evidence."
|
|
656
|
+
if mode == "prompt-agent":
|
|
657
|
+
return "Stage and evaluate a Foundry prompt candidate, then record deployment."
|
|
658
|
+
return "Generate CI placeholders; add the project-specific build/deploy steps."
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _eval_runner_check_detail(eval_runner: str) -> str:
|
|
662
|
+
if eval_runner == AGENTOPS_CLOUD_RUNNER:
|
|
663
|
+
return "Foundry executes the eval; AgentOps downloads results, applies thresholds, and writes evidence."
|
|
664
|
+
if eval_runner == OFFICIAL_EVAL_RUNNER:
|
|
665
|
+
return "Prompt agent plus dataset fit Foundry eval; AgentOps keeps evidence."
|
|
666
|
+
return "AgentOps runs local eval and writes normalized results/report artifacts."
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _signal_rows(analysis: WorkflowAnalysis) -> List[tuple[str, str, str, str]]:
|
|
670
|
+
if not analysis.signals:
|
|
671
|
+
return [
|
|
672
|
+
(
|
|
673
|
+
"[ ]",
|
|
674
|
+
"Signals",
|
|
675
|
+
"No strong project signals",
|
|
676
|
+
"No accelerator, azd, AgentOps, or CI files were detected.",
|
|
677
|
+
)
|
|
678
|
+
]
|
|
679
|
+
return [
|
|
680
|
+
(
|
|
681
|
+
"[x]" if signal.confidence == "high" else "[?]",
|
|
682
|
+
_signal_type(signal.key),
|
|
683
|
+
signal.label,
|
|
684
|
+
signal.detail + (f" ({signal.path})" if signal.path else ""),
|
|
685
|
+
)
|
|
686
|
+
for signal in analysis.signals
|
|
687
|
+
]
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _foundry_eval_rows(analysis: WorkflowAnalysis) -> List[tuple[str, str, str]]:
|
|
691
|
+
selected = analysis.recommended_eval_runner in {AGENTOPS_CLOUD_RUNNER, OFFICIAL_EVAL_RUNNER}
|
|
692
|
+
if selected:
|
|
693
|
+
rows = [
|
|
694
|
+
(
|
|
695
|
+
"[x]",
|
|
696
|
+
"Agent target",
|
|
697
|
+
analysis.official_eval_reasons[0]
|
|
698
|
+
if analysis.official_eval_reasons
|
|
699
|
+
else "Foundry prompt agent.",
|
|
700
|
+
),
|
|
701
|
+
(
|
|
702
|
+
"[x]",
|
|
703
|
+
"Dataset",
|
|
704
|
+
analysis.official_eval_reasons[1]
|
|
705
|
+
if len(analysis.official_eval_reasons) > 1
|
|
706
|
+
else "Compatible with Foundry cloud eval.",
|
|
707
|
+
),
|
|
708
|
+
]
|
|
709
|
+
if analysis.official_evaluators:
|
|
710
|
+
rows.append(("[x]", "Evaluators", ", ".join(analysis.official_evaluators)))
|
|
711
|
+
return rows
|
|
712
|
+
|
|
713
|
+
return [
|
|
714
|
+
("[ ]", "Microsoft Foundry eval", reason)
|
|
715
|
+
for reason in analysis.official_eval_reasons
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _signal_type(key: str) -> str:
|
|
720
|
+
return {
|
|
721
|
+
"agentops_config": "Config",
|
|
722
|
+
"official_ai_agent_evaluation": "Eval runner",
|
|
723
|
+
"agentops_cloud_evaluation": "Eval runner",
|
|
724
|
+
"azd_project": "Deploy mode",
|
|
725
|
+
"prompt_file": "Prompt source",
|
|
726
|
+
"bicep_infra": "Infrastructure",
|
|
727
|
+
"ailz_manifest": "Landing zone",
|
|
728
|
+
"ailz_preflight": "Preflight",
|
|
729
|
+
"network_isolation": "Runner topology",
|
|
730
|
+
"network_isolation_hint": "Runner topology",
|
|
731
|
+
"container_app": "Application host",
|
|
732
|
+
"accelerator_hint": "Accelerator",
|
|
733
|
+
"existing_ci": "Existing CI",
|
|
734
|
+
}.get(key, "Signal")
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _agentops_signal(root: Path) -> Dict[str, Any]:
|
|
738
|
+
path = root / "agentops.yaml"
|
|
739
|
+
if not path.exists():
|
|
740
|
+
return {}
|
|
741
|
+
try:
|
|
742
|
+
data = load_yaml(path)
|
|
743
|
+
target = classify_agent(str(data.get("agent", "") or ""), data.get("protocol"))
|
|
744
|
+
except Exception as exc:
|
|
745
|
+
return {
|
|
746
|
+
"signal": WorkflowSignal(
|
|
747
|
+
"agentops_config",
|
|
748
|
+
"AgentOps config",
|
|
749
|
+
f"agentops.yaml exists but could not be classified: {exc}",
|
|
750
|
+
"agentops.yaml",
|
|
751
|
+
confidence="medium",
|
|
752
|
+
)
|
|
753
|
+
}
|
|
754
|
+
prompt_file = data.get("prompt_file") if isinstance(data, dict) else None
|
|
755
|
+
return {
|
|
756
|
+
"prompt_agent": target.kind == "foundry_prompt",
|
|
757
|
+
"prompt_file": prompt_file,
|
|
758
|
+
"signal": WorkflowSignal(
|
|
759
|
+
"agentops_config",
|
|
760
|
+
"AgentOps config",
|
|
761
|
+
f"agentops.yaml targets {target.kind}.",
|
|
762
|
+
"agentops.yaml",
|
|
763
|
+
),
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _find_files(root: Path, pattern: str) -> List[Path]:
|
|
768
|
+
found: List[Path] = []
|
|
769
|
+
for path in root.rglob(pattern):
|
|
770
|
+
if _ignored(path, root):
|
|
771
|
+
continue
|
|
772
|
+
found.append(path)
|
|
773
|
+
if len(found) >= _SCAN_LIMIT:
|
|
774
|
+
break
|
|
775
|
+
return found
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _infra_scan_files(root: Path, bicep_files: Iterable[Path]) -> List[Path]:
|
|
779
|
+
candidates = [
|
|
780
|
+
root / "azure.yaml",
|
|
781
|
+
root / "main.parameters.json",
|
|
782
|
+
root / "infra" / "main.parameters.json",
|
|
783
|
+
root / "manifest.json",
|
|
784
|
+
]
|
|
785
|
+
candidates.extend(list(bicep_files)[:_SCAN_LIMIT])
|
|
786
|
+
return [path for path in candidates if path.exists() and not _ignored(path, root)]
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _ignored(path: Path, root: Path) -> bool:
|
|
790
|
+
try:
|
|
791
|
+
rel = path.relative_to(root)
|
|
792
|
+
except ValueError:
|
|
793
|
+
return True
|
|
794
|
+
return any(part in _IGNORE_PARTS for part in rel.parts)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def _read_text(path: Path) -> str:
|
|
798
|
+
try:
|
|
799
|
+
if not path.exists() or path.stat().st_size > _TEXT_LIMIT:
|
|
800
|
+
return ""
|
|
801
|
+
return path.read_text(encoding="utf-8", errors="ignore")
|
|
802
|
+
except OSError:
|
|
803
|
+
return ""
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _read_json(path: Path) -> Any:
|
|
807
|
+
text = _read_text(path)
|
|
808
|
+
if not text:
|
|
809
|
+
return None
|
|
810
|
+
try:
|
|
811
|
+
return json.loads(text)
|
|
812
|
+
except json.JSONDecodeError:
|
|
813
|
+
return None
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _has_network_isolation(text: str) -> bool:
|
|
817
|
+
lowered = text.lower()
|
|
818
|
+
structural_terms = (
|
|
819
|
+
"network_isolation",
|
|
820
|
+
"networkisolation",
|
|
821
|
+
"privateendpoint",
|
|
822
|
+
"private endpoint",
|
|
823
|
+
"microsoft.network/privatednszones",
|
|
824
|
+
"azurefirewall",
|
|
825
|
+
"azure firewall",
|
|
826
|
+
"bastion",
|
|
827
|
+
"jumpbox",
|
|
828
|
+
"acr_task_agent_pool",
|
|
829
|
+
"acr task",
|
|
830
|
+
"egressnexthopip",
|
|
831
|
+
)
|
|
832
|
+
return any(term in lowered for term in structural_terms)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _looks_like_container_app(root: Path, infra_text: str, readme_lower: str) -> bool:
|
|
836
|
+
docker = bool(_find_files(root, "Dockerfile"))
|
|
837
|
+
infra_lower = infra_text.lower()
|
|
838
|
+
return (
|
|
839
|
+
"microsoft.app/containerapps" in infra_lower
|
|
840
|
+
or "container app" in readme_lower
|
|
841
|
+
or "containerapp" in infra_lower
|
|
842
|
+
or docker
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _accelerator_hint(readme_lower: str) -> Optional[WorkflowSignal]:
|
|
847
|
+
if "gpt-rag" in readme_lower or "retrieval-augmented generation" in readme_lower:
|
|
848
|
+
return WorkflowSignal(
|
|
849
|
+
"accelerator_hint",
|
|
850
|
+
"Azure AI accelerator hint",
|
|
851
|
+
"README looks like a RAG accelerator; expect app-specific data/index seeding and eval datasets.",
|
|
852
|
+
"README.md",
|
|
853
|
+
confidence="medium",
|
|
854
|
+
)
|
|
855
|
+
if "live voice" in readme_lower or "voice live" in readme_lower:
|
|
856
|
+
return WorkflowSignal(
|
|
857
|
+
"accelerator_hint",
|
|
858
|
+
"Azure AI accelerator hint",
|
|
859
|
+
"README looks like a voice accelerator; expect real-time app deploy plus scenario/rubric evaluation assets.",
|
|
860
|
+
"README.md",
|
|
861
|
+
confidence="medium",
|
|
862
|
+
)
|
|
863
|
+
if "ai landing zone" in readme_lower or "landing zone" in readme_lower:
|
|
864
|
+
return WorkflowSignal(
|
|
865
|
+
"accelerator_hint",
|
|
866
|
+
"AI Landing Zone hint",
|
|
867
|
+
"README mentions AI Landing Zone; verify topology and runner access before CI/CD rollout.",
|
|
868
|
+
"README.md",
|
|
869
|
+
confidence="medium",
|
|
870
|
+
)
|
|
871
|
+
return None
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _existing_ci_signal(root: Path) -> Optional[WorkflowSignal]:
|
|
875
|
+
github = root / ".github" / "workflows"
|
|
876
|
+
ado = root / ".azuredevops" / "pipelines"
|
|
877
|
+
if github.is_dir():
|
|
878
|
+
return WorkflowSignal(
|
|
879
|
+
"existing_ci",
|
|
880
|
+
"Existing GitHub Actions workflows",
|
|
881
|
+
"Existing workflows found; prefer updating generated AgentOps files rather than creating parallel pipeline names.",
|
|
882
|
+
".github/workflows",
|
|
883
|
+
)
|
|
884
|
+
if ado.is_dir():
|
|
885
|
+
return WorkflowSignal(
|
|
886
|
+
"existing_ci",
|
|
887
|
+
"Existing Azure DevOps pipelines",
|
|
888
|
+
"Existing pipelines found; prefer updating generated AgentOps files rather than creating parallel pipeline names.",
|
|
889
|
+
".azuredevops/pipelines",
|
|
890
|
+
)
|
|
891
|
+
return None
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _recommended_deploy_mode(has_azd: bool, prompt_agent: bool) -> str:
|
|
895
|
+
if has_azd:
|
|
896
|
+
return "azd"
|
|
897
|
+
if prompt_agent:
|
|
898
|
+
return "prompt-agent"
|
|
899
|
+
return "placeholder"
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _classification(
|
|
903
|
+
has_azd: bool,
|
|
904
|
+
prompt_agent: bool,
|
|
905
|
+
network_isolated: bool,
|
|
906
|
+
ailz_manifest: bool,
|
|
907
|
+
accelerator_hint: Optional[WorkflowSignal],
|
|
908
|
+
) -> str:
|
|
909
|
+
if network_isolated or ailz_manifest:
|
|
910
|
+
return "Azure AI accelerator / landing-zone application"
|
|
911
|
+
if has_azd and accelerator_hint:
|
|
912
|
+
return "azd-managed Azure AI accelerator"
|
|
913
|
+
if has_azd:
|
|
914
|
+
return "azd-managed Azure AI application"
|
|
915
|
+
if prompt_agent:
|
|
916
|
+
return "Foundry prompt-agent project"
|
|
917
|
+
return "custom AI application"
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _complexity(
|
|
921
|
+
network_isolated: bool,
|
|
922
|
+
has_azd: bool,
|
|
923
|
+
bicep_files: List[Path],
|
|
924
|
+
accelerator_hint: Optional[WorkflowSignal],
|
|
925
|
+
ailz_manifest: bool,
|
|
926
|
+
) -> str:
|
|
927
|
+
if network_isolated:
|
|
928
|
+
return "high - network-isolated deployment topology"
|
|
929
|
+
if ailz_manifest:
|
|
930
|
+
return "medium - AI Landing Zone deployment path"
|
|
931
|
+
if len(bicep_files) > 5 or (has_azd and accelerator_hint):
|
|
932
|
+
return "medium - accelerator or multi-resource Azure app"
|
|
933
|
+
if has_azd:
|
|
934
|
+
return "medium - azd-managed app"
|
|
935
|
+
return "low - simple AgentOps workflow scaffold"
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _deployment_strategy(mode: str, network_isolated: bool, ailz_preflight: bool) -> str:
|
|
939
|
+
if mode == "azd":
|
|
940
|
+
suffix = (
|
|
941
|
+
" Use private runner/jumpbox/ACR Tasks for private data-plane steps."
|
|
942
|
+
if network_isolated
|
|
943
|
+
else ""
|
|
944
|
+
)
|
|
945
|
+
preflight = (
|
|
946
|
+
" Run the AI Landing Zone preflight before provision."
|
|
947
|
+
if ailz_preflight
|
|
948
|
+
else ""
|
|
949
|
+
)
|
|
950
|
+
return "AgentOps gates; azd owns provision/deploy and hooks." + preflight + suffix
|
|
951
|
+
if mode == "prompt-agent":
|
|
952
|
+
return "AgentOps stages a Foundry prompt candidate, evaluates it, then records the deployed version."
|
|
953
|
+
return "AgentOps writes gates/placeholders; Copilot must adapt project-specific build/deploy steps."
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _eval_strategy(eval_runner: str) -> str:
|
|
957
|
+
if eval_runner == AGENTOPS_CLOUD_RUNNER:
|
|
958
|
+
return (
|
|
959
|
+
"Use AgentOps cloud eval so Foundry executes the prompt-agent eval "
|
|
960
|
+
"and AgentOps applies thresholds to normalized results."
|
|
961
|
+
)
|
|
962
|
+
if eval_runner == OFFICIAL_EVAL_RUNNER:
|
|
963
|
+
return (
|
|
964
|
+
"Use Microsoft Foundry AI Agent Evaluation for prompt-agent execution, "
|
|
965
|
+
"then keep AgentOps Doctor/evidence as advisory provenance."
|
|
966
|
+
)
|
|
967
|
+
return "Use AgentOps local eval as the CI gate and normalized results artifact."
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _eval_stage(eval_runner: str) -> WorkflowStage:
|
|
971
|
+
if eval_runner == AGENTOPS_CLOUD_RUNNER:
|
|
972
|
+
return WorkflowStage(
|
|
973
|
+
"PR evaluation gate",
|
|
974
|
+
"Foundry + AgentOps",
|
|
975
|
+
"Run Foundry cloud evaluation and let AgentOps enforce thresholds before merge.",
|
|
976
|
+
["agentops eval run --config <cloud-eval-config> --output .agentops/results/latest"],
|
|
977
|
+
)
|
|
978
|
+
if eval_runner == OFFICIAL_EVAL_RUNNER:
|
|
979
|
+
return WorkflowStage(
|
|
980
|
+
"PR evaluation gate",
|
|
981
|
+
"Microsoft Foundry + AgentOps",
|
|
982
|
+
"Run Microsoft Foundry AI Agent Evaluation as advisory provenance.",
|
|
983
|
+
[
|
|
984
|
+
"python -m agentops.pipeline.official_eval prepare",
|
|
985
|
+
official_eval_action_ref(),
|
|
986
|
+
],
|
|
987
|
+
)
|
|
988
|
+
return WorkflowStage(
|
|
989
|
+
"PR evaluation gate",
|
|
990
|
+
"AgentOps",
|
|
991
|
+
"Run repeatable evals before merge and publish report artifacts.",
|
|
992
|
+
["agentops eval run"],
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _stages(
|
|
997
|
+
mode: str,
|
|
998
|
+
eval_runner: str,
|
|
999
|
+
network_isolated: bool,
|
|
1000
|
+
prompt_agent: bool,
|
|
1001
|
+
ailz_preflight: bool,
|
|
1002
|
+
) -> List[WorkflowStage]:
|
|
1003
|
+
stages = [
|
|
1004
|
+
_eval_stage(eval_runner),
|
|
1005
|
+
WorkflowStage(
|
|
1006
|
+
"Operational readiness",
|
|
1007
|
+
"AgentOps Doctor",
|
|
1008
|
+
"Run repo, CI/CD, telemetry, and Foundry readiness checks.",
|
|
1009
|
+
["agentops doctor"],
|
|
1010
|
+
),
|
|
1011
|
+
]
|
|
1012
|
+
if mode == "azd" and ailz_preflight:
|
|
1013
|
+
stages.append(
|
|
1014
|
+
WorkflowStage(
|
|
1015
|
+
"AI Landing Zone preflight",
|
|
1016
|
+
"AI Landing Zone + azd",
|
|
1017
|
+
"Validate topology, parameters, CIDRs, BYO resources, and observability wiring before ARM deployment.",
|
|
1018
|
+
["pwsh ./scripts/Invoke-PreflightChecks.ps1 -Strict"],
|
|
1019
|
+
)
|
|
1020
|
+
)
|
|
1021
|
+
if mode == "azd":
|
|
1022
|
+
notes = ["Use azd hooks for pre/post provision or deploy customization."]
|
|
1023
|
+
if prompt_agent:
|
|
1024
|
+
notes.append("Keep prompt-agent evaluation in AgentOps even though azd owns app deployment.")
|
|
1025
|
+
if network_isolated:
|
|
1026
|
+
notes.append("Run private data-plane work from a runner with VNet/private endpoint access.")
|
|
1027
|
+
stages.append(
|
|
1028
|
+
WorkflowStage(
|
|
1029
|
+
"DEV/QA/PROD deploy",
|
|
1030
|
+
"azd",
|
|
1031
|
+
"Provision and deploy accelerator infrastructure/application.",
|
|
1032
|
+
["azd provision", "azd deploy"],
|
|
1033
|
+
notes,
|
|
1034
|
+
)
|
|
1035
|
+
)
|
|
1036
|
+
elif mode == "prompt-agent":
|
|
1037
|
+
stages.append(
|
|
1038
|
+
WorkflowStage(
|
|
1039
|
+
"Foundry prompt candidate deploy",
|
|
1040
|
+
"Foundry + AgentOps",
|
|
1041
|
+
"Create/reuse candidate prompt-agent version, evaluate it, then record deployment.",
|
|
1042
|
+
[
|
|
1043
|
+
"python -m agentops.pipeline.prompt_deploy stage",
|
|
1044
|
+
(
|
|
1045
|
+
"python -m agentops.pipeline.official_eval prepare"
|
|
1046
|
+
if eval_runner == OFFICIAL_EVAL_RUNNER
|
|
1047
|
+
else "agentops eval run --config <cloud-eval-config>"
|
|
1048
|
+
if eval_runner == AGENTOPS_CLOUD_RUNNER
|
|
1049
|
+
else "agentops eval run"
|
|
1050
|
+
),
|
|
1051
|
+
],
|
|
1052
|
+
)
|
|
1053
|
+
)
|
|
1054
|
+
else:
|
|
1055
|
+
stages.append(
|
|
1056
|
+
WorkflowStage(
|
|
1057
|
+
"Project-specific deploy",
|
|
1058
|
+
"Copilot + project tooling",
|
|
1059
|
+
"Replace placeholders with the repo's build/deploy primitives.",
|
|
1060
|
+
notes=["Prefer azd if the project can be converted to azure.yaml."],
|
|
1061
|
+
)
|
|
1062
|
+
)
|
|
1063
|
+
return stages
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _next_steps(
|
|
1067
|
+
mode: str,
|
|
1068
|
+
eval_runner: str,
|
|
1069
|
+
requires_copilot: bool,
|
|
1070
|
+
network_isolated: bool,
|
|
1071
|
+
skills_installed: bool,
|
|
1072
|
+
ailz_preflight: bool,
|
|
1073
|
+
) -> List[str]:
|
|
1074
|
+
steps = [
|
|
1075
|
+
"Run `agentops eval run` locally and commit agentops.yaml plus datasets.",
|
|
1076
|
+
f"Generate workflows with `agentops workflow generate --deploy-mode {mode}`.",
|
|
1077
|
+
]
|
|
1078
|
+
if eval_runner == AGENTOPS_CLOUD_RUNNER:
|
|
1079
|
+
steps.insert(
|
|
1080
|
+
1,
|
|
1081
|
+
"Set AZURE_OPENAI_DEPLOYMENT so Foundry cloud eval can judge responses, then review AgentOps results.json/report.md after the run.",
|
|
1082
|
+
)
|
|
1083
|
+
elif eval_runner == OFFICIAL_EVAL_RUNNER:
|
|
1084
|
+
steps.insert(
|
|
1085
|
+
1,
|
|
1086
|
+
"Set AZURE_OPENAI_DEPLOYMENT so Microsoft Foundry AI Agent Evaluation can judge responses.",
|
|
1087
|
+
)
|
|
1088
|
+
if ailz_preflight:
|
|
1089
|
+
steps.insert(0, "Run `pwsh ./scripts/Invoke-PreflightChecks.ps1 -Strict` before provisioning the AI Landing Zone.")
|
|
1090
|
+
if requires_copilot:
|
|
1091
|
+
if not skills_installed:
|
|
1092
|
+
steps.append("Install the AgentOps Copilot skills first: `agentops skills install --platform copilot`.")
|
|
1093
|
+
steps.append(
|
|
1094
|
+
"Copy/paste the Copilot handoff prompt shown below to inspect project-specific build/deploy hooks and adapt the workflow."
|
|
1095
|
+
)
|
|
1096
|
+
if network_isolated:
|
|
1097
|
+
steps.append(
|
|
1098
|
+
"Decide where private-network deploy steps run: self-hosted runner, jumpbox, or ACR Tasks agent pool."
|
|
1099
|
+
)
|
|
1100
|
+
steps.append("Configure environment approvals and Azure federated identity/service connection before making gates required.")
|
|
1101
|
+
return steps
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _skills_installed(root: Path) -> bool:
|
|
1105
|
+
return (
|
|
1106
|
+
(root / ".github" / "skills" / "agentops-workflow" / "SKILL.md").exists()
|
|
1107
|
+
or (root / ".claude" / "commands" / "agentops-workflow.md").exists()
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _copilot_prompt(classification: str, mode: str, network_isolated: bool) -> str:
|
|
1112
|
+
network_note = (
|
|
1113
|
+
" This repo appears network-isolated; plan self-hosted runner, jumpbox handoff, or ACR Tasks for private steps."
|
|
1114
|
+
if network_isolated
|
|
1115
|
+
else ""
|
|
1116
|
+
)
|
|
1117
|
+
return (
|
|
1118
|
+
"/agentops-workflow Use the AgentOps workflow analysis above to adapt this "
|
|
1119
|
+
f"{classification} pipeline. Keep AgentOps as eval/Doctor/Cockpit gate, use deploy mode {mode}, "
|
|
1120
|
+
"and preserve existing azd/Bicep/project deploy hooks instead of inventing a parallel deployment path."
|
|
1121
|
+
+ network_note
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def _rel(root: Path, path: Path) -> str:
|
|
1126
|
+
try:
|
|
1127
|
+
return str(path.relative_to(root))
|
|
1128
|
+
except ValueError:
|
|
1129
|
+
return str(path)
|