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