Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions inferedgelab/commands/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from inferedgelab.compare.comparator import build_runtime_compare_report, compare_group, render_runtime_compare_markdown
from inferedgelab.result.loader import load_results_grouped_by_compare_key
from inferedgelab.services.compare_service import build_compare_bundle
from inferedgelab.services.guard_analysis import guard_status, guard_verdict


def _fmt_num(v):
Expand Down Expand Up @@ -47,15 +48,19 @@ def _render_guard_analysis(guard_analysis: dict | None) -> None:
if not guard_analysis:
return

if guard_analysis.get("status") == "skipped":
normalized_status = guard_status(guard_analysis)
if normalized_status == "skipped":
rprint("[yellow]Warning[/yellow]: InferEdgeAIGuard is not installed. Guard analysis skipped.")
return

rprint("[bold]Guard Analysis[/bold]")
rprint(f"- status: {guard_analysis.get('status')}")
rprint(f"- status: {normalized_status}")
rprint(f"- guard_verdict: {guard_verdict(guard_analysis)}")
if guard_analysis.get("primary_reason"):
rprint(f"- primary_reason: {guard_analysis.get('primary_reason')}")
rprint(f"- confidence: {guard_analysis.get('confidence')}")

for field in ("anomalies", "suspected_causes", "recommendations"):
for field in ("anomalies", "evidence", "suspected_causes", "recommendations"):
rprint(f"- {field}:")
values = guard_analysis.get(field) or []
if values:
Expand Down
11 changes: 8 additions & 3 deletions inferedgelab/commands/compare_latest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rich.table import Table

from inferedgelab.services.compare_service import build_compare_latest_bundle
from inferedgelab.services.guard_analysis import guard_status, guard_verdict


def _handle_error_or_warning(message: str, strict: bool) -> None:
Expand Down Expand Up @@ -38,15 +39,19 @@ def _render_guard_analysis(guard_analysis: dict | None) -> None:
if not guard_analysis:
return

if guard_analysis.get("status") == "skipped":
normalized_status = guard_status(guard_analysis)
if normalized_status == "skipped":
rprint("[yellow]Warning[/yellow]: InferEdgeAIGuard is not installed. Guard analysis skipped.")
return

rprint("[bold]Guard Analysis[/bold]")
rprint(f"- status: {guard_analysis.get('status')}")
rprint(f"- status: {normalized_status}")
rprint(f"- guard_verdict: {guard_verdict(guard_analysis)}")
if guard_analysis.get("primary_reason"):
rprint(f"- primary_reason: {guard_analysis.get('primary_reason')}")
rprint(f"- confidence: {guard_analysis.get('confidence')}")

for field in ("anomalies", "suspected_causes", "recommendations"):
for field in ("anomalies", "evidence", "suspected_causes", "recommendations"):
rprint(f"- {field}:")
values = guard_analysis.get(field) or []
if values:
Expand Down
99 changes: 96 additions & 3 deletions inferedgelab/report/html_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from html import escape
from typing import Any, Dict, Optional

from inferedgelab.services.guard_analysis import guard_primary_reason, guard_status, guard_verdict


def _fmt_num(v: Optional[float]) -> str:
if v is None:
Expand Down Expand Up @@ -149,30 +151,121 @@ def _guard_values_to_html(values: Any) -> str:
return "\n".join(f"<li>{escape(str(value))}</li>" for value in values)


def _guard_source_to_html(source: Any) -> str:
if not isinstance(source, dict) or not source:
return ""
items = "\n".join(
f"<li><strong>{escape(str(key))}</strong>: <code>{escape(str(value))}</code></li>"
for key, value in source.items()
)
return f"<p><strong>source</strong></p><ul>{items}</ul>"


def _guard_evidence_to_html(evidence: Any) -> str:
if not isinstance(evidence, list) or not evidence:
return ""
rows: list[str] = []
details: list[str] = []
for item in evidence:
if not isinstance(item, dict):
continue
rows.append(
f"""
<tr>
<td>{escape(str(item.get("type", "-")))}</td>
<td>{escape(str(item.get("metric_name", "-")))}</td>
<td>{escape(str(item.get("observed_value", "-")))}</td>
<td>{escape(str(item.get("baseline_value", "-")))}</td>
<td>{escape(str(item.get("threshold", "-")))}</td>
<td>{escape(str(item.get("status", "-")))}</td>
<td>{escape(str(item.get("severity", "-")))}</td>
</tr>
"""
)
explanation = item.get("explanation")
recommendation = item.get("recommendation")
if explanation:
details.append(
"<li>"
f"<strong>{escape(str(item.get('metric_name', 'evidence')))}</strong>: "
f"{escape(str(explanation))}"
+ (
f"<br><em>recommendation</em>: {escape(str(recommendation))}"
if recommendation
else ""
)
+ "</li>"
)
if not rows:
return ""
return f"""
<h3>Guard Evidence</h3>
<table>
<thead>
<tr>
<th>type</th>
<th>metric</th>
<th>observed</th>
<th>baseline</th>
<th>threshold</th>
<th>status</th>
<th>severity</th>
</tr>
</thead>
<tbody>{''.join(rows)}</tbody>
</table>
<ul>{''.join(details)}</ul>
"""


def _guard_analysis_to_html(guard_analysis: Dict[str, Any] | None) -> str:
if guard_analysis is None:
return ""

if guard_analysis.get("status") == "skipped":
normalized_status = guard_status(guard_analysis)
normalized_verdict = guard_verdict(guard_analysis)
verdict_html = (
f'<p><strong>guard_verdict</strong>: <code>{escape(str(normalized_verdict))}</code></p>'
if normalized_verdict is not None
else ""
)
severity_html = (
f'<p><strong>severity</strong>: <code>{escape(str(guard_analysis.get("severity")))}</code></p>'
if guard_analysis.get("severity") is not None
else ""
)
primary_reason = guard_primary_reason(guard_analysis)
primary_reason_html = (
f"<p><strong>primary_reason</strong>: {escape(str(primary_reason))}</p>"
if primary_reason
else ""
)

if normalized_status == "skipped":
return f"""
<h2>Guard Analysis</h2>
<div class="meta">
<p><strong>status</strong>: <code>{escape(str(guard_analysis.get("status")))}</code></p>
<p><strong>status</strong>: <code>{escape(str(normalized_status))}</code></p>
<p><strong>reason</strong>: {escape(str(guard_analysis.get("reason")))}</p>
</div>
"""

return f"""
<h2>Guard Analysis</h2>
<div class="meta">
<p><strong>status</strong>: <code>{escape(str(guard_analysis.get("status")))}</code></p>
<p><strong>status</strong>: <code>{escape(str(normalized_status))}</code></p>
{verdict_html}
{severity_html}
<p><strong>confidence</strong>: <code>{escape(str(guard_analysis.get("confidence")))}</code></p>
{primary_reason_html}
{_guard_source_to_html(guard_analysis.get("source"))}
<p><strong>anomalies</strong></p>
<ul>{_guard_values_to_html(guard_analysis.get("anomalies"))}</ul>
<p><strong>suspected_causes</strong></p>
<ul>{_guard_values_to_html(guard_analysis.get("suspected_causes"))}</ul>
<p><strong>recommendations</strong></p>
<ul>{_guard_values_to_html(guard_analysis.get("recommendations"))}</ul>
{_guard_evidence_to_html(guard_analysis.get("evidence"))}
</div>
"""

Expand Down
52 changes: 49 additions & 3 deletions inferedgelab/report/markdown_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import Any, Dict, Optional

from inferedgelab.services.guard_analysis import guard_primary_reason, guard_status, guard_verdict


def _fmt_num(v: Optional[float]) -> str:
if v is None:
Expand Down Expand Up @@ -42,14 +44,29 @@ def _sorted_accuracy_metric_items(accuracy: Dict[str, Any]) -> list[tuple[str, D
def _append_guard_analysis(lines: list[str], guard_analysis: Dict[str, Any]) -> None:
lines.append("## Guard Analysis")
lines.append("")
lines.append(f"- status: {guard_analysis.get('status')}")

if guard_analysis.get("status") == "skipped":
normalized_status = guard_status(guard_analysis)
normalized_verdict = guard_verdict(guard_analysis)
lines.append(f"- status: {normalized_status}")
if normalized_verdict is not None:
lines.append(f"- guard_verdict: {normalized_verdict}")
if guard_analysis.get("severity") is not None:
lines.append(f"- severity: {guard_analysis.get('severity')}")

if normalized_status == "skipped":
lines.append(f"- reason: {guard_analysis.get('reason')}")
lines.append("")
return

lines.append(f"- confidence: {guard_analysis.get('confidence')}")
primary_reason = guard_primary_reason(guard_analysis)
if primary_reason:
lines.append(f"- primary_reason: {primary_reason}")

source = guard_analysis.get("source")
if isinstance(source, dict) and source:
lines.append("- source:")
for key, value in source.items():
lines.append(f" - {key}: `{value}`")

for field in ("anomalies", "suspected_causes", "recommendations"):
lines.append(f"- {field}:")
Expand All @@ -59,6 +76,35 @@ def _append_guard_analysis(lines: list[str], guard_analysis: Dict[str, Any]) ->
lines.append(f" - {value}")
else:
lines.append(" - -")
evidence = guard_analysis.get("evidence")
if isinstance(evidence, list) and evidence:
lines.append("")
lines.append("### Guard Evidence")
lines.append("")
lines.append("| type | metric | observed | baseline | threshold | status | severity |")
lines.append("| --- | --- | ---: | ---: | ---: | --- | --- |")
for item in evidence:
if not isinstance(item, dict):
continue
lines.append(
"| "
f"{item.get('type', '-')} | "
f"{item.get('metric_name', '-')} | "
f"{item.get('observed_value', '-')} | "
f"{item.get('baseline_value', '-')} | "
f"{item.get('threshold', '-')} | "
f"{item.get('status', '-')} | "
f"{item.get('severity', '-')} |"
)
for item in evidence:
if not isinstance(item, dict):
continue
explanation = item.get("explanation")
recommendation = item.get("recommendation")
if explanation:
lines.append(f"- {item.get('metric_name', 'evidence')}: {explanation}")
if recommendation:
lines.append(f" - recommendation: {recommendation}")
lines.append("")


Expand Down
9 changes: 7 additions & 2 deletions inferedgelab/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from .compare_service import build_compare_bundle, select_latest_compare_pair
"""Service-layer helpers for InferEdgeLab.

__all__ = ["build_compare_bundle", "select_latest_compare_pair"]
Keep this package initializer light. Several services are intentionally allowed
to import report/rendering modules, so importing compare_service here can create
cycles during direct module imports.
"""

__all__: list[str] = []
5 changes: 4 additions & 1 deletion inferedgelab/services/api_response_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import Any

from inferedgelab.services.guard_analysis import guard_status, guard_verdict


def build_api_response_bundle(
bundle: dict[str, Any],
Expand Down Expand Up @@ -80,7 +82,8 @@ def _build_summary(
or precision.get("comparison_mode"),
"precision_pair": judgement.get("precision_pair") or precision.get("pair"),
"deployment_decision": deployment_decision.get("decision"),
"guard_status": (guard_analysis or {}).get("status"),
"guard_status": guard_status(guard_analysis),
"guard_verdict": guard_verdict(guard_analysis),
}


Expand Down
Loading
Loading