Skip to content

Commit d34d58e

Browse files
committed
merge: dev1 → master — case-studies: funnel trovate→tenute/escluse + donut motivi esclusione + intraday Codex + budget per-fase (beta-2)
2 parents b6d54ac + 4fe1b64 commit d34d58e

11 files changed

Lines changed: 2621 additions & 93 deletions

File tree

tools/case-study-extract/build_case_study_json.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,18 @@ def norm(s):
181181
agents = sorted({e["agent"] for e in events})
182182
ts_min, ts_max = Q("SELECT MIN(ts),MAX(ts) FROM position_state_transitions")[0]
183183

184-
# ── usage giornaliero (% del budget SETTIMANALE AI consumato per giorno) ──
185-
# Da sentinel-data.jsonl: weekly_usage è cumulativo dentro la settimana (cresce
186-
# 0→~100) e si azzera al reset (Gio ~06:00 UTC). Consumo del giorno = valore di
187-
# FINE giornata − fine del giorno precedente nella STESSA settimana (0 se è il
188-
# primo giorno della settimana). Robusto all'oscillazione intraday; ogni
189-
# settimana completa somma ~100%.
190-
def _week_key(day_iso): # giovedì di riferimento (settimana di budget)
191-
d = datetime.date.fromisoformat(day_iso)
192-
return (d - datetime.timedelta(days=(d.weekday() - 3) % 7)).isoformat()
184+
# ── usage giornaliero (% del budget AI consumato per giorno) ──
185+
# Da sentinel-data.jsonl: weekly_usage è cumulativo dentro il ciclo di budget
186+
# (cresce 0→~100) e si azzera al reset. Il reset NON è solo il giovedì di
187+
# calendario: l'utente può prendere un nuovo abbonamento e ripartire da 0 senza
188+
# aspettare la settimana. Quindi i CICLI si rilevano dai dati: nuovo ciclo quando
189+
# il cum SCENDE (reset, naturale o nuovo abbonamento) o c'è un buco di giorni.
190+
# Consumo del giorno = fine giornata − fine del giorno prima NELLO STESSO ciclo
191+
# (= il valore stesso se è il primo giorno del ciclo). `week` = data inizio ciclo.
193192

194193
SENTINEL = "/root/.jht/logs/sentinel-data.jsonl"
195194
usage = None
195+
hour_cum = {} # "YYYY-MM-DDTHH" -> budget cumulato % (ultimo sample dell'ora)
196196
if os.path.exists(SENTINEL):
197197
samples = [] # (ts, weekly_usage)
198198
provider = "codex"
@@ -211,18 +211,29 @@ def _week_key(day_iso): # giovedì di riferimento (settimana di budget)
211211
if e.get("provider"):
212212
provider = e["provider"]
213213
samples.sort()
214+
for ts, wu in samples: # budget cum per ORA (ultimo sample dell'ora vince)
215+
hour_cum[ts[:13]] = wu
214216
day_last = collections.OrderedDict() # day -> weekly_usage di fine giornata
215217
for ts, wu in samples:
216218
day_last[ts[:10]] = wu # l'ultimo sample del giorno sovrascrive
217219
daily = []
218-
week_prev = {} # week_key -> fine del giorno precedente (nella settimana)
220+
prev_day = None
221+
prev_end = None
222+
cycle = None # data di inizio del ciclo di budget corrente
219223
for day, end in day_last.items():
220-
wk = _week_key(day)
221-
pct = end - week_prev.get(wk, 0)
222-
week_prev[wk] = end
223-
# pct = consumo del giorno; cum = totale settimanale a fine giornata
224+
gap = (prev_day is not None and
225+
(datetime.date.fromisoformat(day)
226+
- datetime.date.fromisoformat(prev_day)).days > 1)
227+
reset = prev_end is not None and end < prev_end - 1 # cum sceso = reset
228+
if cycle is None or reset or gap:
229+
cycle, base = day, 0.0 # nuovo ciclo: riparte da 0
230+
else:
231+
base = prev_end
232+
pct = end - base
233+
# pct = consumo del giorno; cum = totale del ciclo a fine giornata
224234
daily.append({"day": day, "pct": round(max(pct, 0), 1),
225-
"cum": round(end, 1), "week": wk})
235+
"cum": round(end, 1), "week": cycle})
236+
prev_day, prev_end = day, end
226237
# orario di lavoro configurato (contesto: su quali ore/giorni si spalma)
227238
working_hours = None
228239
CONFIG = "/root/.jht/jht.config.json"
@@ -243,6 +254,53 @@ def _week_key(day_iso): # giovedì di riferimento (settimana di budget)
243254
usage = {"provider": provider, "unit": "weekly_budget_pct",
244255
"daily": daily, "workingHours": working_hours}
245256

257+
# ── attività + budget per ORA (per viste intraday su fasi corte, es. burst free-run) ──
258+
# Solo le ore con almeno una transizione (compatto). Ogni bucket:
259+
# {hour:"YYYY-MM-DDTHH", counts:{ruolo:n}, cum: budget% a quell'ora}. Il cum è
260+
# riportato in avanti dall'ultimo sample noto, così le ore senza sample budget
261+
# non azzerano la linea.
262+
hour_acts = collections.OrderedDict() # hour -> {role: n}
263+
for e in events:
264+
hr = e["ts"][:13] # "YYYY-MM-DDTHH"
265+
role = e["agent"].split("-")[0]
266+
b = hour_acts.setdefault(hr, {})
267+
b[role] = b.get(role, 0) + 1
268+
hourly = []
269+
_last_cum = 0.0
270+
for hr in sorted(hour_acts):
271+
if hr in hour_cum:
272+
_last_cum = hour_cum[hr]
273+
hourly.append({"hour": hr, "counts": hour_acts[hr], "cum": round(_last_cum, 1)})
274+
275+
# ── funnel TROVATE → ESCLUSE / TENUTE (per giorno + totali) ──────────
276+
# Per ogni giorno di found_at: quante posizioni trovate e come sono finite
277+
# (status attuale). "tenute" = non escluse. Serve a mostrare la proporzione
278+
# escluse vs tenute giorno per giorno (e in totale, per il donut).
279+
fd_rows = Q(
280+
"SELECT substr(found_at,1,10) d, status, COUNT(*) "
281+
"FROM positions WHERE found_at IS NOT NULL AND TRIM(found_at)<>'' "
282+
"GROUP BY 1, 2")
283+
fd = collections.OrderedDict() # day -> {status: n}
284+
for d, st, n in fd_rows:
285+
fd.setdefault(d, {})[(st or "?")] = n
286+
funnel_daily = []
287+
for d in sorted(fd):
288+
sm = fd[d]
289+
excl = sm.get("excluded", 0)
290+
found = sum(sm.values())
291+
funnel_daily.append({
292+
"day": d, "found": found, "excluded": excl, "kept": found - excl,
293+
"scored": sm.get("scored", 0), "ready": sm.get("ready", 0),
294+
})
295+
status_tot = {(st or "?"): n for st, n in Q(
296+
"SELECT status, COUNT(*) FROM positions GROUP BY status")}
297+
_excl_tot = status_tot.get("excluded", 0)
298+
_found_tot = sum(status_tot.values())
299+
funnel_totals = {
300+
"found": _found_tot, "excluded": _excl_tot, "kept": _found_tot - _excl_tot,
301+
"scored": status_tot.get("scored", 0), "ready": status_tot.get("ready", 0),
302+
}
303+
246304
out = {
247305
"source": "andris-codex",
248306
"tsRange": [ts_min, ts_max],
@@ -259,6 +317,9 @@ def _week_key(day_iso): # giovedì di riferimento (settimana di budget)
259317
"salary": salary,
260318
"agents": agents,
261319
"events": events,
320+
"hourly": hourly,
321+
"funnelDaily": funnel_daily,
322+
"funnelTotals": funnel_totals,
262323
"usage": usage,
263324
}
264325
print(json.dumps(out, ensure_ascii=False, indent=2))

0 commit comments

Comments
 (0)