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,86 @@
1
+ """Workspace initialization service for ``agentops init``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from importlib.resources import files
7
+ from pathlib import Path
8
+ from typing import Dict, List
9
+
10
+
11
+ @dataclass
12
+ class InitResult:
13
+ workspace_dir: Path
14
+ created_dirs: List[Path] = field(default_factory=list)
15
+ created_files: List[Path] = field(default_factory=list)
16
+ overwritten_files: List[Path] = field(default_factory=list)
17
+ skipped_files: List[Path] = field(default_factory=list)
18
+
19
+
20
+ _TEMPLATE_PACKAGE = "agentops.templates"
21
+
22
+ # 1.0 flat workspace: a single agentops.yaml at the project root and a tiny
23
+ # seed dataset under .agentops/data/. Everything else (bundles, datasets YAML,
24
+ # run-*.yaml variants) was removed in the revamp.
25
+ _FLAT_FILES: Dict[str, str] = {
26
+ "agentops.yaml": "agentops.yaml",
27
+ ".agentops/.gitignore": ".gitignore",
28
+ ".agentops/data/smoke.jsonl": "smoke.jsonl",
29
+ ".agentops/traces/sample-traces.jsonl": "sample-traces.jsonl",
30
+ ".agentops/waf-checklist.csv": "waf-checklist.csv",
31
+ ".agentops/waf-checklist.README.md": "waf-checklist.README.md",
32
+ }
33
+
34
+
35
+ # Project-root .gitignore. Only written when one doesn't already exist so we
36
+ # never clobber a user's curated ignore file.
37
+ _PROJECT_GITIGNORE_TEMPLATE = "project.gitignore"
38
+ _PROJECT_GITIGNORE_TARGET = ".gitignore"
39
+
40
+
41
+ def initialize_flat_workspace(directory: Path, force: bool = False) -> InitResult:
42
+ """Bootstrap the AgentOps 1.0 workspace.
43
+
44
+ Creates ``agentops.yaml`` at the project root, an AgentOps-local
45
+ ``.agentops/.gitignore``, and a tiny seed dataset at
46
+ ``.agentops/data/smoke.jsonl``. Also drops a starter ``.gitignore`` at the
47
+ project root if one does not exist yet (covers ``.venv/``, Python build
48
+ artifacts, and the ``.agentops/results/`` runtime output).
49
+ """
50
+ project_root = directory.resolve()
51
+ result = InitResult(workspace_dir=project_root / ".agentops")
52
+
53
+ templates_root = files(_TEMPLATE_PACKAGE)
54
+ for relative_path, template_name in _FLAT_FILES.items():
55
+ target = project_root / relative_path
56
+ existed_before = target.exists()
57
+ if existed_before and not force:
58
+ result.skipped_files.append(target)
59
+ continue
60
+
61
+ target.parent.mkdir(parents=True, exist_ok=True)
62
+ if not target.parent.exists():
63
+ result.created_dirs.append(target.parent)
64
+
65
+ content = templates_root.joinpath(template_name).read_text(encoding="utf-8")
66
+ target.write_text(content, encoding="utf-8")
67
+
68
+ if existed_before:
69
+ result.overwritten_files.append(target)
70
+ else:
71
+ result.created_files.append(target)
72
+
73
+ # Write a starter project-root .gitignore. We never overwrite an existing
74
+ # one (even with --force) - users often have curated ignores we don't want
75
+ # to clobber.
76
+ gitignore_target = project_root / _PROJECT_GITIGNORE_TARGET
77
+ if not gitignore_target.exists():
78
+ content = templates_root.joinpath(_PROJECT_GITIGNORE_TEMPLATE).read_text(
79
+ encoding="utf-8"
80
+ )
81
+ gitignore_target.write_text(content, encoding="utf-8")
82
+ result.created_files.append(gitignore_target)
83
+ else:
84
+ result.skipped_files.append(gitignore_target)
85
+
86
+ return result
@@ -0,0 +1,470 @@
1
+ """Pre-flight checks shared by `agentops doctor`, `agentops cockpit`,
2
+ and `agentops agent serve`.
3
+
4
+ Goal: surface every reason the agent might silently misbehave *before*
5
+ the user waits ~20 seconds for the first cockpit render or the first
6
+ doctor run, and tell them exactly what to fix.
7
+
8
+ The checks are intentionally fast and best-effort:
9
+
10
+ * No retries — a single shot with the standard 30-second
11
+ ``DefaultAzureCredential(process_timeout=30)`` timeout.
12
+ * Failures never raise into the CLI; they return a status row.
13
+ * Optional checks (Foundry / App Insights / ARM) are skipped cleanly
14
+ when the relevant env var or config is absent.
15
+
16
+ Callers receive a list of :class:`PreflightCheck` rows and decide what
17
+ to do with them. The default policy is *advisory* — print warnings and
18
+ continue. Strict CI pipelines can pass ``--strict-preflight`` to make
19
+ the CLI exit non-zero on any failure.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import os
26
+ import textwrap
27
+ import time
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import List, Literal, Optional
31
+
32
+ log = logging.getLogger(__name__)
33
+
34
+
35
+ _Status = Literal["ok", "warn", "skip", "fail"]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class PreflightCheck:
40
+ """One row in the pre-flight summary.
41
+
42
+ ``name`` is the stable internal id; ``display_name`` is what
43
+ appears in the terminal (e.g. ``Azure authentication``).
44
+ ``remediation`` is the one-line "do this to fix it" hint shown
45
+ indented under the message for ``warn`` / ``fail`` rows.
46
+ """
47
+
48
+ name: str
49
+ status: _Status
50
+ message: str
51
+ display_name: str = ""
52
+ remediation: str = ""
53
+ duration_seconds: float = 0.0
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class PreflightReport:
58
+ """Aggregate result of a pre-flight run."""
59
+
60
+ checks: List[PreflightCheck]
61
+
62
+ @property
63
+ def has_failures(self) -> bool:
64
+ return any(c.status == "fail" for c in self.checks)
65
+
66
+ @property
67
+ def has_warnings(self) -> bool:
68
+ return any(c.status == "warn" for c in self.checks)
69
+
70
+
71
+ _AZURE_RM_SCOPE = "https://management.azure.com/.default"
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Individual checks
76
+ # ---------------------------------------------------------------------------
77
+
78
+
79
+ def _check_workspace(workspace: Path) -> PreflightCheck:
80
+ """The workspace must exist and contain `.agentops/`."""
81
+ started = time.time()
82
+ if not workspace.exists() or not workspace.is_dir():
83
+ return PreflightCheck(
84
+ name="workspace",
85
+ display_name="Workspace",
86
+ status="fail",
87
+ message=f"Workspace path does not exist: {workspace}",
88
+ remediation="Create the directory or pass --workspace <path>.",
89
+ duration_seconds=time.time() - started,
90
+ )
91
+ agentops_dir = workspace / ".agentops"
92
+ if not agentops_dir.is_dir():
93
+ return PreflightCheck(
94
+ name="workspace",
95
+ display_name="Workspace",
96
+ status="warn",
97
+ message=(
98
+ f"No `.agentops/` directory in {workspace}. Checks that "
99
+ "depend on workspace files will be silent."
100
+ ),
101
+ remediation="Run `agentops init` to scaffold the workspace.",
102
+ duration_seconds=time.time() - started,
103
+ )
104
+ return PreflightCheck(
105
+ name="workspace",
106
+ display_name="Workspace",
107
+ status="ok",
108
+ message=str(workspace),
109
+ duration_seconds=time.time() - started,
110
+ )
111
+
112
+
113
+ def _check_azure_cli() -> PreflightCheck:
114
+ """`DefaultAzureCredential` can acquire an ARM token.
115
+
116
+ On a dev box this is shorthand for "`az login` is current"; on a
117
+ CI runner it covers any of the workload-identity / managed-identity
118
+ sub-credentials.
119
+ """
120
+ started = time.time()
121
+ try:
122
+ from azure.identity import DefaultAzureCredential
123
+ except ImportError:
124
+ return PreflightCheck(
125
+ name="azure_auth",
126
+ display_name="Azure authentication",
127
+ status="skip",
128
+ message="azure-identity SDK not installed.",
129
+ remediation="Install the agent extra: pip install agentops-accelerator[agent].",
130
+ duration_seconds=time.time() - started,
131
+ )
132
+ try:
133
+ cred = DefaultAzureCredential(
134
+ exclude_developer_cli_credential=True, process_timeout=30
135
+ )
136
+ token = cred.get_token(_AZURE_RM_SCOPE)
137
+ except Exception as exc: # noqa: BLE001
138
+ # Humanize the wall-of-text DefaultAzureCredential failure.
139
+ text = str(exc).lower()
140
+ if (
141
+ "azureclicredential: failed to invoke the azure cli" in text
142
+ or "no accounts were found in the cache" in text
143
+ ):
144
+ msg = "Not signed in to Azure."
145
+ remediation = (
146
+ "Run `az login` in this terminal, then re-run the command."
147
+ )
148
+ else:
149
+ snippet = str(exc).splitlines()[0][:200]
150
+ msg = f"Azure token acquisition failed: {snippet}"
151
+ remediation = (
152
+ "Check your network, your `az login` state, and the "
153
+ "cockpit server logs at DEBUG for the full credential "
154
+ "chain."
155
+ )
156
+ return PreflightCheck(
157
+ name="azure_auth",
158
+ display_name="Azure authentication",
159
+ status="fail",
160
+ message=msg,
161
+ remediation=remediation,
162
+ duration_seconds=time.time() - started,
163
+ )
164
+ expires_in = int(token.expires_on - time.time())
165
+ return PreflightCheck(
166
+ name="azure_auth",
167
+ display_name="Azure authentication",
168
+ status="ok",
169
+ message=f"ARM token acquired (expires in {expires_in // 60} min)",
170
+ duration_seconds=time.time() - started,
171
+ )
172
+
173
+
174
+ def _check_foundry_project() -> PreflightCheck:
175
+ """`AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` reachable + App Insights wired."""
176
+ started = time.time()
177
+ endpoint = os.getenv("AZURE_AI_FOUNDRY_PROJECT_ENDPOINT")
178
+ if not endpoint:
179
+ return PreflightCheck(
180
+ name="foundry_project",
181
+ display_name="Foundry project",
182
+ status="skip",
183
+ message="AZURE_AI_FOUNDRY_PROJECT_ENDPOINT is not set.",
184
+ remediation=(
185
+ "Export AZURE_AI_FOUNDRY_PROJECT_ENDPOINT=<your-project-url> "
186
+ "to enable Foundry-aware checks."
187
+ ),
188
+ duration_seconds=time.time() - started,
189
+ )
190
+ try:
191
+ from agentops.utils.foundry_discovery import (
192
+ resolve_appinsights_connection_with_reason,
193
+ )
194
+ except ImportError:
195
+ return PreflightCheck(
196
+ name="foundry_project",
197
+ display_name="Foundry project",
198
+ status="skip",
199
+ message="agentops.utils.foundry_discovery not available.",
200
+ duration_seconds=time.time() - started,
201
+ )
202
+ conn, reason = resolve_appinsights_connection_with_reason(endpoint)
203
+ if conn:
204
+ return PreflightCheck(
205
+ name="foundry_project",
206
+ display_name="Foundry project",
207
+ status="ok",
208
+ message="Project reachable; App Insights auto-discovered.",
209
+ duration_seconds=time.time() - started,
210
+ )
211
+ return PreflightCheck(
212
+ name="foundry_project",
213
+ display_name="Foundry project",
214
+ status="warn",
215
+ message=f"Discovery failed — {reason}",
216
+ remediation=(
217
+ "Confirm the signed-in identity has Reader on the project "
218
+ "resource group, then re-run."
219
+ ),
220
+ duration_seconds=time.time() - started,
221
+ )
222
+
223
+
224
+ def _check_application_insights_env(
225
+ foundry_check: Optional[PreflightCheck] = None,
226
+ ) -> PreflightCheck:
227
+ """Heads-up when neither env var nor Foundry discovery yields a
228
+ connection string. The production telemetry tile will stay grey."""
229
+ started = time.time()
230
+ explicit_connection_string = os.getenv(
231
+ "APPLICATIONINSIGHTS_CONNECTION_STRING"
232
+ ) or os.getenv("AGENTOPS_APPLICATIONINSIGHTS_CONNECTION_STRING")
233
+ if explicit_connection_string:
234
+ from agentops.utils.telemetry import is_appinsights_connection_string
235
+
236
+ if not is_appinsights_connection_string(explicit_connection_string):
237
+ return PreflightCheck(
238
+ name="app_insights",
239
+ display_name="Application Insights",
240
+ status="warn",
241
+ message=(
242
+ "APPLICATIONINSIGHTS_CONNECTION_STRING is set but is not "
243
+ "a valid App Insights connection string."
244
+ ),
245
+ remediation=(
246
+ "Set the full App Insights connection string from the "
247
+ "Azure portal or let Foundry discovery resolve it."
248
+ ),
249
+ duration_seconds=time.time() - started,
250
+ )
251
+ return PreflightCheck(
252
+ name="app_insights",
253
+ display_name="Application Insights",
254
+ status="ok",
255
+ message="APPLICATIONINSIGHTS_CONNECTION_STRING is set.",
256
+ duration_seconds=time.time() - started,
257
+ )
258
+
259
+ if foundry_check and foundry_check.status == "ok":
260
+ return PreflightCheck(
261
+ name="app_insights",
262
+ display_name="Application Insights",
263
+ status="ok",
264
+ message="Resolved via Foundry discovery.",
265
+ duration_seconds=time.time() - started,
266
+ )
267
+
268
+ if (
269
+ foundry_check
270
+ and foundry_check.status == "warn"
271
+ and "discovery failed" in foundry_check.message.lower()
272
+ and "returned no application insights connection"
273
+ not in foundry_check.message.lower()
274
+ ):
275
+ return PreflightCheck(
276
+ name="app_insights",
277
+ display_name="Application Insights",
278
+ status="warn",
279
+ message=(
280
+ "Could not verify the Foundry App Insights connection because "
281
+ "Foundry discovery did not complete."
282
+ ),
283
+ remediation=(
284
+ "Fix the Foundry project warning above, or set "
285
+ "APPLICATIONINSIGHTS_CONNECTION_STRING explicitly."
286
+ ),
287
+ duration_seconds=time.time() - started,
288
+ )
289
+
290
+ # Try Foundry discovery as a fallback (uses the same cached helper).
291
+ endpoint = os.getenv("AZURE_AI_FOUNDRY_PROJECT_ENDPOINT")
292
+ if endpoint:
293
+ try:
294
+ from agentops.utils.foundry_discovery import (
295
+ resolve_appinsights_connection_with_reason,
296
+ )
297
+ conn, _ = resolve_appinsights_connection_with_reason(endpoint)
298
+ if conn:
299
+ return PreflightCheck(
300
+ name="app_insights",
301
+ display_name="Application Insights",
302
+ status="ok",
303
+ message="Resolved via Foundry discovery.",
304
+ duration_seconds=time.time() - started,
305
+ )
306
+ except ImportError:
307
+ pass
308
+ return PreflightCheck(
309
+ name="app_insights",
310
+ display_name="Application Insights",
311
+ status="warn",
312
+ message=(
313
+ "No connection string available; production telemetry will be empty."
314
+ ),
315
+ remediation=(
316
+ "Wire App Insights to your Foundry project (Project details "
317
+ "> Connected resources > Add connection > Application "
318
+ "Insights) or set APPLICATIONINSIGHTS_CONNECTION_STRING."
319
+ ),
320
+ duration_seconds=time.time() - started,
321
+ )
322
+
323
+
324
+ # ---------------------------------------------------------------------------
325
+ # Public entry point
326
+ # ---------------------------------------------------------------------------
327
+
328
+
329
+ _Scope = Literal["doctor", "cockpit", "agent_serve"]
330
+
331
+
332
+ def run_preflight(
333
+ workspace: Path,
334
+ *,
335
+ scope: _Scope = "doctor",
336
+ ) -> PreflightReport:
337
+ """Run every check applicable to ``scope`` and return the report."""
338
+ checks: List[PreflightCheck] = []
339
+
340
+ checks.append(_check_workspace(workspace))
341
+
342
+ # Auth is needed by every scope that talks to Azure.
343
+ checks.append(_check_azure_cli())
344
+
345
+ # Foundry / App Insights probes are advisory; they help the user
346
+ # understand *why* certain sources will be silent rather than
347
+ # blocking the run.
348
+ foundry_check = _check_foundry_project()
349
+ checks.append(foundry_check)
350
+ checks.append(_check_application_insights_env(foundry_check))
351
+
352
+ return PreflightReport(checks=checks)
353
+
354
+
355
+ def format_report(
356
+ report: PreflightReport,
357
+ *,
358
+ color: Optional[bool] = None,
359
+ show_ok_details: bool = False,
360
+ ) -> str:
361
+ """Render the report as a human-friendly multi-line summary.
362
+
363
+ When ``color`` is ``None`` (the default), ANSI escapes are emitted
364
+ only when ``stderr`` is a TTY and the ``NO_COLOR`` env var is not
365
+ set — so piping the output into a log file or CI buffer produces
366
+ clean ASCII automatically.
367
+
368
+ Layout
369
+ ------
370
+ ::
371
+
372
+ AgentOps pre-flight 3 ok · 1 warning · 0 failed
373
+
374
+ ✓ Workspace /repo/path
375
+ ✓ Azure authentication ARM token acquired (expires in 89 min)
376
+ ✓ Foundry project Project reachable; App Insights auto-discovered.
377
+ ⚠ Application Insights No connection string available; production telemetry will be empty.
378
+ → Wire App Insights in Foundry or set APPLICATIONINSIGHTS_CONNECTION_STRING.
379
+
380
+ When every check is ``ok`` the body is collapsed to a single
381
+ one-liner by default so a healthy run does not clutter the terminal.
382
+ Set ``show_ok_details`` when the caller wants to show which checks
383
+ passed even on a healthy run.
384
+ """
385
+ if color is None:
386
+ import sys
387
+ color = (
388
+ sys.stderr.isatty()
389
+ and not os.environ.get("NO_COLOR")
390
+ )
391
+ counts = {"ok": 0, "warn": 0, "skip": 0, "fail": 0}
392
+ for c in report.checks:
393
+ counts[c.status] = counts.get(c.status, 0) + 1
394
+
395
+ # ANSI palette — green / yellow / dim / red.
396
+ palette = {
397
+ "ok": "32",
398
+ "warn": "33",
399
+ "skip": "37",
400
+ "fail": "31",
401
+ }
402
+ glyphs = {
403
+ "ok": "\u2713", # check
404
+ "warn": "\u26a0", # warning sign
405
+ "skip": "\u00b7", # middle dot
406
+ "fail": "\u2717", # ballot x
407
+ }
408
+
409
+ def _color(text: str, status: _Status) -> str:
410
+ if not color:
411
+ return text
412
+ return f"\x1b[{palette[status]}m{text}\x1b[0m"
413
+
414
+ # Headline summary — counts are shown in their tone color so the
415
+ # eye lands on whatever is non-zero.
416
+ pieces = [_color(f"{counts['ok']} ok", "ok")]
417
+ if counts["warn"]:
418
+ pieces.append(_color(f"{counts['warn']} warning", "warn"))
419
+ if counts["fail"]:
420
+ pieces.append(_color(f"{counts['fail']} failed", "fail"))
421
+ if counts["skip"]:
422
+ pieces.append(_color(f"{counts['skip']} skipped", "skip"))
423
+ summary = " \u00b7 ".join(pieces)
424
+ lines = [f"AgentOps pre-flight {summary}"]
425
+
426
+ # Healthy-run short circuit: no per-check rows when everything passed.
427
+ if (
428
+ not show_ok_details
429
+ and not report.has_failures
430
+ and not report.has_warnings
431
+ and counts["skip"] == 0
432
+ ):
433
+ return lines[0]
434
+
435
+ lines.append("")
436
+
437
+ # Compute display-name column width once for clean alignment.
438
+ label_w = max(
439
+ (len(c.display_name or c.name) for c in report.checks),
440
+ default=12,
441
+ )
442
+
443
+ for c in report.checks:
444
+ glyph = _color(glyphs[c.status], c.status)
445
+ label = (c.display_name or c.name).ljust(label_w)
446
+ indent = " " * (label_w + 5)
447
+ available_width = max(60, 100 - len(indent))
448
+ message_lines = textwrap.wrap(
449
+ c.message,
450
+ width=available_width,
451
+ break_long_words=False,
452
+ break_on_hyphens=False,
453
+ ) or [""]
454
+ lines.append(f" {glyph} {label} {message_lines[0]}")
455
+ for continuation in message_lines[1:]:
456
+ lines.append(f"{indent} {continuation}")
457
+ # Show the remediation hint for warn / fail (action required)
458
+ # AND for skip (so the user knows how to enable the check).
459
+ if c.remediation and c.status in ("warn", "fail", "skip"):
460
+ remediation_lines = textwrap.wrap(
461
+ c.remediation,
462
+ width=available_width,
463
+ break_long_words=False,
464
+ break_on_hyphens=False,
465
+ ) or [""]
466
+ lines.append(f"{indent}\u2192 {remediation_lines[0]}")
467
+ for continuation in remediation_lines[1:]:
468
+ lines.append(f"{indent} {continuation}")
469
+
470
+ return "\n".join(lines)