Skip to content

Commit b563e89

Browse files
authored
feat: contesto data-explorer — temi, gap analysis, deploy status (#35)
* feat: consuma themes.json da data-explorer per context arricchito ACB legge catalog/themes.json (contratto gia' pubblico su data-explorer) e arricchisce tutti e 3 gli output: - session_bootstrap.md: nuova sezione EXPLORER con temi, dataset pubblicati, e gap analysis (clean_ready non su explorer) - workspace_triage.json: nuovo campo 'explorer' con themes, published_count, clean_ready_not_published - topic_index.json: nuova sezione 'explorer_themes' Contratto: data-explorer/catalog/themes.json formalizzato come upstream artifact. Nessuna modifica a data-explorer. Test: 68/68 passati. Build locale verificato. * feat: aggiungi deploy status data-explorer a context - GitHubCollector.get_latest_workflow_run() nuova API generica - DataExplorerFetcher.fetch_deploy_status() via GitHub Actions API - workspace_triage.json: explorer.last_deploy con run_id, conclusion, date - session_bootstrap.md: riga deploy con icona ✅/❌ e data Operativo, non strutturale — se API fallisce, last_deploy = null * fix: filtra workflow per nome (deploy.yml) in get_latest_workflow_run Aggiunto parametro workflow_id opzionale — quando fornito usa l'endpoint /actions/workflows/{workflow_id}/runs invece del generico /actions/runs, evitando ambiguità con altri workflow che condividono lo stesso evento (push). DataExplorerFetcher ora passa workflow_id='deploy.yml'.
1 parent 301aa3c commit b563e89

6 files changed

Lines changed: 299 additions & 3 deletions

File tree

src/agent_context_builder/github.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from typing import Optional
6+
from typing import Any, Optional
77

88
from lab_connectors.http import HttpClient
99

@@ -187,6 +187,63 @@ def get_repos_info(self, repos: list[str]) -> dict[str, RepoInfo]:
187187
self.fetch_errors[f"{repo}:info"] = str(exc)
188188
return result_map
189189

190+
def get_latest_workflow_run(
191+
self,
192+
repo: str,
193+
event: str = "push",
194+
status: str = "completed",
195+
workflow_id: str | None = None,
196+
) -> dict[str, Any] | None:
197+
"""Fetch the latest workflow run for a repo.
198+
199+
If workflow_id is provided, uses the workflow-specific endpoint
200+
(e.g. ``deploy.yml``) to avoid ambiguity when multiple workflows
201+
trigger on the same event.
202+
203+
Args:
204+
repo: Repository name (under self.org)
205+
event: Event type filter (e.g. push, workflow_dispatch)
206+
status: Status filter (e.g. completed, success)
207+
workflow_id: Workflow filename (e.g. ``deploy.yml``) for
208+
precise targeting. Optional.
209+
210+
Returns:
211+
Dict with run_id, name, status, conclusion, started_at, completed_at, html_url,
212+
or None if no runs found or on error.
213+
"""
214+
if workflow_id:
215+
url = (
216+
f"{self.base_url}/repos/{self.org}/{repo}"
217+
f"/actions/workflows/{workflow_id}/runs"
218+
)
219+
else:
220+
url = f"{self.base_url}/repos/{self.org}/{repo}/actions/runs"
221+
params = {
222+
"event": event,
223+
"status": status,
224+
"per_page": 1,
225+
}
226+
try:
227+
result = self._http.get(url, params=params, headers=self._headers())
228+
self._raise_on_bad_status(result, url)
229+
data = result.response.json() # type: ignore[union-attr]
230+
runs = data.get("workflow_runs", [])
231+
if not runs:
232+
return None
233+
run = runs[0]
234+
return {
235+
"run_id": run.get("id"),
236+
"name": run.get("name", ""),
237+
"status": run.get("status", ""),
238+
"conclusion": run.get("conclusion"),
239+
"started_at": run.get("run_started_at", ""),
240+
"completed_at": run.get("updated_at", ""),
241+
"html_url": run.get("html_url", ""),
242+
}
243+
except Exception as exc:
244+
self.fetch_errors[f"{repo}:workflow_runs"] = str(exc)
245+
return None
246+
190247
def _get_repo_issues(self, repo: str, state: str = "open") -> list[Issue]:
191248
"""Get issues for a specific repo (excluding pull requests).
192249

src/agent_context_builder/render.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
from .discussions import DiscussionCollector
1010
from .github import GitHubCollector, PR
1111
from .git_local import GitLocalCollector, GitState
12+
from .sources.de import DataExplorerFetcher
1213
from .sources.di import DatasetIncubatorFetcher
1314
from .sources.so import SourceObservatoryFetcher
1415
from .signals import (
16+
ExplorerTheme,
1517
RadarSummary,
1618
SourceObservatorySignals,
1719
)
@@ -44,6 +46,7 @@ def __init__(
4446
self.fixed_timestamp = fixed_timestamp or datetime.now().isoformat()
4547
self._so_fetcher = SourceObservatoryFetcher(self.github_collector)
4648
self._di_fetcher = DatasetIncubatorFetcher(self.github_collector)
49+
self._de_fetcher = DataExplorerFetcher(self.github_collector)
4750

4851
def render_session_bootstrap(self) -> str:
4952
"""Render session_bootstrap.md.
@@ -137,6 +140,51 @@ def render_session_bootstrap(self) -> str:
137140
lines.append("**Dataset Catalog**: unavailable")
138141
lines.append("")
139142

143+
# ── EXPLORER ──────────────────────────────────────────────────────
144+
explorer_themes = self._fetch_explorer_themes()
145+
if explorer_themes is not None:
146+
lines.append("## 🗂 EXPLORER")
147+
lines.append("")
148+
149+
# Count themed datasets
150+
themed_slugs: set[str] = set()
151+
for t in explorer_themes:
152+
themed_slugs.update(t.datasets)
153+
154+
# Gap analysis
155+
catalog = self._fetch_di_clean_catalog()
156+
clean_ready_slugs: set[str] = set()
157+
if catalog is not None:
158+
for ds in catalog.clean_ready:
159+
clean_ready_slugs.add(ds.slug)
160+
gap = sorted(clean_ready_slugs - themed_slugs)
161+
162+
lines.append(
163+
f"**Pubblicati**: {len(themed_slugs)} dataset · "
164+
f"{len(explorer_themes)} temi"
165+
)
166+
for t in explorer_themes:
167+
lines.append(f" · **{t.name}**: {len(t.datasets)} dataset")
168+
if gap:
169+
lines.append(f" ⚠ {len(gap)} dataset clean_ready non ancora su explorer:")
170+
for slug in gap[:5]:
171+
lines.append(f" · {slug}")
172+
if len(gap) > 5:
173+
lines.append(f" · ... e altri {len(gap) - 5}")
174+
175+
# Deploy status
176+
last_deploy = self._de_fetcher.fetch_deploy_status()
177+
if last_deploy is not None:
178+
conclusion = last_deploy.get("conclusion", "unknown")
179+
icon = "✅" if conclusion == "success" else "❌"
180+
completed = (last_deploy.get("completed_at", "")[:10]
181+
if last_deploy.get("completed_at") else "?")
182+
lines.append(f" **Deploy**: {icon} {conclusion} ({completed})")
183+
else:
184+
lines.append(f" **Deploy**: dati non disponibili")
185+
186+
lines.append("")
187+
140188
# ── OPEN ─────────────────────────────────────────────────────────
141189
prs = self.github_collector.get_prs(self.config.repos)
142190
github_errors = self.github_collector.fetch_errors
@@ -225,10 +273,14 @@ def render_workspace_triage(self) -> dict[str, Any]:
225273
self.fixed_timestamp,
226274
so_fetcher=self._so_fetcher,
227275
di_fetcher=self._di_fetcher,
276+
de_fetcher=self._de_fetcher,
228277
)
229278

230279

231280

281+
def _fetch_explorer_themes(self) -> list[ExplorerTheme] | None:
282+
return self._de_fetcher.fetch_themes()
283+
232284
def _fetch_di_pipeline_signals(self) -> RepoSignals | None:
233285
return self._di_fetcher.fetch_pipeline_signals()
234286

@@ -329,11 +381,25 @@ def render_topic_index(self) -> dict[str, Any]:
329381
"next": topic.next,
330382
}
331383

384+
# Explorer themes from data-explorer
385+
explorer_themes_list: list[dict[str, Any]] = []
386+
explorer_themes = self._fetch_explorer_themes()
387+
if explorer_themes is not None:
388+
explorer_themes_list = [
389+
{
390+
"slug": t.slug,
391+
"name": t.name,
392+
"datasets": t.datasets,
393+
}
394+
for t in explorer_themes
395+
]
396+
332397
return {
333398
"schema_version": 2,
334399
"generated_at": self.fixed_timestamp,
335400
"repos": repos_section,
336401
"datasets_by_source": datasets_by_source,
337402
"candidates_by_source": candidates_by_source,
338403
"operational_topics": operational_topics,
404+
"explorer_themes": explorer_themes_list,
339405
}

src/agent_context_builder/signals.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,45 @@ def unhealthy(self) -> list[RadarSource]:
320320
return [s for s in self.sources if s.status in ("YELLOW", "RED")]
321321

322322

323+
@dataclass
324+
class ExplorerTheme:
325+
"""Single theme entry from data-explorer catalog/themes.json."""
326+
327+
slug: str
328+
name: str
329+
datasets: list[str]
330+
331+
332+
def parse_explorer_themes(raw: str) -> list[ExplorerTheme]:
333+
"""Parse data-explorer catalog/themes.json into ExplorerTheme instances.
334+
335+
Args:
336+
raw: Raw JSON content of themes.json
337+
338+
Returns:
339+
List of ExplorerTheme instances
340+
341+
Raises:
342+
ValueError: If the JSON is invalid
343+
"""
344+
try:
345+
data: list[dict[str, Any]] = json.loads(raw)
346+
except json.JSONDecodeError as exc:
347+
raise ValueError(f"Invalid JSON: {exc}") from exc
348+
349+
if not isinstance(data, list):
350+
raise ValueError("Expected JSON array at root")
351+
352+
return [
353+
ExplorerTheme(
354+
slug=item.get("slug", ""),
355+
name=item.get("name", ""),
356+
datasets=item.get("datasets", []),
357+
)
358+
for item in data
359+
]
360+
361+
323362
def parse_radar_summary(raw: str) -> RadarSummary:
324363
try:
325364
data: dict[str, Any] = json.loads(raw)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Data-explorer fetch and parse helpers."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import Any
7+
8+
from ..github import GitHubCollector
9+
from ..signals import ExplorerTheme, parse_explorer_themes
10+
11+
12+
@dataclass
13+
class DataExplorerData:
14+
"""Cached data-explorer artifact bundle."""
15+
16+
themes: list[ExplorerTheme] | None
17+
last_deploy: dict[str, Any] | None
18+
19+
20+
class DataExplorerFetcher:
21+
"""Fetch data-explorer artifacts from GitHub raw URLs + API.
22+
23+
Consumes:
24+
- catalog/themes.json (editorial theme assignments)
25+
- GitHub Actions API (deploy status, operativo)
26+
"""
27+
28+
def __init__(self, collector: GitHubCollector):
29+
self.collector = collector
30+
self._themes_cache: list[ExplorerTheme] | None | object = _UNSET
31+
self._deploy_cache: dict[str, Any] | None | object = _UNSET
32+
33+
def fetch(self) -> DataExplorerData:
34+
"""Fetch all data-explorer artifacts."""
35+
return DataExplorerData(
36+
themes=self.fetch_themes(),
37+
last_deploy=self.fetch_deploy_status(),
38+
)
39+
40+
def fetch_themes(self) -> list[ExplorerTheme] | None:
41+
"""Fetch and parse catalog/themes.json from data-explorer.
42+
43+
Returns None if the artifact is unavailable or malformed.
44+
"""
45+
if self._themes_cache is not _UNSET:
46+
return self._themes_cache # type: ignore[return-value]
47+
raw = self.collector.get_raw_file("data-explorer", "catalog/themes.json")
48+
if raw is None:
49+
self._themes_cache = None
50+
return None
51+
try:
52+
result = parse_explorer_themes(raw)
53+
except ValueError as exc:
54+
self.collector.fetch_errors["data-explorer:themes"] = str(exc)
55+
result = None
56+
self._themes_cache = result
57+
return result
58+
59+
def fetch_deploy_status(self) -> dict[str, Any] | None:
60+
"""Fetch latest deploy workflow run for data-explorer.
61+
62+
Returns a dict with run_id, name, status, conclusion, started_at,
63+
completed_at, html_url — or None if unavailable.
64+
"""
65+
if self._deploy_cache is not _UNSET:
66+
return self._deploy_cache # type: ignore[return-value]
67+
result = self.collector.get_latest_workflow_run("data-explorer", workflow_id="deploy.yml")
68+
self._deploy_cache = result
69+
return result
70+
71+
72+
class _Unset:
73+
pass
74+
75+
76+
_UNSET = _Unset()

0 commit comments

Comments
 (0)