bloc (les
# styles bloc s'appliquent au tout).
# Markdown interne au thinking (genre
# `code` ou *italique*) ne sera pas
# rendu — acceptable pour un bloc déjà
# marqué comme « discret ».
t_html = (
t.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\n", "
")
)
line = (
f"
"
f"💭 {t_html}
"
)
_add_line(line)
# 2) Texte parlé entre 2 tool_calls (Claude/
# GPT le font ; Gemini souvent vide).
# Blockquote normal pour le distinguer du
# thinking (qui est plus discret).
spoken = _content_to_text(m.content)
if tcs and spoken.strip():
_add_line(f"> 💬 {spoken.strip()}")
if tcs:
for tc in tcs:
name = tc.get("name", "?")
tc_args = tc.get("args") or {}
narrated = _narrate_tool_call(name, tc_args)
if narrated:
_add_line(
f'
'
f'{narrated}
'
)
else:
args_str = ", ".join(
f"{k}={v!r}"
for k, v in tc_args.items()
)
_add_line(
f'
'
f'🔧 `{name}({args_str})`
'
)
# Ajoute en bas un indicateur fugace
# « génération en cours » pour qu'on
# sache que ça tourne (le LLM peut
# tarder à produire sa réponse finale
# ou son prochain tool_call). Cette
# ligne disparaît au prochain yield
# ou au yield final.
live_with_pending = (
"\n\n".join(progress_live)
+ "\n\n" + _pending_line()
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": live_with_pending}],
last_file_path,
_read_file_preview(last_file_path),
)
else:
# Pas de tool_calls → réponse finale
final_answer = spoken
elif isinstance(m, ToolMessage):
content = _content_to_text(m.content)
if m.name == "write_submission_file":
p = _extract_submission_path(content)
if p:
last_file_path = p
narrated_done = _narrate_tool_result(m.name, content)
if narrated_done:
_add_line(
f'
'
f'{narrated_done}
'
)
else:
preview = content[:120].replace("\n", " ")
if len(content) > 120:
preview += "…"
_add_line(
f'
'
f'✓ *{m.name}* renvoie {len(content)} chars : `{preview}`'
f'
'
)
live_with_pending = (
"\n\n".join(progress_live)
+ "\n\n" + _pending_line()
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": live_with_pending}],
_current_file_path(),
_read_file_preview(_current_file_path()),
)
# FIN DE CHUNK : check si historique
# dépasse le seuil → condensation proactive
# (mêmes conditions que post-PerMinute :
# build_relance_summary + nudge random).
# Si condensé, on BREAK le for chunk et on
# CONTINUE le while True pour relancer
# agent.stream avec les messages condensés.
chars_before = _history_total_chars(accumulated_messages)
if chars_before > HISTORY_CONDENSE_THRESHOLD_CHARS:
condensed = condense_history_with_nudge(
accumulated_messages,
consolidation_target=consolidation_target,
attempt=proactive_condense_count,
)
if condensed is not None:
proactive_condense_count += 1
accumulated_messages = condensed
_add_line(
f"*🗜️ Historique condensé "
f"({chars_before // 1000}k chars → résumé, "
f"relance {proactive_condense_count}) — "
f"l'agent reprend avec un nudge frais.*"
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": "\n\n".join(progress_live)}],
last_file_path,
_read_file_preview(last_file_path),
)
_need_restart_after_condense = True
break # sort du for chunk
if _need_restart_after_condense:
continue # relance agent.stream avec accumulated_messages condensé
# Sortie normale de la boucle for chunk → quitter while
break
except Exception as e:
# Quota PerMinute Gemini ET premier essai : on attend
# le délai, on AFFICHE un message d'attente, et on
# CONTINUE le travail en cours — on ne reset PAS les
# progress lists, et on relance agent.stream() en
# passant les `accumulated_messages` pour que langgraph
# reprenne là où il en était (les messages déjà
# produits = HumanMessage + AIMessages + ToolMessages).
# 1) Quota QUOTIDIEN épuisé.
# Si on a un POOL de clés (GOOGLE_API_KEYS CSV),
# on marque la clé courante comme blown et on
# tente de basculer sur la suivante non-blown.
# Sinon (pool vide / toutes blown), on signale
# et on stop.
# 0) Clé API invalide (typo, révoquée). Marquer
# la clé courante comme invalide pour la session
# et basculer sur la suivante du pool. Même
# logique que PerDay (rebuild LLM + agent +
# continue), mais marquage différent (permanent).
if is_invalid_api_key(e):
# On NE MARQUE INVALIDE que si c'est le modèle
# protégé (3.1) qui rejette la clé. Pour les
# autres modèles (2.5, 3.5), un INVALID_KEY
# peut être trompeur (endpoint OpenAI-compat
# qui retourne ce code pour d'autres raisons
# — modèle non dispo, version, etc.). Marquer
# globalement invalide pourrait gâcher une
# clé pourtant valide pour 3.1.
_app = _get_app_module()
_PROTECTED = (getattr(_app, 'GEMINI_POOL_PROTECTED_MODEL',
"gemini-3.1-flash-lite")
if _app else "gemini-3.1-flash-lite")
if model != _PROTECTED:
# Abort + DIAG complet pour pouvoir tracer
# honnêtement (cas 2.5 qui renvoie INVALID
# alors que clé valide pour 3.1).
diag_lines = [
f"⚠️ **`API_KEY_INVALID` pour `{model}`** "
f"alors que clé attendue valide pour `{_PROTECTED}`."
f"\n\n**Diagnostic du dernier build LLM** :",
]
try:
db = getattr(_app, '_DEBUG_LAST_BUILD', {}) or {}
if not db:
diag_lines.append("- *(_DEBUG_LAST_BUILD vide)*")
for k, v in db.items():
diag_lines.append(f"- `{k}` : `{v}`")
except Exception as _ex:
diag_lines.append(f"- *(diag indisponible : {_ex})*")
diag_lines.append(
"\n**Exception brute Gemini** (premiers 1500 chars) :"
)
diag_lines.append(f"```\n{str(e)[:1500]}\n```")
diag_lines.append(
f"\n➡️ Pour l'instant, bascule sur "
f"`{_PROTECTED}` ou un BYOK. La clé n'est "
f"PAS marquée invalide globalement."
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": "\n".join(diag_lines)}],
last_file_path,
_read_file_preview(last_file_path),
)
return
switched = False
try:
if _app is None:
raise RuntimeError("app module unavailable")
mark_gemini_key_invalid = _app.mark_gemini_key_invalid
pick_unblown_gemini_key = _app.pick_unblown_gemini_key
gemini_pool_size = _app.gemini_pool_size
if current_gemini_key:
mark_gemini_key_invalid(current_gemini_key)
next_key = pick_unblown_gemini_key(
model, skip=current_gemini_key
)
if next_key:
pool_n = gemini_pool_size()
current_gemini_key = next_key
try:
_app.set_current_gemini_key(current_gemini_key)
except Exception:
pass
llm = build_llm_fn(
model, api_key,
use_thinking=use_thinking,
gemini_key_override=current_gemini_key,
)
agent = build_agent_fn(
client=get_client_fn(), llm=llm
)
_add_line(
f"*🔑 Clé Google invalide détectée — "
f"bascule sur une autre clé du pool "
f"(pool : {pool_n} clés).*"
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": "\n\n".join(progress_live)}],
last_file_path,
_read_file_preview(last_file_path),
)
switched = True
except Exception:
pass
if switched:
continue
# Yield au chatbot avec DIAGNOSTIC : ce qui
# a été parsé depuis GOOGLE_API_KEYS (4 chars
# début + 4 chars fin + longueur de chaque
# clé) pour vérifier que le parsing CSV
# n'a rien tronqué.
try:
_app = _get_app_module()
if _app is None:
raise RuntimeError("app module unavailable")
_parse_keys = _app._parse_google_keys
_masked_key = _app._masked_key
parsed = _parse_keys()
diag = "\n".join(
f" - {i+1}. {_masked_key(k)}"
for i, k in enumerate(parsed)
) or " (aucune clé parsée)"
except Exception:
parsed = []
diag = " (diagnostic indisponible)"
err_msg = (
"❌ **Toutes les clés Google du pool ont "
"échoué**.\n\n"
f"**Diagnostic** : {len(parsed)} clé(s) "
f"parsée(s) depuis `GOOGLE_API_KEYS` :\n"
f"{diag}\n\n"
"Vérifie ci-dessus que chaque clé a la "
"**longueur attendue (~39 chars)** et "
"commence par `AIza`. Si une clé est "
"tronquée → problème de parsing CSV.\n\n"
"Sinon, causes possibles côté Google :\n"
"1. Clés non activées pour l'**API "
"Generative Language** (Google Cloud "
"Console).\n"
"2. Clés d'un projet sans accès aux "
"modèles Gemini 3.x.\n"
"3. Quotas PerDay tous épuisés (reset "
"à minuit UTC).\n\n"
"Bascule sur un modèle BYOK Claude / GPT."
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant", "content": err_msg}],
last_file_path,
_read_file_preview(last_file_path),
)
return
# PerDay : on TRACE TOUJOURS la clé courante
# comme blown pour ce modèle (visuel dropdown
# « épuisé »). On ne BASCULE de clé que si on
# est sur le modèle protégé (gemini-3.1-flash-
# lite, 500 req/jour). Pour les autres (quotas
# ~20 req/jour), on remonte l'erreur après le
# marquage.
_app = _get_app_module()
if _app is not None:
_PROTECTED = getattr(_app, 'GEMINI_POOL_PROTECTED_MODEL', "gemini-3.1-flash-lite")
_mark_blown_fn = getattr(_app, 'mark_gemini_key_blown', None)
else:
_PROTECTED = "gemini-3.1-flash-lite"
_mark_blown_fn = None
if is_per_day_quota_exhausted(e, expected_model=model):
if _mark_blown_fn and current_gemini_key:
_mark_blown_fn(current_gemini_key, model)
# PerDay sur modèle non-protégé.
# Deux modes :
# - auto_switch_on_perday=True (option C) : on
# bascule silencieusement sur _PROTECTED et
# on continue le flow (state préservé via
# accumulated_messages, strip_thinking pour
# les tokens).
# - sinon (option B, défaut) : ABORT, save
# state stripped, yield un 5-tuple avec
# state + marker → le wrapper affiche un
# bouton « Continuer avec 3.1 ».
if (model != _PROTECTED
and is_per_day_quota_exhausted(e, expected_model=model)
and _app is not None):
try:
_app.set_current_model(_PROTECTED)
except Exception:
pass
if auto_switch_on_perday and current_gemini_key:
# Option C : auto-retry silencieux
try:
accumulated_messages = strip_thinking_blocks(
accumulated_messages, keep_last=True
)
model = _PROTECTED
llm = build_llm_fn(
model, api_key,
use_thinking=use_thinking,
gemini_key_override=current_gemini_key,
)
agent = build_agent_fn(
client=get_client_fn(), llm=llm
)
_add_line(
f"*🔄 Quota épuisé — bascule auto "
f"sur `{_PROTECTED}`, je continue.*"
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": "\n\n".join(progress_live)}],
last_file_path,
_read_file_preview(last_file_path),
)
continue
except Exception:
pass # fallback sur abort si erreur
# Option B (défaut) : abort + save state +
# yield 5-tuple pour activer bouton continuer.
try:
stripped = strip_thinking_blocks(
accumulated_messages, keep_last=True
)
except Exception:
stripped = accumulated_messages
saved_state = {
"accumulated_messages": stripped,
"progress_full": list(progress_full),
"progress_live": list(progress_live),
"last_file_path": last_file_path,
"user_display": user_display,
}
switch_msg = (
f"⚠️ **Modèle `{model}` épuisé pour "
f"aujourd'hui** (quota quotidien).\n\n"
f"Le sélecteur est passé sur "
f"`{_PROTECTED}` (500 req/j).\n\n"
f"➡️ Clique sur **« ▶️ Continuer avec "
f"`{_PROTECTED}` »** pour reprendre EXACTEMENT "
f"où l'agent s'est arrêté (state préservé), "
f"ou re-clique « Lancer » pour repartir de "
f"zéro."
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant", "content": switch_msg}],
last_file_path,
_read_file_preview(last_file_path),
saved_state,
"show_continue_btn",
)
return
if (model == _PROTECTED
and is_per_day_quota_exhausted(e, expected_model=model)):
switched = False
try:
_app = _get_app_module()
if _app is None:
raise RuntimeError("app module unavailable")
mark_gemini_key_blown = _app.mark_gemini_key_blown
pick_unblown_gemini_key = _app.pick_unblown_gemini_key
gemini_pool_size = _app.gemini_pool_size
if current_gemini_key:
mark_gemini_key_blown(current_gemini_key, model)
next_key = pick_unblown_gemini_key(
model, skip=current_gemini_key
)
if next_key:
# Rebuild LLM + agent avec la nouvelle clé.
pool_n = gemini_pool_size()
current_gemini_key = next_key
try:
_app.set_current_gemini_key(current_gemini_key)
except Exception:
pass
llm = build_llm_fn(
model, api_key,
use_thinking=use_thinking,
gemini_key_override=current_gemini_key,
)
agent = build_agent_fn(
client=get_client_fn(), llm=llm
)
_add_line(
f"*🔄 Quota quotidien atteint sur "
f"cette clé Google — bascule sur "
f"une autre clé du pool "
f"(pool : {pool_n} clés).*"
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": "\n\n".join(progress_live)}],
last_file_path,
_read_file_preview(last_file_path),
)
switched = True
except Exception:
pass # bascule indisponible → on raise comme avant
if switched:
continue # reprend la boucle avec la nouvelle clé
raise RuntimeError(
"Quota quotidien Gemini free tier épuisé sur "
"TOUTES les clés du pool (ou pool vide). Le "
"quota se réinitialise à minuit UTC. Réessaie "
"demain ou bascule sur un modèle BYOK "
"(Claude / GPT)."
) from e
# 2) Rate limit PerMinute → retry avec attente
retry_delay = detect_rate_limit_retry(e)
if retry_delay is not None:
consecutive_rate_limit_hits += 1
# Filet : 3 hits PerMinute consécutifs sans
# progrès = quotas glissants croisés bloqués.
if consecutive_rate_limit_hits >= MAX_CONSECUTIVE_RATE_LIMIT:
raise RuntimeError(
f"Quotas Gemini free tier croisés "
f"({consecutive_rate_limit_hits} hits "
f"PerMinute consécutifs sans progrès). "
f"Les fenêtres glissantes ne s'ouvrent "
f"jamais en même temps. Réessaie dans "
f"quelques minutes ou bascule sur un "
f"modèle BYOK (Claude / GPT)."
) from e
rate_limit_attempts += 1
wait_msg = (
f"*⏳ Quota Gemini free tier atteint — j'attends "
f"{retry_delay:.0f}s puis je CONTINUE le travail "
f"en cours (pas de redémarrage).*"
)
current_progress = "\n\n".join(progress_live)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": current_progress + "\n\n" + wait_msg}],
_current_file_path(), _read_file_preview(_current_file_path()),
)
_time.sleep(retry_delay)
# PAS de reset des progress / last_file_path.
# MAIS strip des blocs thinking pour réduire
# massivement les tokens ré-envoyés (le LLM
# n'a pas besoin de ses propres pensées pour
# continuer — juste des tool_calls et résultats).
# On garde le DERNIER thinking pour préserver
# le thought_signature Gemini 3.x.
accumulated_messages = strip_thinking_blocks(
accumulated_messages, keep_last=True
)
# Condensation proactive si APRÈS strip
# l'historique reste massif (>seuil). Le
# helper renvoie None si pas nécessaire,
# ou la nouvelle liste [initial, summary
# + nudge random] sinon. Logique identique
# à la condensation proactive en cours de
# streaming (cf. fin du for chunk).
chars_before = _history_total_chars(accumulated_messages)
condensed = condense_history_with_nudge(
accumulated_messages,
consolidation_target=consolidation_target,
attempt=rate_limit_attempts,
)
if condensed is not None:
accumulated_messages = condensed
_add_line(
f"*🗜️ Historique condensé "
f"({chars_before // 1000}k chars → résumé) — "
f"l'agent reprend avec un nudge frais.*"
)
# Yield immédiat — sans ça la ligne n'est
# poussée à Gradio QU'AU PROCHAIN chunk,
# qui peut tarder (PerMinute en boucle) →
# l'utilisateur ne voyait jamais le message
# condensé, juste « j'attends Xs ».
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": "\n\n".join(progress_live)}],
last_file_path,
_read_file_preview(last_file_path),
)
continue
# Pas un quota retryable, ou déjà tenté : erreur finale
err_block = ""
if progress_full:
err_block = (
f"\n\n
🧠 Voir les étapes avant erreur "
f"({len(progress_full)})
\n\n"
f"{(chr(10)*2).join(progress_full)}\n\n"
)
# PRÉSERVATION DU FICHIER SUR ERREUR : on yield
# _current_file_path() (priorité canonical_path
# si auto-append a écrit quelque chose) pour
# que l'utilisateur garde son fichier de
# consolidation même quand l'API LLM crashe.
yield (
[{"role": "user", "content": user_display},
{"role": "assistant",
"content": f"❌ Erreur agent : {e}" + err_block}],
_current_file_path(), _read_file_preview(_current_file_path()),
)
return
# Sortie normale du with → check persistance.
# Si le LLM a finalisé prématurément (consolidés < target)
# et qu'on a encore des relances disponibles, on injecte un
# nudge et on relance le with (= nouveau budget_context,
# mais accumulated_messages conservé donc l'agent reprend
# avec tout son contexte).
if consolidation_target is None:
persistence_done = True
continue
# Source de vérité = registry GLOBAL des consolidations
# (cumulatif depuis l'entrée dans exclusion_context, survit
# aux RESET de accumulated_messages opérés par les relances
# persistance). count_consolidated_in_messages() était
# défaillant ici car il ne voyait que les ToolMessages du
# tour COURANT — chaque relance était comptée from scratch
# → boucle infinie possible si le LLM consolide < target
# par tour. Cf. bug observé : « il a déjà ses 15 mais il
# pense être à deux ».
from jdm_agent.enrich import count_consolidations
n_done = count_consolidations()
if n_done >= consolidation_target:
persistence_done = True
continue
# Pas de cap dur sur les relances persistance — on continue
# tant que le LLM finalise sans avoir atteint le target.
# Si l'utilisateur veut un cap, il passe max_persistence_relances
# (par défaut None = illimité).
if max_persistence_relances is not None and persistence_relances >= max_persistence_relances:
persistence_done = True
continue
# On relance avec un nudge fort.
persistence_relances += 1
# Construit un résumé condensé (consolidés / échecs / pré-fetchs)
# à partir des accumulated_messages, PUIS reset à juste :
# [HumanMessage initial, HumanMessage du résumé+nudge]
# → drop massif des tokens (de ~50k à ~2k typiquement).
# Le LLM reprend frais avec un état explicite plutôt que de
# devoir digérer 50+ messages avec leurs raisonnements.
summary = build_relance_summary(
accumulated_messages, n_done, consolidation_target,
persistence_relances, max_persistence_relances,
)
initial_human = accumulated_messages[0] # HumanMessage(prompt)
accumulated_messages = [
initial_human,
HumanMessage(content=summary),
]
_cap_label = (
f"/{max_persistence_relances}"
if max_persistence_relances is not None else ""
)
_add_line(
f"*🔁 Relance automatique {persistence_relances}{_cap_label} — "
f"{n_done}/{consolidation_target} consolidés, on continue.*"
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant", "content": "\n\n".join(progress_live)}],
last_file_path, _read_file_preview(last_file_path),
)
# Boucle continue → nouveau with budget_context + nouvelle
# invocation agent.stream avec accumulated_messages enrichi.
# Réponse finale : on remplace les progress_lines par la réponse
# définitive du modèle, suivie d'un footer avec compteur (limite
# n'est mentionnée que si elle est bornée).
n = budget.count
if budget.limit:
footer = (
f"\n\n---\n*Budget : {n} appel{'s' if n > 1 else ''} "
f"d'outils consommé{'s' if n > 1 else ''} / {budget.limit}.*"
)
if budget.exhausted:
footer += " ⚠️ **Budget atteint** — relance avec un budget plus large si besoin."
else:
footer = (
f"\n\n---\n*Budget illimité — {n} appel{'s' if n > 1 else ''} "
f"d'outils consommé{'s' if n > 1 else ''}.*"
)
# Bloc collapsible
avec la trace complète : résumé de
# raisonnement (le « thought summary » de Gemini, déjà condensé
# côté API — pas de version raw exposée) + texte parlé +
# tool_calls + retours, dans l'ordre chronologique. Replié par
# défaut pour ne pas polluer la réponse.
#
# Libellé adaptatif :
# - raisonnement ON → « Voir le résumé du raisonnement (N étapes) »
# - raisonnement OFF → « Voir les étapes (N étapes) »
# (pas de raisonnement LLM dans le bloc, juste la narration des
# appels d'outils et leurs résultats)
reasoning_block = ""
if progress_full:
full_text = "\n\n".join(progress_full)
n_steps = len(progress_full)
plural = "s" if n_steps > 1 else ""
if use_thinking:
summary_label = (
f"🧠 Voir le résumé du raisonnement "
f"({n_steps} étape{plural})"
)
else:
summary_label = f"🧠 Voir les étapes ({n_steps} étape{plural})"
reasoning_block = (
f"\n\n{summary_label}
\n\n"
f"{full_text}\n\n "
)
# OPTION B — FUSION FINALE depuis le registry de consolidation.
# On dumpe TOUS les triplets consolidés du run (cumulatif via le
# registry survivant grâce à exclusion_context wrappant le while
# persistance) dans canonical_path. Garantit que le fichier
# affiché contient TOUT, peu importe ce que le LLM a écrit dans
# des paths intermédiaires ou si certaines écritures ont été
# écrasées par des appels successifs.
try:
from jdm_agent.enrich import list_consolidations
from jdm_agent.enrich.pipeline import write_submission as _write_sub
from jdm_agent.enrich import Candidate as _Candidate
from pathlib import Path as _Path
entries = list_consolidations()
if entries:
_Path(canonical_path).parent.mkdir(parents=True, exist_ok=True)
cands = [
_Candidate(
term=e["term"], relation=e["relation"], target=e["target"],
annotation="",
consolidation_explanation=e.get("explanation") or "",
confidence=0.8, source="agent",
validation_status="ok",
consolidation_status="consolidated",
)
for e in entries
]
_write_sub(canonical_path, cands, client=get_client_fn())
last_file_path = canonical_path
_add_line(
f"*📦 Fichier final fusionné : {len(entries)} triplets "
f"consolidés écrits dans `{canonical_path}` "
f"(garantit que rien n'est perdu).*"
)
except Exception as _e:
# Safety : si la fusion finale foire, on ne casse pas le flow,
# on garde last_file_path tel que la dernière écriture LLM
# l'avait laissé.
_add_line(
f"*⚠️ Fusion finale impossible : {_e}. Le fichier affiché "
f"correspond à la dernière écriture du LLM.*"
)
final_content = (
(final_answer or "*(réponse vide)*")
+ footer
+ reasoning_block
)
yield (
[{"role": "user", "content": user_display},
{"role": "assistant", "content": final_content}],
_current_file_path(), _read_file_preview(_current_file_path()),
)
finally:
# Désactive l'auto-append (path persistait globalement)
try:
from jdm_agent.enrich import set_consolidation_output_path
set_consolidation_output_path(None)
except Exception:
pass
# Ferme manuellement l'exclusion_context ouvert avant le
# while persistance (cf. __enter__ plus haut). Try/except pour
# supporter le cas où _excl_ctx n'a pas été initialisé (erreur
# tres precoce dans le run).
try:
_excl_ctx.__exit__(None, None, None)
except Exception:
pass
# Restore env var si on l'avait modifiée
if drops_key and drops_key.strip():
if saved_drops_key is None:
os.environ.pop("JDM_DROPS_API_KEY", None)
else:
os.environ["JDM_DROPS_API_KEY"] = saved_drops_key