Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Trois workflows GitHub Actions s'enchaînent quand tu pousses un `.feature` Gher
- Pas de tests automatisés côté frontend — la CI ne fait que `tsc --noEmit`.
- Pas de linter Python configuré — pas de `ruff`/`flake8`/`black` à invoquer.
- Catégories d'articles canoniques : `["IA", "DevOps", "Cloud", "Sécurité", "Dev", "IT", "Autre"]`. Définies en doublon dans `backend/app/routers/articles.py`, `backend/app/models/article.py`, et `collector/processors/gemini_processor.py` — modifier les trois ensemble.
- `DEFAULT_MODEL_PRIORITY` (liste de modèles Gemini) est dupliquée dans `collector/main.py`, `collector/processors/gemini_processor.py`, et `backend/app/routers/admin.py`. Quand un nouveau modèle est ajouté en tête de cette liste, le backend nettoie automatiquement les modèles inconnus stockés en Firestore et insère les nouveaux.
- `DEFAULT_MODEL_PRIORITY` (liste de modèles Gemini, **le moins cher en tête**) est définie dans `collector/processors/gemini_processor.py` et dupliquée dans `backend/app/routers/admin.py` et `backend/app/services/article_summarizer.py` — modifier les trois ensemble (`tests/test_collector.py` vérifie qu'elles ne divergent pas). `collector/main.py` et `collector/analyze_logs.py` l'importent depuis le collector.
- **L'ordre stocké en Firestore fait autorité sur la constante** : il est réglable dans l'admin. Pour qu'un changement de l'ordre par défaut s'applique à un projet existant, il faut **incrémenter `MODEL_PRIORITY_VERSION`** (dupliqué dans `gemini_processor.py` et `admin.py`) — sinon la modification reste sans effet en production. La nouvelle liste écrase alors l'ordre stocké une seule fois, puis le choix de l'admin redevient prioritaire. `merge_model_priority()` porte cette règle, purge les modèles inconnus et insère les nouveaux en tête.
- Secrets en prod via Secret Manager (montés en env vars par `--set-secrets` dans `.github/workflows/ci-cd.yml`). En local : `backend/.env` et `collector/.env`, jamais commités (`.gitignore` les bloque).
- `gmail_token.json` est généré une seule fois via `collector/auth_gmail.py` et monté en env var `GMAIL_TOKEN` en prod.
47 changes: 34 additions & 13 deletions backend/app/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,29 @@ def _check_emulator_reachable():
)


# Le modèle le moins cher passe en premier (cf. collector/processors/gemini_processor.py,
# où cette liste et sa version sont dupliquées — modifier les deux ensemble).
DEFAULT_MODEL_PRIORITY = [
"gemini-3.5-flash",
"gemini-3.1-flash-lite",
"gemini-3-flash-preview",
"gemma-4-31b-it",
"gemma-4-26b-a4b-it",
"gemma-4-26b-a4b-it", # 0,07 $ / 0,30 $
"gemma-4-31b-it", # 0,09 $ / 0,34 $
"gemini-3.1-flash-lite", # 0,25 $ / 1,50 $ — GA
"gemini-3-flash-preview", # 0,25 $ / 1,50 $ — preview
"gemini-3.5-flash", # 1,50 $ / 9,00 $
]

# À incrémenter à CHAQUE changement de l'ordre par défaut : sans ce marqueur,
# l'ordre stocké en Firestore l'emporte pour toujours et la constante ci-dessus
# reste sans effet sur un projet existant.
MODEL_PRIORITY_VERSION = 3

class GlobalSettings(BaseModel):
llm_enabled: bool = True
thinking_enabled: bool = True
model_priority: list[str] = DEFAULT_MODEL_PRIORITY
# Défaut = version courante : un PUT qui omettrait le champ ne doit pas
# rejouer la migration et écraser l'ordre choisi dans l'admin. La détection
# d'un document périmé se fait sur le Firestore brut, pas sur ce défaut.
model_priority_version: int = MODEL_PRIORITY_VERSION
gmail_lookback_days: int = 1
retention_days: int = 0
interest: str = ""
Expand All @@ -81,15 +92,25 @@ def get_settings(_: dict = Depends(require_admin)):
if not doc.exists:
return GlobalSettings()
data = doc.to_dict()
stored = data.get("model_priority", [])
# Garde uniquement les modèles connus, ajoute les nouveaux en tête
stored = [m for m in stored if m in DEFAULT_MODEL_PRIORITY]
for model in reversed(DEFAULT_MODEL_PRIORITY):
if model not in stored:
stored.insert(0, model)
stored_version = data.get("model_priority_version", 0)

if stored_version < MODEL_PRIORITY_VERSION:
# L'ordre par défaut du code vient de changer : il s'applique une fois,
# puis l'ordre choisi dans l'admin redevient prioritaire.
stored = list(DEFAULT_MODEL_PRIORITY)
else:
stored = [m for m in data.get("model_priority", []) if m in DEFAULT_MODEL_PRIORITY]
for model in reversed(DEFAULT_MODEL_PRIORITY):
if model not in stored:
stored.insert(0, model)

data["model_priority"] = stored
# Persiste la liste nettoyée
db.collection("settings").document("global").update({"model_priority": stored})
data["model_priority_version"] = MODEL_PRIORITY_VERSION
# Persiste la liste nettoyée et la version appliquée
db.collection("settings").document("global").update({
"model_priority": stored,
"model_priority_version": MODEL_PRIORITY_VERSION,
})
return GlobalSettings(**data)


Expand Down
11 changes: 6 additions & 5 deletions backend/app/services/article_summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

logger = logging.getLogger(__name__)

# Doit rester aligné sur app/routers/admin.py et collector/processors/gemini_processor.py.
DEFAULT_MODEL_PRIORITY = [
"gemini-3.5-flash",
"gemini-3.1-flash-lite",
"gemini-3-flash-preview",
"gemma-4-31b-it",
"gemma-4-26b-a4b-it",
"gemma-4-26b-a4b-it", # 0,07 $ / 0,30 $
"gemma-4-31b-it", # 0,09 $ / 0,34 $
"gemini-3.1-flash-lite", # 0,25 $ / 1,50 $ — GA
"gemini-3-flash-preview", # 0,25 $ / 1,50 $ — preview
"gemini-3.5-flash", # 1,50 $ / 9,00 $
]

PROMPT_VERSION = "linkedin-v3"
Expand Down
9 changes: 5 additions & 4 deletions collector/analyze_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

from google.cloud import firestore
from processors.gemini_processor import DEFAULT_MODEL_PRIORITY
from processors.gemini_processor import DEFAULT_MODEL_PRIORITY, merge_model_priority
from processors.log_analyzer import run_log_analysis


Expand All @@ -20,9 +20,10 @@ def main():
settings_doc = db.collection("settings").document("global").get()
model_priority = DEFAULT_MODEL_PRIORITY
if settings_doc.exists:
stored = settings_doc.to_dict().get("model_priority", [])
if stored:
model_priority = stored
data = settings_doc.to_dict()
model_priority = merge_model_priority(
data.get("model_priority", []), data.get("model_priority_version", 0)
)

result = run_log_analysis(db, model_priority=model_priority)
logging.info(
Expand Down
21 changes: 14 additions & 7 deletions collector/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from scrapers.gmail_reader import read_gmail_source
from processors.gemini_processor import (
enrich_articles_batch, save_raw_articles, generate_run_report,
merge_model_priority, DEFAULT_MODEL_PRIORITY, MODEL_PRIORITY_VERSION,
TITLE_LOG_MAX_LENGTH,
)
from processors.synthesis import run_synthesis
Expand All @@ -36,17 +37,23 @@ def emit(self, record):
db = firestore.Client(project=os.environ.get("FIRESTORE_PROJECT_ID", "tech-news-aggregator-001"))


DEFAULT_MODEL_PRIORITY = ["gemini-3.5-flash", "gemini-3.1-flash-lite", "gemini-3-flash-preview", "gemma-4-31b-it", "gemma-4-26b-a4b-it"]
# DEFAULT_MODEL_PRIORITY et MODEL_PRIORITY_VERSION vivent dans gemini_processor
# (cf. import ci-dessus) — une seule définition côté collector.

def get_global_settings() -> dict:
doc = db.collection("settings").document("global").get()
data = doc.to_dict() if doc.exists else {}
# Filtre les modèles inconnus et ajoute les nouveaux
stored = [m for m in data.get("model_priority", []) if m in DEFAULT_MODEL_PRIORITY]
for model in reversed(DEFAULT_MODEL_PRIORITY):
if model not in stored:
stored.insert(0, model)
data["model_priority"] = stored
# Purge les modèles inconnus, insère les nouveaux, et réapplique l'ordre
# par défaut si la version stockée est périmée.
stored_version = data.get("model_priority_version", 0)
data["model_priority"] = merge_model_priority(
data.get("model_priority", []), stored_version
)
if stored_version < MODEL_PRIORITY_VERSION:
logger.info(
f"Ordre des modèles réinitialisé sur la valeur par défaut du code "
f"(version {stored_version} → {MODEL_PRIORITY_VERSION}) : {data['model_priority']}"
)
data.setdefault("llm_enabled", True)
data.setdefault("translation_enabled", True)
return data
Expand Down
Loading
Loading