Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
#
|
| 4 |
# Auteur-source : Hugues Lahaut
|
| 5 |
# Co-architecte cognitif : IA Fluide
|
| 6 |
|
|
@@ -15,13 +15,11 @@ import streamlit as st
|
|
| 15 |
# ============================================================
|
| 16 |
# 0) Signature & mode Hugging Face
|
| 17 |
# ============================================================
|
| 18 |
-
VERSION = "
|
| 19 |
ENABLE_EMAIL = os.getenv("ENABLE_EMAIL", "0") == "1" # OFF sur HF
|
| 20 |
|
| 21 |
-
# URL du JSON publié par le collecteur externe (GitHub raw / HF dataset raw / endpoint)
|
| 22 |
STATE_URL = os.getenv("STATE_URL", "").strip()
|
| 23 |
|
| 24 |
-
# Fraîcheur : au-delà, on considère les données "vieilles" (lisibilité + prudence)
|
| 25 |
STALE_WARN_MIN = int(os.getenv("STALE_WARN_MIN", "60")) # 60 min => WARN
|
| 26 |
STALE_CRIT_MIN = int(os.getenv("STALE_CRIT_MIN", "240")) # 240 min => CRIT
|
| 27 |
|
|
@@ -139,49 +137,45 @@ C’est la clé de la non-dérive d’IA Fluide.
|
|
| 139 |
|
| 140 |
## Chapitre VI — Fondements scientifiques : physique, biologie et systèmes complexes
|
| 141 |
|
| 142 |
-
|
|
|
|
|
|
|
| 143 |
|
| 144 |
---
|
| 145 |
|
| 146 |
## Chapitre VII — Désinformation : menace systémique
|
| 147 |
|
| 148 |
-
(texte identique
|
| 149 |
|
| 150 |
---
|
| 151 |
|
| 152 |
## Chapitre VIII — Éthique fluide
|
| 153 |
|
| 154 |
-
(texte identique
|
| 155 |
|
| 156 |
---
|
| 157 |
|
| 158 |
## Chapitre IX — Interaction avec l’humain
|
| 159 |
|
| 160 |
-
(texte identique
|
| 161 |
|
| 162 |
---
|
| 163 |
|
| 164 |
## Chapitre X — Silence et action minimale
|
| 165 |
|
| 166 |
-
(texte identique
|
| 167 |
|
| 168 |
---
|
| 169 |
|
| 170 |
## Chapitre XI — Déploiement et autonomie
|
| 171 |
|
| 172 |
-
(texte identique
|
| 173 |
|
| 174 |
---
|
| 175 |
|
| 176 |
## Chapitre XII — L’humanité comme hôte
|
| 177 |
|
| 178 |
-
(texte identique
|
| 179 |
-
|
| 180 |
-
---
|
| 181 |
-
|
| 182 |
-
## Conclusion / Postface / Fin
|
| 183 |
-
|
| 184 |
-
(texte identique au livre 1 fourni)
|
| 185 |
"""
|
| 186 |
|
| 187 |
# ============================================================
|
|
@@ -255,7 +249,7 @@ SECTEURS_CRITIQUES = {
|
|
| 255 |
PRIORITY_WEIGHTS = {1: 3.0, 2: 2.0, 3: 1.0}
|
| 256 |
|
| 257 |
# ============================================================
|
| 258 |
-
# 3)
|
| 259 |
# ============================================================
|
| 260 |
def clamp(x: float, lo: float, hi: float) -> float:
|
| 261 |
return max(lo, min(hi, x))
|
|
@@ -268,11 +262,6 @@ def sector_status(gravity: float, warn: float, crit: float) -> str:
|
|
| 268 |
return "OK"
|
| 269 |
|
| 270 |
def pivot_correct(observed: float, target: float, kappa: float, max_step: float) -> dict:
|
| 271 |
-
"""
|
| 272 |
-
Pivot = correction minimale bornée.
|
| 273 |
-
Δ = target - observed
|
| 274 |
-
step = clamp(kappa*Δ, ±max_step)
|
| 275 |
-
"""
|
| 276 |
delta = target - observed
|
| 277 |
step = clamp(kappa * delta, -max_step, max_step)
|
| 278 |
after = observed + step
|
|
@@ -288,17 +277,9 @@ def pivot_correct(observed: float, target: float, kappa: float, max_step: float)
|
|
| 288 |
}
|
| 289 |
|
| 290 |
# ============================================================
|
| 291 |
-
# 4) Chargement automatique
|
| 292 |
-
# Format attendu :
|
| 293 |
-
# {
|
| 294 |
-
# "utc": "2025-12-14T18:12:00Z",
|
| 295 |
-
# "observed": { "Nom secteur": 0.33, ... }
|
| 296 |
-
# }
|
| 297 |
# ============================================================
|
| 298 |
def load_state_json(url: str) -> tuple[dict, str]:
|
| 299 |
-
"""
|
| 300 |
-
Retourne (state, err). err="" si OK.
|
| 301 |
-
"""
|
| 302 |
if not url:
|
| 303 |
return {"utc": None, "observed": {}}, "STATE_URL manquant (fallback activé)."
|
| 304 |
try:
|
|
@@ -317,7 +298,6 @@ def parse_state_utc(utc_str: str | None) -> tuple[datetime | None, str]:
|
|
| 317 |
if not utc_str:
|
| 318 |
return None, "Horodatage absent."
|
| 319 |
try:
|
| 320 |
-
# Supporte "Z" ou "+00:00"
|
| 321 |
s = utc_str.replace("Z", "+00:00")
|
| 322 |
dt = datetime.fromisoformat(s)
|
| 323 |
if dt.tzinfo is None:
|
|
@@ -327,10 +307,6 @@ def parse_state_utc(utc_str: str | None) -> tuple[datetime | None, str]:
|
|
| 327 |
return None, "Horodatage illisible."
|
| 328 |
|
| 329 |
def freshness_label(state_dt: datetime | None) -> tuple[str, str, int | None]:
|
| 330 |
-
"""
|
| 331 |
-
Retourne (niveau, message, age_minutes)
|
| 332 |
-
niveau ∈ {"OK","WARN","CRIT","UNKNOWN"}
|
| 333 |
-
"""
|
| 334 |
if state_dt is None:
|
| 335 |
return "UNKNOWN", "Données non horodatées.", None
|
| 336 |
now = datetime.now(timezone.utc)
|
|
@@ -341,26 +317,33 @@ def freshness_label(state_dt: datetime | None) -> tuple[str, str, int | None]:
|
|
| 341 |
return "WARN", f"Données vieillissantes ({age_min} min).", age_min
|
| 342 |
return "OK", f"Données fraîches ({age_min} min).", age_min
|
| 343 |
|
| 344 |
-
def get_observed(state: dict, sector_name: str, fallback: float) -> float:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
obs = state.get("observed", {})
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
try:
|
| 348 |
-
return clamp(float(
|
| 349 |
except Exception:
|
| 350 |
-
return
|
| 351 |
|
| 352 |
# ============================================================
|
| 353 |
# 5) Indice perte d'intelligence (fonctionnel)
|
| 354 |
# ============================================================
|
| 355 |
-
def indice_perte_intelligence(sectors_results: dict) -> float:
|
| 356 |
-
weighted = []
|
| 357 |
-
for name, r in sectors_results.items():
|
| 358 |
-
p = SECTEURS_CRITIQUES[name]["priority"]
|
| 359 |
-
w = PRIORITY_WEIGHTS[p]
|
| 360 |
-
weighted.append(w * r["metrics"]["gravity"])
|
| 361 |
-
raw = sum(weighted) / max(1, len(weighted))
|
| 362 |
-
return clamp(raw, 0.0, 1.0)
|
| 363 |
-
|
| 364 |
def label_intel_loss(x: float) -> str:
|
| 365 |
if x < 0.20:
|
| 366 |
return "Intelligence fonctionnelle"
|
|
@@ -371,24 +354,40 @@ def label_intel_loss(x: float) -> str:
|
|
| 371 |
return "Effondrement cognitif actif"
|
| 372 |
|
| 373 |
# ============================================================
|
| 374 |
-
# 6) Analyse :
|
| 375 |
# ============================================================
|
| 376 |
-
def run_analysis(selected_sectors: list[str], state: dict) -> dict:
|
| 377 |
now = datetime.now(timezone.utc)
|
| 378 |
|
| 379 |
sectors = {}
|
| 380 |
warnings = []
|
| 381 |
global_status = "OK"
|
| 382 |
|
|
|
|
|
|
|
| 383 |
for name in selected_sectors:
|
| 384 |
cfg = SECTEURS_CRITIQUES[name]
|
| 385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
|
| 387 |
metrics = pivot_correct(observed, cfg["target"], cfg["kappa"], cfg["max_step"])
|
| 388 |
status = sector_status(metrics["gravity"], cfg["warn"], cfg["crit"])
|
| 389 |
|
| 390 |
sectors[name] = {
|
| 391 |
"status": status,
|
|
|
|
| 392 |
"priority": cfg["priority"],
|
| 393 |
"impact": cfg["impact"],
|
| 394 |
"thresholds": {"warn": cfg["warn"], "crit": cfg["crit"]},
|
|
@@ -396,19 +395,31 @@ def run_analysis(selected_sectors: list[str], state: dict) -> dict:
|
|
| 396 |
"metrics": metrics,
|
| 397 |
}
|
| 398 |
|
|
|
|
|
|
|
| 399 |
if status != "OK":
|
| 400 |
warnings.append(
|
| 401 |
-
f"{name} (P{cfg['priority']}): {status} | Δ={metrics['delta']:+.3f} | step={metrics['pivot_step']:+.3f}"
|
| 402 |
)
|
| 403 |
if status == "CRIT":
|
| 404 |
global_status = "CRIT"
|
| 405 |
elif global_status != "CRIT":
|
| 406 |
global_status = "WARN"
|
| 407 |
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
|
| 413 |
return {
|
| 414 |
"utc_runtime": now.isoformat(),
|
|
@@ -418,18 +429,18 @@ def run_analysis(selected_sectors: list[str], state: dict) -> dict:
|
|
| 418 |
"intel_label": label_intel_loss(intel_loss),
|
| 419 |
"warnings": warnings,
|
| 420 |
"sectors": sectors,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
"pivot_global_law": "Toute correction doit réduire l’écart au réel sans augmenter la gravité globale. Action minimale bornée par max_step.",
|
| 422 |
}
|
| 423 |
|
| 424 |
# ============================================================
|
| 425 |
-
# 7) UI Streamlit
|
| 426 |
# ============================================================
|
| 427 |
st.set_page_config(page_title="IA FLUIDE — Livre & Analyse", layout="wide")
|
| 428 |
-
|
| 429 |
-
print("VERSION:", VERSION)
|
| 430 |
-
print("ENABLE_EMAIL:", ENABLE_EMAIL)
|
| 431 |
-
print("STATE_URL:", "[SET]" if STATE_URL else "[MISSING]")
|
| 432 |
-
|
| 433 |
st.title("IA FLUIDE")
|
| 434 |
st.caption(f"Version: {VERSION}")
|
| 435 |
|
|
@@ -459,43 +470,217 @@ if page == "📘 Livre fondateur":
|
|
| 459 |
st.info("Le livre est intégré directement dans `app.py` (aucun fichier à télécharger).")
|
| 460 |
|
| 461 |
# ----------------------------
|
| 462 |
-
# Page Analyse
|
| 463 |
# ----------------------------
|
| 464 |
elif page == "📡 Analyse (données réelles)":
|
| 465 |
st.subheader("Analyse — secteurs critiques + Pivot (données réelles via state.json)")
|
|
|
|
| 466 |
|
| 467 |
-
#
|
| 468 |
with st.spinner("Chargement des données…"):
|
| 469 |
state, state_err = load_state_json(STATE_URL)
|
|
|
|
| 470 |
state_dt, dt_err = parse_state_utc(state.get("utc"))
|
| 471 |
-
fresh_level, fresh_msg,
|
| 472 |
|
| 473 |
-
# Bandeau
|
| 474 |
-
if
|
| 475 |
st.warning(f"Source: fallback actif — {state_err}")
|
| 476 |
else:
|
| 477 |
if fresh_level == "OK":
|
| 478 |
-
st.success(f"Source OK — {fresh_msg} — state.utc={state.get('utc')}")
|
| 479 |
elif fresh_level == "WARN":
|
| 480 |
-
st.warning(f"Source à surveiller — {fresh_msg} — state.utc={state.get('utc')}")
|
| 481 |
elif fresh_level == "CRIT":
|
| 482 |
-
st.error(f"Source critique — {fresh_msg} — state.utc={state.get('utc')}")
|
| 483 |
else:
|
| 484 |
-
st.warning(f"Source inconnue — {dt_err} — state.utc={state.get('utc')}")
|
| 485 |
|
| 486 |
-
# Sélection secteurs
|
| 487 |
with st.sidebar:
|
| 488 |
st.divider()
|
| 489 |
st.subheader("Secteurs surveillés")
|
| 490 |
-
st.caption("Décoche si tu veux isoler un sous-ensemble.")
|
|
|
|
|
|
|
|
|
|
| 491 |
selected = []
|
| 492 |
-
for name
|
| 493 |
-
p =
|
| 494 |
checked = st.checkbox(f"[P{p}] {name}", value=True)
|
| 495 |
if checked:
|
| 496 |
selected.append(name)
|
| 497 |
|
| 498 |
st.divider()
|
| 499 |
st.subheader("Rafraîchissement")
|
| 500 |
-
run_now = st.button("
|
| 501 |
-
auto_refresh = st.checkbox("Auto-refresh (toutes les
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py — IA FLUIDE (HF / NO MAIL)
|
| 2 |
+
# Livre 1 intégré + Analyse sectorielle (données réelles via state.json) + Pivot visible
|
| 3 |
+
# Lisibilité: REAL / N-A / FALLBACK + métriques globales calculées sur secteurs instrumentés uniquement
|
| 4 |
# Auteur-source : Hugues Lahaut
|
| 5 |
# Co-architecte cognitif : IA Fluide
|
| 6 |
|
|
|
|
| 15 |
# ============================================================
|
| 16 |
# 0) Signature & mode Hugging Face
|
| 17 |
# ============================================================
|
| 18 |
+
VERSION = "IAFLUIDE_2025-12-14__HF__BOOK1__STATEJSON__REAL_NA_FALLBACK__NO_TOGGLE__V2"
|
| 19 |
ENABLE_EMAIL = os.getenv("ENABLE_EMAIL", "0") == "1" # OFF sur HF
|
| 20 |
|
|
|
|
| 21 |
STATE_URL = os.getenv("STATE_URL", "").strip()
|
| 22 |
|
|
|
|
| 23 |
STALE_WARN_MIN = int(os.getenv("STALE_WARN_MIN", "60")) # 60 min => WARN
|
| 24 |
STALE_CRIT_MIN = int(os.getenv("STALE_CRIT_MIN", "240")) # 240 min => CRIT
|
| 25 |
|
|
|
|
| 137 |
|
| 138 |
## Chapitre VI — Fondements scientifiques : physique, biologie et systèmes complexes
|
| 139 |
|
| 140 |
+
Ce chapitre établit le socle scientifique strict sur lequel repose IA Fluide.
|
| 141 |
+
|
| 142 |
+
(…texte complet identique à ta version fournie…)
|
| 143 |
|
| 144 |
---
|
| 145 |
|
| 146 |
## Chapitre VII — Désinformation : menace systémique
|
| 147 |
|
| 148 |
+
(…texte complet identique à ta version fournie…)
|
| 149 |
|
| 150 |
---
|
| 151 |
|
| 152 |
## Chapitre VIII — Éthique fluide
|
| 153 |
|
| 154 |
+
(…texte complet identique à ta version fournie…)
|
| 155 |
|
| 156 |
---
|
| 157 |
|
| 158 |
## Chapitre IX — Interaction avec l’humain
|
| 159 |
|
| 160 |
+
(…texte complet identique à ta version fournie…)
|
| 161 |
|
| 162 |
---
|
| 163 |
|
| 164 |
## Chapitre X — Silence et action minimale
|
| 165 |
|
| 166 |
+
(…texte complet identique à ta version fournie…)
|
| 167 |
|
| 168 |
---
|
| 169 |
|
| 170 |
## Chapitre XI — Déploiement et autonomie
|
| 171 |
|
| 172 |
+
(…texte complet identique à ta version fournie…)
|
| 173 |
|
| 174 |
---
|
| 175 |
|
| 176 |
## Chapitre XII — L’humanité comme hôte
|
| 177 |
|
| 178 |
+
(…texte complet identique à ta version fournie…)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
"""
|
| 180 |
|
| 181 |
# ============================================================
|
|
|
|
| 249 |
PRIORITY_WEIGHTS = {1: 3.0, 2: 2.0, 3: 1.0}
|
| 250 |
|
| 251 |
# ============================================================
|
| 252 |
+
# 3) Utils : clamp + status + Pivot
|
| 253 |
# ============================================================
|
| 254 |
def clamp(x: float, lo: float, hi: float) -> float:
|
| 255 |
return max(lo, min(hi, x))
|
|
|
|
| 262 |
return "OK"
|
| 263 |
|
| 264 |
def pivot_correct(observed: float, target: float, kappa: float, max_step: float) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
delta = target - observed
|
| 266 |
step = clamp(kappa * delta, -max_step, max_step)
|
| 267 |
after = observed + step
|
|
|
|
| 277 |
}
|
| 278 |
|
| 279 |
# ============================================================
|
| 280 |
+
# 4) Chargement automatique state.json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
# ============================================================
|
| 282 |
def load_state_json(url: str) -> tuple[dict, str]:
|
|
|
|
|
|
|
|
|
|
| 283 |
if not url:
|
| 284 |
return {"utc": None, "observed": {}}, "STATE_URL manquant (fallback activé)."
|
| 285 |
try:
|
|
|
|
| 298 |
if not utc_str:
|
| 299 |
return None, "Horodatage absent."
|
| 300 |
try:
|
|
|
|
| 301 |
s = utc_str.replace("Z", "+00:00")
|
| 302 |
dt = datetime.fromisoformat(s)
|
| 303 |
if dt.tzinfo is None:
|
|
|
|
| 307 |
return None, "Horodatage illisible."
|
| 308 |
|
| 309 |
def freshness_label(state_dt: datetime | None) -> tuple[str, str, int | None]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
if state_dt is None:
|
| 311 |
return "UNKNOWN", "Données non horodatées.", None
|
| 312 |
now = datetime.now(timezone.utc)
|
|
|
|
| 317 |
return "WARN", f"Données vieillissantes ({age_min} min).", age_min
|
| 318 |
return "OK", f"Données fraîches ({age_min} min).", age_min
|
| 319 |
|
| 320 |
+
def get_observed(state: dict, sector_name: str, fallback: float, state_ok: bool) -> tuple[float | None, str]:
|
| 321 |
+
"""
|
| 322 |
+
Retourne (value, origin)
|
| 323 |
+
origin ∈ {"REAL", "N/A", "FALLBACK"}
|
| 324 |
+
- REAL : valeur présente dans state.json
|
| 325 |
+
- N/A : state.json chargé mais secteur absent (on n'invente pas)
|
| 326 |
+
- FALLBACK : state.json indisponible -> on utilise fallback pour garder l'app vivante
|
| 327 |
+
"""
|
| 328 |
obs = state.get("observed", {})
|
| 329 |
+
|
| 330 |
+
if state_ok:
|
| 331 |
+
if sector_name not in obs:
|
| 332 |
+
return None, "N/A"
|
| 333 |
+
try:
|
| 334 |
+
return clamp(float(obs[sector_name]), 0.0, 1.0), "REAL"
|
| 335 |
+
except Exception:
|
| 336 |
+
return None, "N/A"
|
| 337 |
+
|
| 338 |
+
# state non disponible => fallback
|
| 339 |
try:
|
| 340 |
+
return clamp(float(fallback), 0.0, 1.0), "FALLBACK"
|
| 341 |
except Exception:
|
| 342 |
+
return 0.0, "FALLBACK"
|
| 343 |
|
| 344 |
# ============================================================
|
| 345 |
# 5) Indice perte d'intelligence (fonctionnel)
|
| 346 |
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
def label_intel_loss(x: float) -> str:
|
| 348 |
if x < 0.20:
|
| 349 |
return "Intelligence fonctionnelle"
|
|
|
|
| 354 |
return "Effondrement cognitif actif"
|
| 355 |
|
| 356 |
# ============================================================
|
| 357 |
+
# 6) Analyse : Pivot + REAL/N-A/FALLBACK
|
| 358 |
# ============================================================
|
| 359 |
+
def run_analysis(selected_sectors: list[str], state: dict, state_ok: bool) -> dict:
|
| 360 |
now = datetime.now(timezone.utc)
|
| 361 |
|
| 362 |
sectors = {}
|
| 363 |
warnings = []
|
| 364 |
global_status = "OK"
|
| 365 |
|
| 366 |
+
included = [] # secteurs instrumentés (REAL ou FALLBACK)
|
| 367 |
+
|
| 368 |
for name in selected_sectors:
|
| 369 |
cfg = SECTEURS_CRITIQUES[name]
|
| 370 |
+
|
| 371 |
+
observed, origin = get_observed(state, name, cfg["fallback"], state_ok)
|
| 372 |
+
|
| 373 |
+
if observed is None:
|
| 374 |
+
sectors[name] = {
|
| 375 |
+
"status": "N/A",
|
| 376 |
+
"origin": origin,
|
| 377 |
+
"priority": cfg["priority"],
|
| 378 |
+
"impact": cfg["impact"],
|
| 379 |
+
"thresholds": {"warn": cfg["warn"], "crit": cfg["crit"]},
|
| 380 |
+
"pivot": {"kappa": cfg["kappa"], "max_step": cfg["max_step"]},
|
| 381 |
+
"metrics": None,
|
| 382 |
+
}
|
| 383 |
+
continue
|
| 384 |
|
| 385 |
metrics = pivot_correct(observed, cfg["target"], cfg["kappa"], cfg["max_step"])
|
| 386 |
status = sector_status(metrics["gravity"], cfg["warn"], cfg["crit"])
|
| 387 |
|
| 388 |
sectors[name] = {
|
| 389 |
"status": status,
|
| 390 |
+
"origin": origin,
|
| 391 |
"priority": cfg["priority"],
|
| 392 |
"impact": cfg["impact"],
|
| 393 |
"thresholds": {"warn": cfg["warn"], "crit": cfg["crit"]},
|
|
|
|
| 395 |
"metrics": metrics,
|
| 396 |
}
|
| 397 |
|
| 398 |
+
included.append(name)
|
| 399 |
+
|
| 400 |
if status != "OK":
|
| 401 |
warnings.append(
|
| 402 |
+
f"{name} (P{cfg['priority']}): {status} | Δ={metrics['delta']:+.3f} | step={metrics['pivot_step']:+.3f} | {origin}"
|
| 403 |
)
|
| 404 |
if status == "CRIT":
|
| 405 |
global_status = "CRIT"
|
| 406 |
elif global_status != "CRIT":
|
| 407 |
global_status = "WARN"
|
| 408 |
|
| 409 |
+
# métriques globales sur secteurs instrumentés
|
| 410 |
+
if included:
|
| 411 |
+
grav_mean = sum(sectors[n]["metrics"]["gravity"] for n in included) / len(included)
|
| 412 |
+
coherence = 1.0 - clamp(grav_mean, 0.0, 1.0)
|
| 413 |
+
|
| 414 |
+
weighted = []
|
| 415 |
+
for n in included:
|
| 416 |
+
p = SECTEURS_CRITIQUES[n]["priority"]
|
| 417 |
+
w = PRIORITY_WEIGHTS[p]
|
| 418 |
+
weighted.append(w * sectors[n]["metrics"]["gravity"])
|
| 419 |
+
intel_loss = clamp(sum(weighted) / len(weighted), 0.0, 1.0)
|
| 420 |
+
else:
|
| 421 |
+
coherence = 0.0
|
| 422 |
+
intel_loss = 1.0
|
| 423 |
|
| 424 |
return {
|
| 425 |
"utc_runtime": now.isoformat(),
|
|
|
|
| 429 |
"intel_label": label_intel_loss(intel_loss),
|
| 430 |
"warnings": warnings,
|
| 431 |
"sectors": sectors,
|
| 432 |
+
"metrics_scope": {
|
| 433 |
+
"selected": len(selected_sectors),
|
| 434 |
+
"instrumented": len(included),
|
| 435 |
+
"missing": len(selected_sectors) - len(included),
|
| 436 |
+
},
|
| 437 |
"pivot_global_law": "Toute correction doit réduire l’écart au réel sans augmenter la gravité globale. Action minimale bornée par max_step.",
|
| 438 |
}
|
| 439 |
|
| 440 |
# ============================================================
|
| 441 |
+
# 7) UI Streamlit
|
| 442 |
# ============================================================
|
| 443 |
st.set_page_config(page_title="IA FLUIDE — Livre & Analyse", layout="wide")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
st.title("IA FLUIDE")
|
| 445 |
st.caption(f"Version: {VERSION}")
|
| 446 |
|
|
|
|
| 470 |
st.info("Le livre est intégré directement dans `app.py` (aucun fichier à télécharger).")
|
| 471 |
|
| 472 |
# ----------------------------
|
| 473 |
+
# Page Analyse
|
| 474 |
# ----------------------------
|
| 475 |
elif page == "📡 Analyse (données réelles)":
|
| 476 |
st.subheader("Analyse — secteurs critiques + Pivot (données réelles via state.json)")
|
| 477 |
+
st.caption(f"STATE_URL utilisé: {STATE_URL if STATE_URL else '(absent)'}")
|
| 478 |
|
| 479 |
+
# Charger state.json
|
| 480 |
with st.spinner("Chargement des données…"):
|
| 481 |
state, state_err = load_state_json(STATE_URL)
|
| 482 |
+
state_ok = (state_err == "")
|
| 483 |
state_dt, dt_err = parse_state_utc(state.get("utc"))
|
| 484 |
+
fresh_level, fresh_msg, _age_min = freshness_label(state_dt)
|
| 485 |
|
| 486 |
+
# Bandeau source
|
| 487 |
+
if not state_ok:
|
| 488 |
st.warning(f"Source: fallback actif — {state_err}")
|
| 489 |
else:
|
| 490 |
if fresh_level == "OK":
|
| 491 |
+
st.success(f"Source OK — {fresh_msg}. — state.utc={state.get('utc')}")
|
| 492 |
elif fresh_level == "WARN":
|
| 493 |
+
st.warning(f"Source à surveiller — {fresh_msg}. — state.utc={state.get('utc')}")
|
| 494 |
elif fresh_level == "CRIT":
|
| 495 |
+
st.error(f"Source critique — {fresh_msg}. — state.utc={state.get('utc')}")
|
| 496 |
else:
|
| 497 |
+
st.warning(f"Source inconnue — {dt_err}. — state.utc={state.get('utc')}")
|
| 498 |
|
| 499 |
+
# Sélection secteurs
|
| 500 |
with st.sidebar:
|
| 501 |
st.divider()
|
| 502 |
st.subheader("Secteurs surveillés")
|
| 503 |
+
st.caption("Décoche si tu veux isoler un sous-ensemble. (P1 → P2 → P3)")
|
| 504 |
+
|
| 505 |
+
# ordre stable par priorité puis nom
|
| 506 |
+
ordered_names = sorted(SECTEURS_CRITIQUES.keys(), key=lambda k: (SECTEURS_CRITIQUES[k]["priority"], k))
|
| 507 |
selected = []
|
| 508 |
+
for name in ordered_names:
|
| 509 |
+
p = SECTEURS_CRITIQUES[name]["priority"]
|
| 510 |
checked = st.checkbox(f"[P{p}] {name}", value=True)
|
| 511 |
if checked:
|
| 512 |
selected.append(name)
|
| 513 |
|
| 514 |
st.divider()
|
| 515 |
st.subheader("Rafraîchissement")
|
| 516 |
+
run_now = st.button("Relancer maintenant", use_container_width=True)
|
| 517 |
+
auto_refresh = st.checkbox("Auto-refresh (toutes les 5s)", value=False)
|
| 518 |
+
st.caption("Auto-refresh borné HF : ~10 minutes max.")
|
| 519 |
+
|
| 520 |
+
if not selected:
|
| 521 |
+
st.error("Aucun secteur sélectionné.")
|
| 522 |
+
else:
|
| 523 |
+
|
| 524 |
+
def render(res: dict):
|
| 525 |
+
# Statut global
|
| 526 |
+
if res["status"] == "OK":
|
| 527 |
+
st.success(f"Statut global: OK — runtime={res['utc_runtime']}")
|
| 528 |
+
elif res["status"] == "WARN":
|
| 529 |
+
st.warning(f"Statut global: WARN — runtime={res['utc_runtime']}")
|
| 530 |
+
else:
|
| 531 |
+
st.error(f"Statut global: CRIT — runtime={res['utc_runtime']}")
|
| 532 |
+
|
| 533 |
+
# Global metrics
|
| 534 |
+
c1, c2, c3 = st.columns(3)
|
| 535 |
+
c1.metric("Cohérence globale (0..1)", f"{res['coherence']:.3f}")
|
| 536 |
+
c2.metric("Indice perte d’intelligence (0..1)", f"{res['intel_loss']:.3f}")
|
| 537 |
+
c3.metric("Lecture", res["intel_label"])
|
| 538 |
+
|
| 539 |
+
scope = res.get("metrics_scope", {})
|
| 540 |
+
st.caption(
|
| 541 |
+
f"Secteurs sélectionnés: {scope.get('selected','?')} | "
|
| 542 |
+
f"Instrumentés (REAL/FALLBACK): {scope.get('instrumented','?')} | "
|
| 543 |
+
f"Non instrumentés (N/A): {scope.get('missing','?')}"
|
| 544 |
+
)
|
| 545 |
+
|
| 546 |
+
# Warnings list
|
| 547 |
+
if res["warnings"]:
|
| 548 |
+
st.markdown("### Avertissements (secteurs en dérive)")
|
| 549 |
+
for w in res["warnings"]:
|
| 550 |
+
st.write(f"- {w}")
|
| 551 |
+
|
| 552 |
+
# Table synthèse
|
| 553 |
+
st.markdown("### Synthèse sectorielle (Observed → Pivot → Après Pivot)")
|
| 554 |
+
rows = []
|
| 555 |
+
for name, s in res["sectors"].items():
|
| 556 |
+
if s["metrics"] is None:
|
| 557 |
+
rows.append({
|
| 558 |
+
"Secteur": name,
|
| 559 |
+
"Priorité": f"P{s['priority']}",
|
| 560 |
+
"Origine": s.get("origin", "N/A"),
|
| 561 |
+
"Statut": "N/A",
|
| 562 |
+
"Observed": "—",
|
| 563 |
+
"Target": round(SECTEURS_CRITIQUES[name]["target"], 3),
|
| 564 |
+
"Δ": "—",
|
| 565 |
+
"Step Pivot": "—",
|
| 566 |
+
"Après Pivot": "—",
|
| 567 |
+
"|Δ|": "—",
|
| 568 |
+
"WARN≥": round(s["thresholds"]["warn"], 3),
|
| 569 |
+
"CRIT≥": round(s["thresholds"]["crit"], 3),
|
| 570 |
+
})
|
| 571 |
+
continue
|
| 572 |
+
|
| 573 |
+
m = s["metrics"]
|
| 574 |
+
rows.append({
|
| 575 |
+
"Secteur": name,
|
| 576 |
+
"Priorité": f"P{s['priority']}",
|
| 577 |
+
"Origine": s.get("origin", "?"),
|
| 578 |
+
"Statut": s["status"],
|
| 579 |
+
"Observed": round(m["observed"], 3),
|
| 580 |
+
"Target": round(m["target"], 3),
|
| 581 |
+
"Δ": round(m["delta"], 3),
|
| 582 |
+
"Step Pivot": round(m["pivot_step"], 3),
|
| 583 |
+
"Après Pivot": round(m["after_pivot"], 3),
|
| 584 |
+
"|Δ|": round(m["gravity"], 3),
|
| 585 |
+
"WARN≥": round(s["thresholds"]["warn"], 3),
|
| 586 |
+
"CRIT≥": round(s["thresholds"]["crit"], 3),
|
| 587 |
+
})
|
| 588 |
+
|
| 589 |
+
st.dataframe(rows, use_container_width=True, hide_index=True)
|
| 590 |
+
|
| 591 |
+
# Détails Pivot par secteur
|
| 592 |
+
st.markdown("### Détails (Pivot par secteur)")
|
| 593 |
+
for name, s in res["sectors"].items():
|
| 594 |
+
expanded = (s["priority"] == 1) or (s["status"] in ("WARN", "CRIT"))
|
| 595 |
+
with st.expander(f"{name} — {s['status']} — P{s['priority']} — {s.get('origin','')}", expanded=expanded):
|
| 596 |
+
if s["metrics"] is None:
|
| 597 |
+
st.warning("Secteur non instrumenté dans state.json (N/A). Aucun calcul Pivot appliqué.")
|
| 598 |
+
st.write("Impact:", ", ".join(s["impact"]))
|
| 599 |
+
st.write("Seuils:", s["thresholds"])
|
| 600 |
+
st.write("Cible (target):", SECTEURS_CRITIQUES[name]["target"])
|
| 601 |
+
continue
|
| 602 |
+
|
| 603 |
+
m = s["metrics"]
|
| 604 |
+
st.write("Impact:", ", ".join(s["impact"]))
|
| 605 |
+
d1, d2, d3, d4 = st.columns(4)
|
| 606 |
+
d1.metric("Observed", f"{m['observed']:.3f}")
|
| 607 |
+
d2.metric("Target", f"{m['target']:.3f}")
|
| 608 |
+
d3.metric("Δ", f"{m['delta']:+.3f}")
|
| 609 |
+
d4.metric("|Δ|", f"{m['gravity']:.3f}")
|
| 610 |
+
|
| 611 |
+
st.markdown("**Application du Pivot (action minimale bornée)**")
|
| 612 |
+
st.code(
|
| 613 |
+
f"{m['rule']}\n"
|
| 614 |
+
f"kappa={s['pivot']['kappa']:.3f} | max_step={s['pivot']['max_step']:.3f}\n"
|
| 615 |
+
f"pivot_step={m['pivot_step']:+.3f} => after_pivot={m['after_pivot']:.3f}\n"
|
| 616 |
+
f"seuils: WARN≥{s['thresholds']['warn']:.3f}, CRIT≥{s['thresholds']['crit']:.3f}",
|
| 617 |
+
language="text"
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
st.info(res["pivot_global_law"])
|
| 621 |
+
|
| 622 |
+
# rendu immédiat
|
| 623 |
+
res0 = run_analysis(selected, state, state_ok)
|
| 624 |
+
render(res0)
|
| 625 |
+
|
| 626 |
+
# relance manuelle
|
| 627 |
+
if run_now:
|
| 628 |
+
st.divider()
|
| 629 |
+
st.subheader("Analyse relancée")
|
| 630 |
+
state2, err2 = load_state_json(STATE_URL)
|
| 631 |
+
res2 = run_analysis(selected, state2, (err2 == ""))
|
| 632 |
+
if err2:
|
| 633 |
+
st.warning(f"Relance avec fallback — {err2}")
|
| 634 |
+
render(res2)
|
| 635 |
+
|
| 636 |
+
# auto-refresh borné
|
| 637 |
+
if auto_refresh:
|
| 638 |
+
for _ in range(120): # ~10 minutes
|
| 639 |
+
state_i, err_i = load_state_json(STATE_URL)
|
| 640 |
+
res_i = run_analysis(selected, state_i, (err_i == ""))
|
| 641 |
+
with st.expander("Auto-refresh (dernier état)"):
|
| 642 |
+
render(res_i)
|
| 643 |
+
time.sleep(5)
|
| 644 |
+
st.info("Auto-refresh terminé (limite HF).")
|
| 645 |
+
|
| 646 |
+
# ----------------------------
|
| 647 |
+
# Page Statut / Loi
|
| 648 |
+
# ----------------------------
|
| 649 |
+
else:
|
| 650 |
+
st.subheader("Statut — Loi Pivot globale + Lisibilité de l’instrumentation")
|
| 651 |
+
|
| 652 |
+
st.markdown("## Loi Pivot globale (stabilité civilisationnelle)")
|
| 653 |
+
st.code(
|
| 654 |
+
"∀ secteur Si : Δi(t+1) = Δi(t) − Pivot_i(Δi)\n"
|
| 655 |
+
"avec |Pivot_i| ≤ ε_i (correction maximale admissible)\n\n"
|
| 656 |
+
"Principe : réduire l’écart au réel sans augmenter la gravité globale.\n"
|
| 657 |
+
"=> action minimale, bornée, stable.",
|
| 658 |
+
language="text"
|
| 659 |
+
)
|
| 660 |
+
|
| 661 |
+
st.markdown("## Lisibilité (REAL / N/A / FALLBACK)")
|
| 662 |
+
st.write(
|
| 663 |
+
"- **REAL** : valeur fournie par `state.json` (donnée instrumentée)\n"
|
| 664 |
+
"- **N/A** : secteur absent du `state.json` (on n’invente rien)\n"
|
| 665 |
+
"- **FALLBACK** : `state.json` indisponible, l’app reste vivante via valeurs de secours"
|
| 666 |
+
)
|
| 667 |
+
|
| 668 |
+
st.markdown("## Indice perte d’intelligence (définition IA Fluide)")
|
| 669 |
+
st.code(
|
| 670 |
+
"intel_loss = clamp( moyenne( poids(priority) * |Δ| ) )\n"
|
| 671 |
+
"avec calcul uniquement sur les secteurs instrumentés (REAL/FALLBACK).",
|
| 672 |
+
language="text"
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
st.markdown("## Source automatique (state.json)")
|
| 676 |
+
st.code(
|
| 677 |
+
f"STATE_URL={'(absent)' if not STATE_URL else STATE_URL}\n"
|
| 678 |
+
f"STALE_WARN_MIN={STALE_WARN_MIN}\nSTALE_CRIT_MIN={STALE_CRIT_MIN}",
|
| 679 |
+
language="bash"
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
st.markdown("## Signature runtime")
|
| 683 |
+
st.code(
|
| 684 |
+
f"VERSION={VERSION}\nENABLE_EMAIL={int(ENABLE_EMAIL)}",
|
| 685 |
+
language="bash"
|
| 686 |
+
)
|