Skip to content

Commit 8ceaed0

Browse files
Gabrymi93claude
andauthored
feat: consuma radar_summary.json da source-observatory (#21)
Aggiunge RadarSummary parser/dataclass e consumer in Renderer: - signals.py: RadarSource, RadarSummary, parse_radar_summary() - render.py: _fetch_radar_summary(), _render_radar_section() in bootstrap, _build_radar_dict() in workspace_triage - test_signals.py: test parse_radar_summary schema e unhealthy filter - test_render.py: aggiornato call_count (2 → 3 fetch distinti) Dipende da source-observatory#118 (feat/radar-summary-json). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 30ecc4a commit 8ceaed0

4 files changed

Lines changed: 153 additions & 2 deletions

File tree

src/agent_context_builder/render.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from .github import GitHubCollector, PR
99
from .git_local import GitLocalCollector, GitState
1010
from .signals import (
11+
RadarSummary,
1112
RepoSignals,
1213
SourceObservatorySignals,
14+
parse_radar_summary,
1315
parse_repo_signals,
1416
parse_source_observatory_signals,
1517
)
@@ -46,6 +48,7 @@ def __init__(
4648
self.fixed_timestamp = fixed_timestamp or datetime.now().isoformat()
4749
# Cache for remote signal fetches — avoids double requests within one build
4850
self._so_signals_cache: SourceObservatorySignals | None | type[_UNSET] = _UNSET
51+
self._radar_cache: RadarSummary | None | type[_UNSET] = _UNSET
4952
self._di_signals_cache: RepoSignals | None | type[_UNSET] = _UNSET
5053

5154
def render_session_bootstrap(self) -> str:
@@ -133,6 +136,10 @@ def render_session_bootstrap(self) -> str:
133136
lines.append(f"- {topic_name}")
134137
lines.append("")
135138

139+
# Radar health (GREEN/YELLOW/RED per fonte)
140+
radar = self._fetch_radar_summary()
141+
lines += self._render_radar_section(radar)
142+
136143
# Source health (only issues — skip stable sources)
137144
so = self._fetch_source_observatory_signals()
138145
lines += self._render_source_health_section(so)
@@ -143,6 +150,41 @@ def render_session_bootstrap(self) -> str:
143150

144151
return "\n".join(lines)
145152

153+
def _fetch_radar_summary(self) -> RadarSummary | None:
154+
if self._radar_cache is not _UNSET:
155+
return self._radar_cache # type: ignore[return-value]
156+
raw = self.github_collector.get_raw_file(
157+
"source-observatory", "data/radar/radar_summary.json"
158+
)
159+
if raw is None:
160+
self._radar_cache = None
161+
return None
162+
try:
163+
result = parse_radar_summary(raw)
164+
except ValueError as exc:
165+
self.github_collector.fetch_errors["source-observatory:radar_summary"] = str(exc)
166+
result = None
167+
self._radar_cache = result
168+
return result
169+
170+
def _render_radar_section(self, radar: RadarSummary | None) -> list[str]:
171+
lines = ["## Radar Status", ""]
172+
if radar is None:
173+
lines.append("> *radar_summary unavailable*")
174+
lines.append("")
175+
return lines
176+
lines.append(
177+
f"Fonti: {radar.sources_total} — "
178+
f"GREEN {radar.green} · YELLOW {radar.yellow} · RED {radar.red} "
179+
f"(probe: {radar.probe_date})"
180+
)
181+
if radar.unhealthy:
182+
lines.append("")
183+
for s in radar.unhealthy:
184+
lines.append(f"- **{s.id}** ({s.protocol}): {s.status} [HTTP {s.http_code}]")
185+
lines.append("")
186+
return lines
187+
146188
def _fetch_source_observatory_signals(self) -> SourceObservatorySignals | None:
147189
if self._so_signals_cache is not _UNSET:
148190
return self._so_signals_cache # type: ignore[return-value]
@@ -263,11 +305,29 @@ def render_workspace_triage(self) -> dict[str, Any]:
263305
for repo, state in repos_state.items()
264306
},
265307
"warnings": self._collect_warnings(prs, repos_state),
308+
"radar": self._build_radar_dict(),
266309
"source_health": self._build_source_health_dict(),
267310
"pipeline_state": self._build_pipeline_state_dict(),
268311
}
269312
return triage
270313

314+
def _build_radar_dict(self) -> dict[str, Any]:
315+
radar = self._fetch_radar_summary()
316+
if radar is None:
317+
return {"available": False}
318+
return {
319+
"available": True,
320+
"probe_date": radar.probe_date,
321+
"sources_total": radar.sources_total,
322+
"green": radar.green,
323+
"yellow": radar.yellow,
324+
"red": radar.red,
325+
"unhealthy": [
326+
{"id": s.id, "status": s.status, "protocol": s.protocol, "http_code": s.http_code}
327+
for s in radar.unhealthy
328+
],
329+
}
330+
271331
def _build_source_health_dict(self) -> dict[str, Any]:
272332
so = self._fetch_source_observatory_signals()
273333
if so is None:

src/agent_context_builder/signals.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,64 @@ def parse_source_observatory_signals(raw: str) -> SourceObservatorySignals:
146146
sources_checked=data.get("sources_checked", len(signals)),
147147
signals=signals,
148148
)
149+
150+
151+
@dataclass
152+
class RadarSource:
153+
"""Single source entry from radar_summary.json."""
154+
155+
id: str
156+
status: str
157+
protocol: str
158+
observation_mode: str
159+
http_code: str
160+
last_check: str
161+
datasets_in_use: list[str] = field(default_factory=list)
162+
163+
164+
@dataclass
165+
class RadarSummary:
166+
"""Radar health summary from source-observatory radar_summary.json."""
167+
168+
generated_at: str
169+
probe_date: str
170+
sources_total: int
171+
green: int
172+
yellow: int
173+
red: int
174+
sources: list[RadarSource] = field(default_factory=list)
175+
176+
@property
177+
def unhealthy(self) -> list[RadarSource]:
178+
return [s for s in self.sources if s.status in ("YELLOW", "RED")]
179+
180+
181+
def parse_radar_summary(raw: str) -> RadarSummary:
182+
try:
183+
data: dict[str, Any] = json.loads(raw)
184+
except json.JSONDecodeError as exc:
185+
raise ValueError(f"Invalid JSON: {exc}") from exc
186+
187+
counts = data.get("status_counts", {})
188+
sources = [
189+
RadarSource(
190+
id=s.get("id", ""),
191+
status=s.get("status", ""),
192+
protocol=s.get("protocol", ""),
193+
observation_mode=s.get("observation_mode", ""),
194+
http_code=s.get("http_code", "-"),
195+
last_check=s.get("last_check", ""),
196+
datasets_in_use=s.get("datasets_in_use") or [],
197+
)
198+
for s in data.get("sources", [])
199+
]
200+
201+
return RadarSummary(
202+
generated_at=data.get("generated_at", "unknown"),
203+
probe_date=data.get("probe_date", "unknown"),
204+
sources_total=data.get("sources_total", len(sources)),
205+
green=counts.get("GREEN", 0),
206+
yellow=counts.get("YELLOW", 0),
207+
red=counts.get("RED", 0),
208+
sources=sources,
209+
)

tests/test_render.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,10 @@ def _raw_file_side_effect(repo, path, ref="main"):
355355
renderer.render_session_bootstrap()
356356
renderer.render_workspace_triage()
357357

358-
# Two distinct files (SO catalog_signals + DI pipeline_signals), each fetched once
359-
assert gh.get_raw_file.call_count == 2
358+
# Three distinct files (radar_summary + catalog_signals + DI pipeline_signals), each fetched once
359+
assert gh.get_raw_file.call_count == 3
360360
paths_fetched = [call.args[1] for call in gh.get_raw_file.call_args_list]
361+
assert "data/radar/radar_summary.json" in paths_fetched
361362
assert "data/catalog/catalog_signals.json" in paths_fetched
362363
assert "registry/pipeline_signals.json" in paths_fetched
363364

tests/test_signals.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,32 @@ def test_parse_repo_signals_missing_fields_use_defaults():
168168
assert rs.generated_at == "unknown"
169169
assert rs.signals[0].status == "ok"
170170
assert rs.signals[0].label == "test"
171+
172+
173+
def test_parse_radar_summary():
174+
from agent_context_builder.signals import parse_radar_summary
175+
176+
raw = json.dumps({
177+
"generated_at": "2026-04-19T09:00:00+00:00",
178+
"probe_date": "2026-04-19",
179+
"sources_total": 3,
180+
"status_counts": {"GREEN": 2, "YELLOW": 1, "RED": 0},
181+
"sources": [
182+
{"id": "inps", "status": "GREEN", "protocol": "ckan",
183+
"observation_mode": "catalog-watch", "http_code": "200",
184+
"last_check": "2026-04-19", "datasets_in_use": ["ds1"]},
185+
{"id": "anac", "status": "YELLOW", "protocol": "ckan",
186+
"observation_mode": "radar-only", "http_code": "200",
187+
"last_check": "2026-04-19", "datasets_in_use": []},
188+
{"id": "istat", "status": "GREEN", "protocol": "sdmx",
189+
"observation_mode": "catalog-watch", "http_code": "200",
190+
"last_check": "2026-04-19", "datasets_in_use": []},
191+
],
192+
})
193+
summary = parse_radar_summary(raw)
194+
assert summary.sources_total == 3
195+
assert summary.green == 2
196+
assert summary.yellow == 1
197+
assert summary.red == 0
198+
assert len(summary.unhealthy) == 1
199+
assert summary.unhealthy[0].id == "anac"

0 commit comments

Comments
 (0)