diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c0c2781d..ac349d63 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "agentops-accelerator", "source": "../../plugins/agentops", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Toolkit and Microsoft Foundry agents.", - "version": "0.8.8", + "version": "0.9.0", "keywords": [ "agentops", "evaluation", diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index c0c2781d..ac349d63 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "agentops-accelerator", "source": "../../plugins/agentops", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Toolkit and Microsoft Foundry agents.", - "version": "0.8.8", + "version": "0.9.0", "keywords": [ "agentops", "evaluation", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ea0aff0..7bd29653 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,6 +149,8 @@ jobs: needs: [lint, test] runs-on: ubuntu-latest environment: staging + outputs: + version: ${{ steps.package-version.outputs.version }} permissions: id-token: write # Required for PyPI Trusted Publishing (OIDC) steps: @@ -171,10 +173,44 @@ jobs: - name: Build package run: uv build + - name: Determine built version + id: package-version + run: | + VERSION=$(uv run python - <<'PY' + from email.parser import BytesParser + from pathlib import Path + from zipfile import ZipFile + + wheels = sorted(Path("dist").glob("*.whl")) + if len(wheels) != 1: + raise SystemExit( + f"Expected exactly one wheel in dist/, found {len(wheels)}." + ) + with ZipFile(wheels[0]) as archive: + metadata_files = [ + name + for name in archive.namelist() + if name.endswith(".dist-info/METADATA") + ] + if len(metadata_files) != 1: + raise SystemExit( + "Expected exactly one .dist-info/METADATA file in " + f"{wheels[0].name}, found {len(metadata_files)}." + ) + metadata = BytesParser().parsebytes(archive.read(metadata_files[0])) + version = metadata.get("Version") + if not version: + raise SystemExit(f"No Version field found in {wheels[0].name}.") + print(version) + PY + ) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Built package version: $VERSION" + - name: Show version run: | ls -la dist/ - uv run python -c "from importlib.metadata import version; print(f'Dev version: {version(\"agentops-accelerator\")}')" + echo "Dev version: ${{ steps.package-version.outputs.version }}" - name: Publish to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 @@ -189,33 +225,27 @@ jobs: needs: publish-dev runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: Set up Python uses: actions/setup-python@v7 with: python-version: "3.12" - - name: Determine expected version - id: version - run: | - pip install setuptools-scm - VERSION=$(python -m setuptools_scm) - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "Expected dev version: $VERSION" - - name: Install from TestPyPI + env: + PACKAGE_VERSION: ${{ needs.publish-dev.outputs.version }} run: | + if [ -z "$PACKAGE_VERSION" ]; then + echo "::error::publish-dev did not expose the built package version." + exit 1 + fi # TestPyPI serves its simple index through a CDN, so a freshly # uploaded release is not immediately resolvable even after the # upload returns 200 OK. Allow ~6 minutes for it to propagate. ATTEMPTS=12 for i in $(seq 1 "$ATTEMPTS"); do - echo "Attempt $i/$ATTEMPTS: installing agentops-accelerator==${{ steps.version.outputs.version }}" + echo "Attempt $i/$ATTEMPTS: installing agentops-accelerator==$PACKAGE_VERSION" if pip install --no-cache-dir \ - "agentops-accelerator==${{ steps.version.outputs.version }}" \ + "agentops-accelerator==$PACKAGE_VERSION" \ --index-url https://test.pypi.org/simple/ \ --extra-index-url https://pypi.org/simple/; then exit 0 @@ -225,7 +255,7 @@ jobs: sleep 30 fi done - echo "::error::agentops-accelerator==${{ steps.version.outputs.version }} was not available from TestPyPI after $ATTEMPTS attempts (~6 min)." + echo "::error::agentops-accelerator==$PACKAGE_VERSION was not available from TestPyPI after $ATTEMPTS attempts (~6 min)." exit 1 - name: Smoke test @@ -234,13 +264,15 @@ jobs: agentops --help - name: Summary + env: + PACKAGE_VERSION: ${{ needs.publish-dev.outputs.version }} run: | echo "## ✅ Dev build published and verified" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "- Version: \`${{ steps.version.outputs.version }}\`" >> "$GITHUB_STEP_SUMMARY" - echo "- TestPyPI: https://test.pypi.org/project/agentops-accelerator/${{ steps.version.outputs.version }}/" >> "$GITHUB_STEP_SUMMARY" + echo "- Version: \`$PACKAGE_VERSION\`" >> "$GITHUB_STEP_SUMMARY" + echo "- TestPyPI: https://test.pypi.org/project/agentops-accelerator/$PACKAGE_VERSION/" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Install: \`pip install agentops-accelerator==${{ steps.version.outputs.version }} --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/\`" >> "$GITHUB_STEP_SUMMARY" + echo "Install: \`pip install agentops-accelerator==$PACKAGE_VERSION --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/\`" >> "$GITHUB_STEP_SUMMARY" # Validate that the VSIX extension packages correctly build-vsix: diff --git a/CHANGELOG.md b/CHANGELOG.md index 00de98c0..eb253b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,39 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres ## [Unreleased] +## [0.9.0] - 2026-08-14 + +### Changed +- **The AgentOps Cockpit now focuses on five release-readiness sections.** + The page renders only the readiness and Doctor ship verdicts, Foundry and + GitHub connections, the 13-item observability checklist, the latest Doctor + findings, and prioritized next actions. Date-window and auto-refresh controls + were removed because the remaining sections describe current configuration + and the latest analysis rather than time-series dashboards. +- **Observability readiness now distinguishes native Foundry tracing from + optional custom spans.** Hosted agents and prompt agents are recognized as + natively instrumented, while repository OpenTelemetry spans are reported as + optional extensions. App Insights linkage, rubric evaluators, alert + definitions, and tracing evidence are resolved independently so unknown state + is no longer presented as a failure. + +### Fixed +- **Project-managed Application Insights connections now work throughout + Doctor and Cockpit.** AgentOps discovers the attached Application Insights + ARM resource from credential-free Foundry connection metadata and queries it + with `LogsQueryClient.query_resource`, avoiding false missing-telemetry + findings when no API-key connection string exists. +- **`agentops agent register` now derives hosted-agent names from Foundry + target URLs.** A command that supplies only `--sponsor` correctly resolves + names such as `helpdeskbot`, matching the fallback promised by `--help`. + +### Removed +- **The experimental AgentOps telemetry import and custom Operations Dashboard + have been removed.** The public telemetry command group, Log Analytics + workbook deployment, bundled workbook/KQL assets, and dashboard-specific + posture rule are no longer shipped while native product observability support + is reviewed. + ## [0.8.8] - 2026-08-14 ### Fixed diff --git a/docs/index.md b/docs/index.md index b53709bc..c2ad57d0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -78,8 +78,8 @@ candidate versions become a release. Start with
{_html_escape(tenant)}"
- tenant_hint = "Resolved from `az account show`."
- else:
- tenant_status = "warn"
- tenant_label = "Azure tenant unknown"
- tenant_detail = (
- "Run az login so Foundry deep-links open in "
- "the correct directory."
- )
- tenant_hint = None
-
- if agent_id:
- agent_status = "ok"
- agent_label = "Agent configured"
- agent_detail = (
- f"{_html_escape(agent_id)}"
- )
- agent_hint = f"Resolved from {agent_source}."
- else:
- agent_status = "muted"
- agent_label = "No agent pinned"
- agent_detail = (
- "Set agent in agentops.yaml when "
- "you want the cockpit to surface a specific Foundry agent."
- )
- agent_hint = None
-
- telemetry_status = telemetry.get("tone", "muted")
- telemetry_label = telemetry.get("label", "Telemetry off")
- telemetry_detail = telemetry.get("detail", "")
- telemetry_hint = telemetry.get("hint")
+ github = _resolve_github_repository(workspace)
+ github_name = github.get("name")
+ github_url = github.get("url")
items = [
{
@@ -1751,34 +1701,46 @@ def _build_foundry_connection(
"copy_value": project_copy_value,
},
{
- "title": "Azure tenant",
- "status": tenant_status,
- "label": tenant_label,
- "detail": tenant_detail,
- "hint": tenant_hint,
- },
- {
- "title": "Agent",
- "status": agent_status,
- "label": agent_label,
- "detail": agent_detail,
- "hint": agent_hint,
- "link": (_foundry_deeplinks(workspace).get("agent") if agent_id else None),
- "link_label": "Open agent",
- },
- {
- "title": "Application Insights",
- "status": telemetry_status,
- "label": telemetry_label,
- "detail": telemetry_detail,
- "hint": telemetry_hint,
- "link": telemetry.get("portal_url"),
- "link_label": "Open App Insights",
+ "title": "GitHub repository",
+ "status": "ok" if github_url else "warn",
+ "label": github_name or "GitHub repository missing",
+ "detail": (
+ "Repository remote resolved from local Git configuration."
+ if github_url
+ else "Configure an origin remote that points to GitHub."
+ ),
+ "link": github_url,
+ "link_label": "Open in GitHub",
+ "copy_value": github_url,
},
]
+ return {"items": items}
+
+
+def _resolve_github_repository(workspace: Path) -> Dict[str, Optional[str]]:
+ """Resolve a browser URL from the local ``origin`` remote."""
+ proc = _run_quick(["git", "remote", "get-url", "origin"], cwd=workspace)
+ if proc is None or proc.returncode != 0:
+ return {"name": None, "url": None}
+ remote = (proc.stdout or "").strip()
+ if not remote:
+ return {"name": None, "url": None}
+
+ match = re.match(
+ r"^(?:https?://|ssh://git@)(?PAPPLICATIONINSIGHTS_CONNECTION_STRING or attach "
"App Insights to the Foundry project. "
@@ -1952,25 +1884,31 @@ def _build_readiness_checklist(
}
)
- # Client-side tracing is not auto-detectable from outside the
- # caller process — it depends on whether the application itself
- # imports the OTel SDK and instruments outbound model / agent
- # calls. Surface it as an "info" reminder + docs link so the
- # readiness panel stays honest about what AgentOps can verify.
- client_side_ok = bool(os.getenv("AGENTOPS_CLIENT_TRACING")) or tracing_ok
+ agent_tracing_ready = bool(foundry_runtime or custom_tracing_path)
checks.append(
{
- "title": "Client-side tracing (app code instrumented)",
- "status": "info" if client_side_ok else "muted",
+ "title": "Agent tracing instrumentation",
+ "status": "ok" if agent_tracing_ready else "muted",
"detail": (
- "How to complete: instrument the application "
- "that calls your agent, not just AgentOps. Add Azure Monitor "
- "OpenTelemetry to the app process, configure the same "
- "APPLICATIONINSIGHTS_CONNECTION_STRING, and wrap "
- "outbound model/agent/tool calls so a user request shows one "
- "end-to-end trace across client code, agent execution, and "
- "dependencies. Set AGENTOPS_CLIENT_TRACING=1 only "
- "after the app is instrumented. "
+ f"Microsoft Foundry provides native tracing for this "
+ f"{_html_escape(foundry_runtime)}; no application-side "
+ "OpenTelemetry setup is required."
+ + (
+ " Additional custom spans were detected in "
+ f"{_html_escape(custom_tracing_path)}."
+ if custom_tracing_path
+ else " Custom spans remain optional."
+ )
+ if foundry_runtime
+ else (
+ "Detected custom OpenTelemetry instrumentation in "
+ f"{_html_escape(custom_tracing_path)}."
+ )
+ if custom_tracing_path
+ else "How to complete: instrument the agent "
+ "runtime with OpenTelemetry. Foundry hosted "
+ "agents and prompt agents provide this natively; custom or "
+ "external runtimes must configure their own tracer and exporter. "
'Foundry tracing docs ↗'
),
@@ -2011,16 +1949,19 @@ def _build_readiness_checklist(
}
)
- rubrics = agentops_config.get("rubrics")
- rubric_ready = isinstance(rubrics, list) and bool(rubrics)
+ rubric_ready, rubric_source = _detect_rubric_evaluator(
+ workspace,
+ agentops_config,
+ )
checks.append(
{
"title": "Optional rubric evaluator gate",
"status": "ok" if rubric_ready else "muted",
"detail": (
- "Detected rubrics: in agentops.yaml. "
- "Keep thresholds bound only to metric names emitted by your "
- "Foundry / azd run."
+ f"Detected a rubric evaluator in "
+ f"{_html_escape(rubric_source)}. "
+ "Keep thresholds bound only to metric names emitted by the "
+ "corresponding Foundry / azd evaluation run."
if rubric_ready
else "How to complete: optional - add "
"rubrics: only after a real Foundry rubric evaluator "
@@ -2189,29 +2130,22 @@ def _build_readiness_checklist(
}
)
- alerts = bool(telemetry.get("portal_url")) and tracing_ok
+ alerts, alerts_source = _detect_alert_configuration(
+ workspace,
+ agentops_config,
+ )
checks.append(
{
"title": "Alerts wired",
- "status": "info" if alerts else "muted",
+ "status": "ok" if alerts else "info",
"detail": (
- "App Insights is linked. Next, create Azure Monitor alert "
- "rules for the agent workload: failures in "
- "requests, slow P95 duration, high dependency "
- "error rate, and optionally AgentOps CI/Doctor spans such as "
- "agentops.eval.* and "
- "agentops.agent.finding.*. "
- 'Alert docs ↗'
+ "Detected Azure Monitor alert configuration in "
+ f"{_html_escape(alerts_source)}."
if alerts
- else "How to complete: once tracing is wired, "
- "create Azure Monitor / App Insights alert rules for the "
- "agent workload. Start with requests | where success == false "
- "for failures, P95 request duration for latency, and "
- "dependencies failures for downstream services. "
- "If you want AgentOps signals in alerts too, add rules over "
- "agentops.eval.* and "
- "agentops.agent.finding.* custom dimensions. "
+ else "Not verified: Cockpit found no alert definition in repo "
+ "configuration, and the latest Doctor analysis does not inventory "
+ "Azure Monitor alert rules. This does not claim that cloud-side "
+ "alerts are absent. Define alerts as IaC or verify them in Azure Monitor. "
'Alert docs ↗'
),
@@ -2296,87 +2230,39 @@ def _continuous_eval_status_from_watchdog(
def _build_next_actions(
- workspace: Path,
- telemetry: Dict[str, Any],
watchdog: Dict[str, Any],
readiness: Dict[str, Any],
- eval_payload: Dict[str, Any],
) -> Dict[str, Any]:
- """Surface a short, ordered list of contextual next actions.
-
- Each action either opens a Cockpit section or links to the relevant
- Foundry / Azure runtime view.
- """
+ """Prioritize Doctor findings, then incomplete readiness checks."""
actions: List[Dict[str, Any]] = []
-
- if not telemetry.get("enabled"):
- actions.append(
- {
- "title": "Wire App Insights to Foundry",
- "detail": (
- "Tracing is off. Without it Foundry Monitor, "
- "Evaluations, and Traces stay empty."
- ),
- "cta": "Open Foundry connected resources",
- "url": _resolve_foundry_project_url(workspace),
- }
- )
-
- readiness_checks = readiness.get("checks", [])
- if any(c["status"] == "warn" for c in readiness_checks if c["title"].startswith("CI eval gate")):
- actions.append(
- {
- "title": "Add a CI eval workflow",
- "detail": (
- "Generate a PR workflow. Prompt-agent repos use the "
- "official Microsoft eval runner when compatible; hosted "
- "and fallback cases use agentops eval run."
- ),
- "cta": "agentops workflow generate",
- }
- )
-
- latest_findings = watchdog.get("latest_findings") or []
- crit_findings = [f for f in latest_findings if (f.get("severity") or "").lower() == "critical"]
- if crit_findings:
+ severity_order = {"critical": 0, "warning": 1, "info": 2}
+ findings = sorted(
+ watchdog.get("latest_findings") or [],
+ key=lambda item: severity_order.get(str(item.get("severity") or "").lower(), 3),
+ )
+ for finding in findings:
+ recommendation = str(finding.get("recommendation") or finding.get("summary") or "")
actions.append(
{
- "title": f"Fix {len(crit_findings)} critical Doctor finding(s)",
- "detail": "Doctor surfaced critical readiness gaps in the repo.",
- "cta": "Jump to AgentOps Doctor",
+ "title": f"Fix Doctor: {finding.get('title') or finding.get('id') or 'finding'}",
+ "detail": _render_recommendation_body(recommendation),
+ "cta": "Open Doctor finding",
"anchor": "#section-agentops-doctor",
}
)
- official_eval = _official_eval_artifact_status(workspace)
- has_eval_proof = (
- bool(eval_payload.get("has_runs"))
- or bool(eval_payload.get("runs"))
- or bool(official_eval.get("present"))
+ readiness_order = {"warn": 0, "muted": 1, "info": 2}
+ incomplete = sorted(
+ (check for check in readiness.get("checks", []) if check.get("status") != "ok"),
+ key=lambda item: readiness_order.get(str(item.get("status") or ""), 3),
)
- if not has_eval_proof:
+ for check in incomplete:
actions.append(
{
- "title": "Run your first evaluation",
- "detail": (
- "No eval gate evidence yet. Run agentops eval run "
- "locally, or run the generated official-eval workflow for a "
- "compatible Foundry prompt agent."
- ),
- "cta": "agentops eval run",
- }
- )
-
- evidence = _release_evidence_status(workspace)
- if has_eval_proof and evidence.get("status") in {"missing", "unreadable"}:
- actions.append(
- {
- "title": "Generate release evidence",
- "detail": (
- "Package the latest eval gate, Doctor findings, CI/CD "
- "status, and Foundry links into the release-review artifact."
- ),
- "cta": "agentops doctor --evidence-pack",
+ "title": f"Complete readiness: {check.get('title') or 'configuration'}",
+ "detail": check.get("detail", ""),
+ "cta": "Open readiness item",
+ "anchor": "#section-readiness",
}
)
@@ -2385,8 +2271,7 @@ def _build_next_actions(
{
"title": "All caught up",
"detail": (
- "No outstanding readiness gaps detected in the repo. "
- "Use the Foundry deep-links above to monitor runtime."
+ "All readiness items are complete and Doctor has no findings."
),
"cta": None,
}
@@ -2610,6 +2495,142 @@ def _read_agentops_config(workspace: Path) -> Dict[str, Any]:
return payload if isinstance(payload, dict) else {}
+def _detect_foundry_agent_runtime(
+ workspace: Path,
+ agentops_config: Dict[str, Any],
+) -> Optional[str]:
+ """Identify Foundry runtimes that provide native agent tracing."""
+ path = workspace / "azure.yaml"
+ if path.is_file():
+ try:
+ payload = load_yaml(path)
+ except Exception:
+ payload = None
+ if isinstance(payload, dict):
+ services = payload.get("services")
+ if isinstance(services, dict) and any(
+ isinstance(service, dict)
+ and service.get("host") == "azure.ai.agent"
+ and service.get("kind") == "hosted"
+ for service in services.values()
+ ):
+ return "hosted agent runtime"
+
+ raw_agent = agentops_config.get("agent")
+ if isinstance(raw_agent, str) and raw_agent.strip():
+ try:
+ from agentops.core.agentops_config import classify_agent
+
+ target = classify_agent(
+ raw_agent,
+ protocol=agentops_config.get("protocol"),
+ )
+ except (TypeError, ValueError):
+ target = None
+ if target is not None:
+ if target.kind == "foundry_hosted":
+ return "hosted agent runtime"
+ if target.kind == "foundry_prompt":
+ return "prompt agent runtime"
+
+ for eval_path in (workspace / "src").glob("**/eval.y*ml"):
+ try:
+ eval_payload = load_yaml(eval_path)
+ except Exception:
+ continue
+ if not isinstance(eval_payload, dict):
+ continue
+ agent = eval_payload.get("agent")
+ kind = agent.get("kind") if isinstance(agent, dict) else None
+ if kind == "hosted":
+ return "hosted agent runtime"
+ if kind in {"prompt", "prompt-agent"}:
+ return "prompt agent runtime"
+ return None
+
+
+def _detect_custom_tracing(workspace: Path) -> Optional[str]:
+ """Find optional repository code that emits custom OpenTelemetry spans."""
+ candidates = list((workspace / "src").glob("**/*.py"))
+ candidates.extend(workspace.glob("*.py"))
+ for path in candidates:
+ try:
+ text = path.read_text(encoding="utf-8", errors="ignore")
+ except OSError:
+ continue
+ has_otel = "opentelemetry" in text or "configure_azure_monitor" in text
+ emits_spans = any(
+ marker in text
+ for marker in (
+ "start_as_current_span",
+ "start_span(",
+ "get_tracer(",
+ "configure_azure_monitor(",
+ )
+ )
+ if has_otel and emits_spans:
+ return path.relative_to(workspace).as_posix()
+ return None
+
+
+def _detect_rubric_evaluator(
+ workspace: Path,
+ agentops_config: Dict[str, Any],
+) -> Tuple[bool, str]:
+ """Resolve rubric evidence from AgentOps or azd AI Agent eval config."""
+ rubrics = agentops_config.get("rubrics")
+ if isinstance(rubrics, list) and rubrics:
+ return True, "agentops.yaml"
+
+ for path in (workspace / "src").glob("**/eval.y*ml"):
+ try:
+ payload = load_yaml(path)
+ except Exception:
+ continue
+ if not isinstance(payload, dict):
+ continue
+ evaluators = payload.get("evaluators")
+ if isinstance(evaluators, list) and any(
+ isinstance(evaluator, dict)
+ and bool(evaluator.get("local_uri") or evaluator.get("id"))
+ for evaluator in evaluators
+ ):
+ return True, path.relative_to(workspace).as_posix()
+ return False, ""
+
+
+def _detect_alert_configuration(
+ workspace: Path,
+ agentops_config: Dict[str, Any],
+) -> Tuple[bool, str]:
+ """Find explicit alert definitions without claiming cloud state."""
+ observability = agentops_config.get("observability")
+ if isinstance(observability, dict) and observability.get("alerts"):
+ return True, "agentops.yaml"
+
+ markers = (
+ "microsoft.insights/metricalerts",
+ "microsoft.insights/scheduledqueryrules",
+ "azurerm_monitor_metric_alert",
+ "azurerm_monitor_scheduled_query_rules_alert",
+ )
+ candidates: List[Path] = []
+ for root in (workspace / "infra", workspace / "deploy"):
+ if root.is_dir():
+ for pattern in ("**/*.bicep", "**/*.tf", "**/*.json", "**/*.y*ml"):
+ candidates.extend(root.glob(pattern))
+ for pattern in ("*.bicep", "*.tf"):
+ candidates.extend(workspace.glob(pattern))
+ for path in candidates:
+ try:
+ text = path.read_text(encoding="utf-8", errors="ignore").lower()
+ except OSError:
+ continue
+ if any(marker in text for marker in markers):
+ return True, path.relative_to(workspace).as_posix()
+ return False, ""
+
+
def _read_trace_regression_manifest(workspace: Path) -> Dict[str, Any]:
return _read_json_object(workspace / ".agentops" / "data" / "trace-regression-manifest.json")
@@ -3468,10 +3489,8 @@ def _collapsible_section(
def _render_status_cards_section(
readiness: Dict[str, Any],
watchdog: Dict[str, Any],
- eval_payload: Dict[str, Any],
) -> str:
- """Render the consolidated top status: three clickable cards that
- answer "can I ship?" at a glance (Readiness, Doctor, Eval gate).
+ """Render the two go/no-go verdicts that answer "Can I ship?".
Each card is an anchor to the matching detail section below; the
template's hash-open script expands that (collapsed) section when the
@@ -3503,9 +3522,9 @@ def _card(
readiness_tone = "warn"
readiness_card = _card(
title="Readiness",
- value=readiness_label,
+ value="GO" if readiness_tone == "ok" else "NO-GO",
tone=readiness_tone,
- sub="Observability checks green",
+ sub=readiness_label,
anchor="#section-readiness",
)
@@ -3521,9 +3540,9 @@ def _headline_value(key: str) -> int:
return 0
if not watchdog.get("has_history"):
- doctor_tone = "muted"
- doctor_value = "No runs"
- doctor_sub = "Run agentops doctor"
+ doctor_tone = "warn"
+ doctor_value = "NO-GO"
+ doctor_sub = "No Doctor run"
else:
findings_total = _headline_value("findings_total")
critical = _headline_value("critical")
@@ -3533,7 +3552,7 @@ def _headline_value(key: str) -> int:
doctor_tone = "warn"
else:
doctor_tone = "ok"
- doctor_value = f"{findings_total} finding(s)"
+ doctor_value = "GO" if findings_total == 0 else "NO-GO"
doctor_sub = f"{critical} critical"
doctor_card = _card(
title="Doctor",
@@ -3543,28 +3562,7 @@ def _headline_value(key: str) -> int:
anchor="#section-agentops-doctor",
)
- if not eval_payload.get("has_runs"):
- eval_tone = "muted"
- eval_value = "No runs"
- eval_sub = "Run agentops eval run"
- else:
- passed = bool(eval_payload.get("latest_passed"))
- eval_tone = "ok" if passed else "crit"
- eval_value = "Pass" if passed else "Fail"
- pass_rate = eval_payload.get("pass_rate")
- if isinstance(pass_rate, (int, float)):
- eval_sub = f"{int(pass_rate * 100)}% pass rate"
- else:
- eval_sub = "Latest run"
- eval_card = _card(
- title="Eval gate",
- value=eval_value,
- tone=eval_tone,
- sub=eval_sub,
- anchor="#section-eval-gates",
- )
-
- cards = readiness_card + doctor_card + eval_card
+ cards = readiness_card + doctor_card
return (
'.agentops/results/. Official Microsoft Foundry "
- "AI Agent Evaluation evidence exists under "
- f".agentops/official-eval/ with status {status}. "
- "Run agentops doctor --evidence-pack to package it "
- "for release review."
- )
- else:
- empty_text = (
- "No eval runs yet under .agentops/results/. "
- "Run agentops eval run to populate this section."
- )
- eval_body = (
- eval_caption +
- 'gh auth login "
- "to surface workflow runs here."
- )
- deployments_body = f'