# -*- coding: utf-8 -*- """Étape 4 — 90 jours de fréquentation, puis ré-indexation de l'assistant. Le tableau de bord ne vaut comme argument que s'il montre une courbe crédible : pics de week-end, saisonnalité douce, répartition par canal et par langue. """ import json import random from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta, timezone from pathlib import Path from api import Api STATE = Path(__file__).resolve().parent / "state.json" DAYS = 90 SESSIONS_PER_DAY = 22 # moyenne en semaine WEEKEND_FACTOR = 2.3 CHANNELS = [("Web", 0.45), ("Mobile", 0.35), ("Tablet", 0.20)] LANGUAGES = [("FR", 0.45), ("DE", 0.25), ("EN", 0.20), ("NL", 0.10)] random.seed(20260825) # même jeu de données à chaque exécution def weighted(pairs): r = random.random() cumulative = 0.0 for value, weight in pairs: cumulative += weight if r <= cumulative: return value return pairs[-1][0] def build_events(state, catalogue): """Produit la liste des événements à envoyer, sans appel réseau.""" today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) events = [] for day_back in range(DAYS, 0, -1): day = today - timedelta(days=day_back) weekend = day.weekday() >= 5 base = SESSIONS_PER_DAY * (WEEKEND_FACTOR if weekend else 1.0) # Légère montée en charge sur la période, plus un aléa quotidien. trend = 0.75 + 0.5 * (DAYS - day_back) / DAYS sessions = max(1, int(random.gauss(base * trend, base * 0.25))) for _ in range(sessions): channel = weighted(CHANNELS) language = weighted(LANGUAGES) session_id = f"s{day.strftime('%y%m%d')}-{random.randrange(10**6):06d}" site = weighted([("feschmaart", 0.55), ("draieechelen", 0.28), ("echternach", 0.17)]) config_id = state["configs"][site] sections = catalogue[site] if not sections: continue start = day + timedelta(hours=random.randint(10, 17), minutes=random.randint(0, 59)) visited = random.sample(sections, min(len(sections), random.randint(2, 6))) cursor = start def add(event_type, section=None, duration=None, metadata=None): events.append({ "instanceId": state["instanceId"], "configurationId": config_id, "sectionId": section["id"] if section else None, "sessionId": session_id, "eventType": event_type, "appType": channel, "language": language, "durationSeconds": duration, "metadata": json.dumps(metadata) if metadata else None, "timestamp": cursor.strftime("%Y-%m-%dT%H:%M:%SZ"), }) # Un visiteur sur cinq arrive par un code QR posé sur un cartel. if random.random() < 0.20: add("QrScan", visited[0], metadata={"valid": random.random() > 0.06}) for section in visited: duration = int(random.triangular(25, 300, 90)) add("SectionView", section, duration) cursor += timedelta(seconds=duration) # La durée moyenne de visite se calcule en sommant les SectionLeave # par session : sans eux, le tableau de bord affiche zéro minute. add("SectionLeave", section, duration) cursor += timedelta(seconds=random.randint(5, 40)) if section["type"] == "Article" and random.random() < 0.55: add("ArticleRead", section, duration) if section["type"] == "Menu": add("MenuItemTap", section) if section["type"] == "Map" and random.random() < 0.7: poi = random.choice(state["poi"]) add("MapPoiTap", section, metadata={"geoPointId": poi["id"], "geoPointTitle": poi["title"]}) if section["type"] == "Quiz" and random.random() < 0.45: total = 3 add("QuizComplete", section, metadata={"score": float(random.choice([1, 2, 2, 3, 3, 3])), "totalQuestions": total}) if section["type"] == "Agenda" and random.random() < 0.5 and state["agenda"]: ev = random.choice(state["agenda"]) add("AgendaEventTap", section, metadata={"eventId": str(ev["id"]), "eventTitle": ev["title"]}) if section["type"] == "Parcours" and random.random() < 0.3: add("GameComplete", section, int(random.triangular(900, 3600, 1800))) # L'assistant est sollicité sur une visite sur six, deux questions en moyenne. if random.random() < 0.17: for _ in range(random.randint(1, 3)): cursor += timedelta(seconds=random.randint(20, 120)) add("AssistantMessage", random.choice(visited)) return events def main(): api = Api() state = json.loads(STATE.read_text(encoding="utf-8")) catalogue = {} for key, config_id in state["configs"].items(): sections = api.get(f"/api/Section/configuration/{config_id}") or [] catalogue[key] = [{"id": s["id"], "type": s["type"]} for s in sections] print(f" {key}: {len(sections)} sections") points = api.get(f"/api/SectionMap/{state['mapEchternach']}/points") or [] state["poi"] = [{"id": p["id"], "title": next((t["value"] for t in p["title"] if t["language"] == "FR"), "")} for p in points] agenda_section = next((s for s in catalogue["feschmaart"] if s["type"] == "Agenda"), None) state["agenda"] = [] if agenda_section: for ev in api.get(f"/api/SectionAgenda/{agenda_section['id']}/events") or []: title = next((t["value"] for t in ev["label"] if t["language"] == "FR"), "") state["agenda"].append({"id": ev["id"], "title": title}) events = build_events(state, catalogue) print(f"\n{len(events)} événements à envoyer…") def send(event): api.post("/api/Stats/event", event) with ThreadPoolExecutor(max_workers=12) as pool: for i, _ in enumerate(pool.map(send, events), 1): if i % 2000 == 0: print(f" {i}/{len(events)}") print("\nré-indexation de l'assistant…") api.post(f"/api/Ai/reindex/{state['instanceId']}") print("étape 4 terminée") if __name__ == "__main__": main()