expAge commited on
Commit
c7dd694
·
1 Parent(s): 533fa2f

feat(jarvis): auto-switch vers 3.1 quand modèle non-protégé épuisé + clean debug

Browse files

Behavior :
- Si modèle non-3.1 hit PerDay : on switch automatiquement vers
gemini-3.1-flash-lite (modèle protégé, 500 req/j) + nouvelle clé,
on rebuild LLM + agent, on continue le flow sans interruption.
- set_current_model(3.1) + set_current_gemini_key(new) → le dropdown
affiche ✅ devant 3.1 au prochain refresh (via je_chat.change ou
chat.chatbot.change selon l'onglet).
- Côté flow LLM Chatbot ET côté flow Jarvis (run_jarvis_flow).

Cleanup :
- Retire toute la diag debug injectée dans les erreurs UI ([MARKER-
JARVIS-FINAL-YIELD], État du pool, build_model_choices output...).
- Simplifie build_pool_diag_md (garde la version simple, utilisable
pour debug si besoin) et build_model_choices (retire les wraps
try/except qui servaient à pinpoint le 'Button' has no _id —
problème résolu en aval avec _get_app_module).

Files changed (2) hide show
  1. app.py +51 -109
  2. jarvis.py +47 -23
app.py CHANGED
@@ -360,77 +360,27 @@ def _masked_key(key: str) -> str:
360
 
361
 
362
  def build_pool_diag_md() -> str:
363
- """Bloc Markdown de DIAGNOSTIC du pool — affiché dans le chatbot
364
- pour que l'utilisateur voie l'état interne sans accès aux logs.
365
-
366
- Contenu :
367
- - Clé Gemini courante (masquée)
368
- - Modèle actif
369
- - Couples (clé, modèle) blown aujourd'hui
370
- - Clés invalides de la session
371
- """
372
- import traceback
373
  lines = ["**État du pool Gemini** :"]
374
- # Inspect types — révèle si _CURRENT_GEMINI_KEY ou _CURRENT_MODEL
375
- # est devenu un Button (ou autre composant Gradio) au lieu d'une str.
376
- try:
377
- _ck = _CURRENT_GEMINI_KEY
378
- lines.append(f"- type(_CURRENT_GEMINI_KEY)=`{type(_ck).__name__}`")
379
- except Exception as e:
380
- lines.append(f"- ❌ inspect _CURRENT_GEMINI_KEY: {type(e).__name__}: {e}")
381
- try:
382
- _cm = _CURRENT_MODEL
383
- lines.append(f"- type(_CURRENT_MODEL)=`{type(_cm).__name__}` repr=`{_cm!r}`")
384
- except Exception as e:
385
- lines.append(f"- ❌ inspect _CURRENT_MODEL: {type(e).__name__}: {e}")
386
- try:
387
- if _CURRENT_GEMINI_KEY:
388
- lines.append(f"- Clé courante : `{_masked_key(_CURRENT_GEMINI_KEY)}`")
389
- else:
390
- lines.append("- Clé courante : *(aucune)*")
391
- except Exception as e:
392
- lines.append(f"- ❌ Clé courante step: {type(e).__name__}: {e}\n TB: {traceback.format_exc()[-500:]}")
393
- try:
394
- lines.append(f"- Modèle actif : `{_CURRENT_MODEL or '(aucun)'}`")
395
- except Exception as e:
396
- lines.append(f"- ❌ Modèle actif step: {type(e).__name__}: {e}\n TB: {traceback.format_exc()[-500:]}")
397
  today = _today_utc_str()
398
- try:
399
- blown_today = [(k, m) for (k, m, d), v in _BLOWN_TODAY.items()
400
- if d == today and v]
401
- except Exception as e:
402
- blown_today = []
403
- lines.append(f"- ❌ blown_today comprehension: {type(e).__name__}: {e}\n TB: {traceback.format_exc()[-500:]}")
404
  if blown_today:
405
  lines.append("- **Blown aujourd'hui** :")
406
  for k, m in blown_today:
407
- try:
408
- lines.append(f" - `{_masked_key(k)}` / `{m}`")
409
- except Exception as e:
410
- lines.append(f" - ❌ blown entry: {type(e).__name__}: {e}")
411
  else:
412
  lines.append("- **Blown aujourd'hui** : *(aucun)*")
413
  if _INVALID_KEYS:
414
  lines.append("- **Clés marquées invalides (session)** :")
415
  for k in _INVALID_KEYS:
416
  lines.append(f" - `{_masked_key(k)}`")
417
- # DIAG : état in-memory par modèle Gemini (rapide, sûr)
418
- today = _today_utc_str()
419
- lines.append("- **État par modèle Gemini** (debug) :")
420
- for key in GEMINI_NATIVE_REQUIRED:
421
- if _CURRENT_GEMINI_KEY:
422
- blown = _BLOWN_TODAY.get((_CURRENT_GEMINI_KEY, key, today), False)
423
- else:
424
- blown = False
425
- lines.append(f" - `{key}` : blown_on_current_key={blown}")
426
- # DIAG : résultat de build_model_choices() — chaque étape wrappée,
427
- # un [ERR ...] indique la step qui foire dans le contexte Jarvis.
428
- try:
429
- lines.append("- **build_model_choices() output** :")
430
- for lbl, k in build_model_choices():
431
- lines.append(f" - `{k}` → `{lbl}`")
432
- except Exception as exc:
433
- lines.append(f"- *(build_model_choices raised : {type(exc).__name__}: {exc})*")
434
  return "\n".join(lines)
435
 
436
 
@@ -564,53 +514,20 @@ def build_model_choices(for_chatbot: bool = False) -> list[tuple[str, str]]:
564
 
565
  Marquage server-side (les deux dropdowns) :
566
  - ✅ devant le modèle courant (_CURRENT_MODEL)
567
- - suffixe `— épuisé sur cette clé` pour les modèles blown sur
568
- la clé Gemini courante (le JS ajoute ❌ + grisage CSS)
569
-
570
- DIAG (Phase 13) : chaque étape est wrappée en try/except pour
571
- identifier le coupable du mystérieux 'Button' has no attribute
572
- '_id' qui empêche le refresh des dropdowns Jarvis.
573
  """
574
  import re as _re
575
  out: list[tuple[str, str]] = []
576
- try:
577
- items = list(ALL_MODELS.items())
578
- except Exception as e:
579
- return [(f"[ERR ALL_MODELS.items: {type(e).__name__}: {e}]", "err")]
580
- for i, item in enumerate(items):
581
- try:
582
- key, label = item
583
- except Exception as e:
584
- out.append((f"[ERR unpack #{i}: {type(e).__name__}: {e}]", f"err{i}"))
585
- continue
586
- try:
587
- # Étape 1 : check si modèle Gemini natif
588
- is_gemini = key in GEMINI_NATIVE_REQUIRED
589
- except Exception as e:
590
- out.append((f"[ERR is_gemini {key!r}: {type(e).__name__}: {e}]", str(key)))
591
- continue
592
- try:
593
- # Étape 2 : check si blown sur clé courante
594
- is_blown = is_gemini and is_model_blown_on_current_key(key)
595
- except Exception as e:
596
- out.append((f"[ERR is_blown {key!r}: {type(e).__name__}: {e}]", str(key)))
597
- continue
598
- try:
599
- # Étape 3 : décoration label
600
- if is_blown:
601
- base = _re.sub(r"\s*\(.*?\)\s*$", "", str(label)).strip()
602
- decorated = f"{base} — épuisé sur cette clé"
603
- elif key == _CURRENT_MODEL:
604
- decorated = f"✅ {label}"
605
- else:
606
- decorated = label
607
- except Exception as e:
608
- out.append((f"[ERR decorate {key!r}: {type(e).__name__}: {e}]", str(key)))
609
- continue
610
- try:
611
- out.append((decorated, key))
612
- except Exception as e:
613
- out.append((f"[ERR append {key!r}: {type(e).__name__}: {e}]", str(key)))
614
  return out
615
 
616
 
@@ -1181,6 +1098,35 @@ def chat_with_agent(message: str, history: list[dict], api_key: str, model: str,
1181
  if is_per_day_quota_exhausted(e, expected_model=model):
1182
  if current_gemini_key:
1183
  mark_gemini_key_blown(current_gemini_key, model)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1184
  if (model == GEMINI_POOL_PROTECTED_MODEL
1185
  and is_per_day_quota_exhausted(e, expected_model=model)):
1186
  switched = False
@@ -1265,11 +1211,7 @@ def chat_with_agent(message: str, history: list[dict], api_key: str, model: str,
1265
  f"({len(progress_full)})</summary>\n\n"
1266
  f"{(chr(10)*2).join(progress_full)}\n\n</details>"
1267
  )
1268
- try:
1269
- diag = "\n\n---\n" + build_pool_diag_md()
1270
- except Exception:
1271
- diag = ""
1272
- yield f"❌ Erreur agent : {e}" + diag + err_block, _NOOP_FILE
1273
  return
1274
 
1275
  # Viz : iframe interactif embarqué dans un gr.HTML séparé.
 
360
 
361
 
362
  def build_pool_diag_md() -> str:
363
+ """Bloc Markdown de DIAGNOSTIC du pool — utilisable pour debug.
364
+ Non injecté dans les erreurs UI par défaut."""
 
 
 
 
 
 
 
 
365
  lines = ["**État du pool Gemini** :"]
366
+ if _CURRENT_GEMINI_KEY:
367
+ lines.append(f"- Clé courante : `{_masked_key(_CURRENT_GEMINI_KEY)}`")
368
+ else:
369
+ lines.append("- Clé courante : *(aucune)*")
370
+ lines.append(f"- Modèle actif : `{_CURRENT_MODEL or '(aucun)'}`")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  today = _today_utc_str()
372
+ blown_today = [(k, m) for (k, m, d), v in _BLOWN_TODAY.items()
373
+ if d == today and v]
 
 
 
 
374
  if blown_today:
375
  lines.append("- **Blown aujourd'hui** :")
376
  for k, m in blown_today:
377
+ lines.append(f" - `{_masked_key(k)}` / `{m}`")
 
 
 
378
  else:
379
  lines.append("- **Blown aujourd'hui** : *(aucun)*")
380
  if _INVALID_KEYS:
381
  lines.append("- **Clés marquées invalides (session)** :")
382
  for k in _INVALID_KEYS:
383
  lines.append(f" - `{_masked_key(k)}`")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  return "\n".join(lines)
385
 
386
 
 
514
 
515
  Marquage server-side (les deux dropdowns) :
516
  - ✅ devant le modèle courant (_CURRENT_MODEL)
517
+ - suffixe `— épuisé sur cette clé` pour les modèles Gemini natifs
518
+ blown sur la clé courante (le JS ajoute ❌ + grisage CSS)
 
 
 
 
519
  """
520
  import re as _re
521
  out: list[tuple[str, str]] = []
522
+ for key, label in ALL_MODELS.items():
523
+ if key in GEMINI_NATIVE_REQUIRED and is_model_blown_on_current_key(key):
524
+ base = _re.sub(r"\s*\(.*?\)\s*$", "", str(label)).strip()
525
+ decorated = f"{base} épuisé sur cette clé"
526
+ elif key == _CURRENT_MODEL:
527
+ decorated = f"✅ {label}"
528
+ else:
529
+ decorated = label
530
+ out.append((decorated, key))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  return out
532
 
533
 
 
1098
  if is_per_day_quota_exhausted(e, expected_model=model):
1099
  if current_gemini_key:
1100
  mark_gemini_key_blown(current_gemini_key, model)
1101
+ # AUTO-BASCULE vers le modèle protégé (3.1, 500 req/j)
1102
+ # quand un modèle non-protégé est épuisé.
1103
+ if (model != GEMINI_POOL_PROTECTED_MODEL
1104
+ and is_per_day_quota_exhausted(e, expected_model=model)):
1105
+ try:
1106
+ next_key_for_protected = pick_unblown_gemini_key(GEMINI_POOL_PROTECTED_MODEL)
1107
+ if next_key_for_protected:
1108
+ current_gemini_key = next_key_for_protected
1109
+ model = GEMINI_POOL_PROTECTED_MODEL
1110
+ set_current_gemini_key(current_gemini_key)
1111
+ set_current_model(model)
1112
+ llm = _build_llm(
1113
+ model, api_key,
1114
+ use_thinking=use_thinking,
1115
+ gemini_key_override=current_gemini_key,
1116
+ )
1117
+ agent = build_jdm_agent(
1118
+ client=get_client(), llm=llm
1119
+ )
1120
+ switch_msg = (
1121
+ f"\n\n*🔄 Quota quotidien épuisé sur ce "
1122
+ f"modèle — bascule automatique sur "
1123
+ f"`{GEMINI_POOL_PROTECTED_MODEL}` (500 req/j).*"
1124
+ )
1125
+ current_progress = "\n\n".join(progress_live)
1126
+ yield current_progress + switch_msg, _NOOP_FILE
1127
+ continue
1128
+ except Exception:
1129
+ pass
1130
  if (model == GEMINI_POOL_PROTECTED_MODEL
1131
  and is_per_day_quota_exhausted(e, expected_model=model)):
1132
  switched = False
 
1211
  f"({len(progress_full)})</summary>\n\n"
1212
  f"{(chr(10)*2).join(progress_full)}\n\n</details>"
1213
  )
1214
+ yield f"❌ Erreur agent : {e}" + err_block, _NOOP_FILE
 
 
 
 
1215
  return
1216
 
1217
  # Viz : iframe interactif embarqué dans un gr.HTML séparé.
jarvis.py CHANGED
@@ -1467,6 +1467,52 @@ def run_jarvis_flow(
1467
  if is_per_day_quota_exhausted(e, expected_model=model):
1468
  if _mark_blown_fn and current_gemini_key:
1469
  _mark_blown_fn(current_gemini_key, model)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1470
  if (model == _PROTECTED
1471
  and is_per_day_quota_exhausted(e, expected_model=model)):
1472
  switched = False
@@ -1572,32 +1618,10 @@ def run_jarvis_flow(
1572
  f"({len(progress_full)})</summary>\n\n"
1573
  f"{(chr(10)*2).join(progress_full)}\n\n</details>"
1574
  )
1575
- # Diag : on récupère app via sys.modules (déjà
1576
- # chargé par le processus principal Gradio). PAS
1577
- # de `from app import` qui re-déclenche l'évaluation
1578
- # du module dans un worker fork → bug Gradio
1579
- # 'Button' has no '_id' à l'instanciation des
1580
- # composants (problème connu fork+gradio context).
1581
- diag = ""
1582
- try:
1583
- import sys as _sys
1584
- # Sur HF Spaces, app.py est lancé comme __main__,
1585
- # pas 'app'. On cherche les deux.
1586
- app_mod = _sys.modules.get('__main__')
1587
- if app_mod is None or not hasattr(app_mod, 'build_pool_diag_md'):
1588
- app_mod = _sys.modules.get('app')
1589
- if app_mod is None:
1590
- diag = "\n\n---\n*(diag : ni __main__ ni app dans sys.modules)*"
1591
- elif not hasattr(app_mod, 'build_pool_diag_md'):
1592
- diag = "\n\n---\n*(diag : module trouvé mais sans build_pool_diag_md)*"
1593
- else:
1594
- diag = "\n\n---\n" + app_mod.build_pool_diag_md()
1595
- except Exception as _ce:
1596
- diag = f"\n\n---\n*(diag raised : {type(_ce).__name__}: {_ce})*"
1597
  yield (
1598
  [{"role": "user", "content": user_display},
1599
  {"role": "assistant",
1600
- "content": f"❌ Erreur agent : {e}" + diag + err_block}],
1601
  last_file_path, _read_file_preview(last_file_path),
1602
  )
1603
  return
 
1467
  if is_per_day_quota_exhausted(e, expected_model=model):
1468
  if _mark_blown_fn and current_gemini_key:
1469
  _mark_blown_fn(current_gemini_key, model)
1470
+ # AUTO-BASCULE vers le modèle protégé (3.1, 500 req/j)
1471
+ # quand un modèle non-protégé est épuisé. L'utilisateur
1472
+ # continue son flow sans interruption — on rebuild
1473
+ # LLM + agent avec _PROTECTED et la même clé (ou une
1474
+ # autre si celle-ci est aussi blown pour 3.1).
1475
+ if (model != _PROTECTED
1476
+ and is_per_day_quota_exhausted(e, expected_model=model)
1477
+ and _app is not None):
1478
+ try:
1479
+ pick_fn = _app.pick_unblown_gemini_key
1480
+ # Cherche une clé utilisable pour _PROTECTED
1481
+ # (peut être la même que current_gemini_key
1482
+ # car le quota est par-modèle).
1483
+ next_key_for_protected = pick_fn(_PROTECTED)
1484
+ if next_key_for_protected:
1485
+ current_gemini_key = next_key_for_protected
1486
+ model = _PROTECTED # mute le modèle local
1487
+ try:
1488
+ _app.set_current_gemini_key(current_gemini_key)
1489
+ _app.set_current_model(model)
1490
+ except Exception:
1491
+ pass
1492
+ llm = build_llm_fn(
1493
+ model, api_key,
1494
+ use_thinking=use_thinking,
1495
+ gemini_key_override=current_gemini_key,
1496
+ )
1497
+ agent = build_agent_fn(
1498
+ client=get_client_fn(), llm=llm
1499
+ )
1500
+ _add_line(
1501
+ f"*🔄 Quota quotidien épuisé sur ce "
1502
+ f"modèle — bascule automatique sur "
1503
+ f"`{_PROTECTED}` (500 req/j) pour "
1504
+ f"continuer le travail.*"
1505
+ )
1506
+ yield (
1507
+ [{"role": "user", "content": user_display},
1508
+ {"role": "assistant",
1509
+ "content": "\n\n".join(progress_live)}],
1510
+ last_file_path,
1511
+ _read_file_preview(last_file_path),
1512
+ )
1513
+ continue
1514
+ except Exception:
1515
+ pass # bascule indisponible → erreur finale
1516
  if (model == _PROTECTED
1517
  and is_per_day_quota_exhausted(e, expected_model=model)):
1518
  switched = False
 
1618
  f"({len(progress_full)})</summary>\n\n"
1619
  f"{(chr(10)*2).join(progress_full)}\n\n</details>"
1620
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1621
  yield (
1622
  [{"role": "user", "content": user_display},
1623
  {"role": "assistant",
1624
+ "content": f"❌ Erreur agent : {e}" + err_block}],
1625
  last_file_path, _read_file_preview(last_file_path),
1626
  )
1627
  return