- outputs/platform, outputs/prospect : scripts de construction des decks commerciaux + captures sources et .pptx generes. - outputs/mnaha : seed complet de l'instance de demo luxembourgeoise (scripts par etape, contenus, images generees), le deck en version marque blanche et en version brandee, le PDF de presentation. - outputs/Wireframes : maquettes app mobile, app web et manager. - interview clients : notes de l'entretien Fourneau St Michel. state.json et les __pycache__ sont ignores : le premier porte les cles d'API de l'instance seedee, les scripts le regenerent.
166 lines
6.8 KiB
Python
166 lines
6.8 KiB
Python
# -*- 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()
|