diff --git a/.secrets.baseline b/.secrets.baseline index bf7f7eb..e1a4d71 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -140,7 +140,7 @@ "filename": "GUIDE_DEV_PYTHON.md", "hashed_secret": "90bd1b48e958257948487b90bee080ba5ed00caa", "is_verified": true, - "line_number": 4916, + "line_number": 4935, "is_secret": false } ], @@ -177,5 +177,5 @@ } ] }, - "generated_at": "2026-09-07T17:24:40Z" + "generated_at": "2026-09-07T17:59:08Z" } diff --git a/AGENTS.md b/AGENTS.md index 07c49ae..f98fe9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,6 +146,17 @@ pronote-sync --dry-run - Réutiliser un téléchargement/parsing iCal pour l'agenda et les devoirs pendant un même run, sans cache global ni persistant. +### Contrat du provider `openai-compatible` +- Le provider `openai-compatible` réutilise `OpenAISynthesisProvider` avec un `base_url` personnalisé ; aucun nouveau provider n'est créé. +- `AI_BASE_URL` et `AI_MODEL` sont requis ; `AI_API_KEY` est requis (MVP). +- L'URL doit utiliser `https` sauf si `AI_ALLOW_INSECURE_HTTP=true`. +- Les credentials dans l'URL (`user:pass@host`) sont refusés. +- Les paramètres sensibles dans la *query string* sont refusés, y compris ceux sans valeur (`?token`). +- Les URL malformées ou sans hostname sont rejetées (`ValueError` catché). +- Aucune manipulation automatique de `/v1` n'est effectuée. +- Configuration incomplète ou invalide → `None` avec avertissement (mode dégradé) ; la factory ne lève jamais d'exception. +- La factory ne fait aucun appel réseau ; les avertissements utilisent `redact_url()`. + ### Documentation (docstrings) - **Obligatoire** : **Toute** fonction, méthode et classe publique doit avoir une docstring. - **Format** : Utiliser le format **Sphinx/reST** (pas Google ou NumPy) pour une compatibilité native avec Sphinx. diff --git a/GUIDE_DEV_PYTHON.md b/GUIDE_DEV_PYTHON.md index 17f146b..043a379 100644 --- a/GUIDE_DEV_PYTHON.md +++ b/GUIDE_DEV_PYTHON.md @@ -6,6 +6,7 @@ > **⚠️ À noter** : Ce guide est **volontairement détaillé** pour préserver les connaissances acquises sur les spécificités des flux Pronote (iCal) et les décisions architecturales du projet TypeScript. Certaines sections (ex: parsing iCal) contiennent des **observations précises** issues de l'analyse du code existant. > **Mises à jour récentes** : +> - Ajout du provider ``openai-compatible`` dans la section **[3. Configuration](#3-configuration-denvironnement)** (variables §3.1.2, modèle §3.2, exemple §3.1.3) et la factory **[§9.5](#95-factory-pour-les-fournisseurs-ia-synthesis__init__py)** pour supporter les endpoints compatibles OpenAI (OpenRouter, Ollama, proxy LiteLLM) avec validation stricte de l'URL et opt-in HTTP. > - Ajout de la section **[5 bis. Sources externes : blog du collège (RSS)](#5-bis-sources-externes--blog-du-collège-rss)** pour le parsing du flux RSS du blog. > - Mise à jour de la section **[10. Envoi XMPP](#10-envoi-xmpp)** avec la décision architecturale (compte bot dédié, messages directs, pas de PubSub). > - Intégration des modèles `BlogArticle` et `ExternalInfo` dans la section **[6. Modèle de données Pydantic](#6-modèle-de-données-pydantic)**. @@ -299,22 +300,23 @@ d'un besoin réel et testé. | `PRONOTE_MESSAGES_SOURCE` | Source pour les messages (`pronotepy` uniquement). | `pronotepy` | `Literal` | | `SYNC_PAST_DAYS` | Nombre de jours dans le passé pour la sync CalDAV. | `7` | `int` | | `SYNC_FUTURE_DAYS` | Nombre de jours dans le futur pour la sync CalDAV. | `30` | `int` | - -> ⚠️ **Décision d'implémentation** : -> Ces variables sont désormais dans `AppSettings` (et non `CalDAVSettings`) car `CalDAVSettings` utilise `env_prefix="CALDAV_"`, ce qui nécessiterait `CALDAV_SYNC_PAST_DAYS`. -> Leur placement dans `AppSettings` (sans préfixe) garantit un mappage correct avec `SYNC_PAST_DAYS` / `SYNC_FUTURE_DAYS`. | `THEORETICAL_AGENDA_PATH` | Chemin vers le fichier JSON de l'agenda théorique. | `None` | `str \| None`| | `SCHOOL_HOLIDAYS_PATH` | Chemin vers le fichier JSON des vacances scolaires. | `None` | `str \| None`| | `THEORETICAL_WEEK_ANCHOR_DATE` | Date de référence pour la parité des semaines (paire/impaire). | `None` | `date \| None`| | `THEORETICAL_WEEK_ANCHOR_TYPE` | Parité de la semaine de référence (`even` ou `odd`). | `None` | `Literal["even", "odd"] \| None`| | `AI_ENABLED` | Activer la synthèse IA. | `False` | `bool` | -| `AI_PROVIDER` | Fournisseur IA (`openai` ou `litellm`). | `openai` | `str` | +| `AI_PROVIDER` | Fournisseur IA (`openai`, `openai-compatible` ou `litellm`). | `openai` | `Literal["openai", "litellm", "openai-compatible"]` | | `AI_BASE_URL` | URL de base pour l'API IA (ex: OpenAI compatible). | `None` | `str \| None`| | `AI_API_KEY` | Clé API pour l'API IA. | `None` | `SecretStr` | | `AI_MODEL` | Modèle IA à utiliser (exemple recommandé : `gpt-4o-mini`). | `None` | `str \| None`| +| `AI_ALLOW_INSECURE_HTTP` | Autoriser HTTP (non sécurisé) pour `openai-compatible` uniquement. | `False` | `bool` | | `DRY_RUN` | Mode dry-run (pas de modifications CalDAV/XMPP). | `False` | `bool` | | `LOG_LEVEL` | Niveau de log (`DEBUG`, `INFO`, `WARNING`, `ERROR`). | `INFO` | `str` | +> ⚠️ **Décision d'implémentation** : +> Ces variables sont désormais dans `AppSettings` (et non `CalDAVSettings`) car `CalDAVSettings` utilise `env_prefix="CALDAV_"`, ce qui nécessiterait `CALDAV_SYNC_PAST_DAYS`. +> Leur placement dans `AppSettings` (sans préfixe) garantit un mappage correct avec `SYNC_PAST_DAYS` / `SYNC_FUTURE_DAYS`. + #### 3.1.3 Exemple de fichier `.env.example` @@ -360,6 +362,20 @@ AI_BASE_URL=https://api.openai.com/v1 AI_API_KEY=your_ai_api_key # AI_MODEL=gpt-4o-mini # exemple recommandé, non activé par défaut +# Exemple : OpenRouter (HTTPS, provider openai-compatible) +# AI_PROVIDER=openai-compatible +# AI_BASE_URL=https://openrouter.ai/api/v1 +# AI_MODEL=fournisseur/modele +# AI_API_KEY=your-openrouter-key +# AI_ALLOW_INSECURE_HTTP=false + +# Exemple : Ollama local (HTTP, provider openai-compatible) +# AI_PROVIDER=openai-compatible +# AI_BASE_URL=http://127.0.0.1:11434/v1 +# AI_MODEL=modele-local +# AI_API_KEY=local-not-required +# AI_ALLOW_INSECURE_HTTP=true + # --- Divers --- DRY_RUN=false LOG_LEVEL=INFO @@ -376,6 +392,8 @@ LOG_LEVEL=INFO > `sync_past_days` et `sync_future_days` sont dans `AppSettings`, et non `CalDAVSettings`. > `CalDAVSettings.calendar_path` a pour valeur par défaut `"/pronote-sync/"`. > `XmppSettings.resource` a pour valeur par défaut `"pronote-sync"`. +> > ``AISettings.provider`` accepte également ``openai-compatible`` (réutilise ``OpenAISynthesisProvider`` avec un ``base_url`` personnalisé). +> > ``AISettings.allow_insecure_http`` (défaut ``False``) autorise les URLs HTTP pour le provider ``openai-compatible`` uniquement. ```python from typing import Literal @@ -409,9 +427,10 @@ class CalDAVSettings(BaseSettings): class AISettings(BaseSettings): model_config = SettingsConfigDict(env_prefix="AI_", env_file=".env", extra="ignore") enabled: bool = False - provider: Literal["openai", "litellm"] = "openai" + provider: Literal["openai", "litellm", "openai-compatible"] = "openai" base_url: str | None = None api_key: SecretStr | None = None + allow_insecure_http: bool = False model: str | None = None diff --git a/TODO.md b/TODO.md index 79df3c3..00b2ee3 100644 --- a/TODO.md +++ b/TODO.md @@ -176,7 +176,7 @@ Générer une synthèse optionnelle via un fournisseur IA, avec mode dégradé s - [x] Créer `synthesis/provider.py` : protocole `SynthesisProvider.generate → Optional[SynthesisResult]` (ne lève jamais d'exception). - [x] Créer `synthesis/openai.py` : `OpenAISynthesisProvider` (httpx, prompt système FR, max 800 car., timeout 30 s, temp 0.3). - [x] Créer `synthesis/litellm.py` : `LiteLLMSynthesisProvider` (optionnel, extra `ai-litellm`). -- [x] Créer `synthesis/__init__.py` : factory `get_synthesis_provider(settings)` (OpenAI par défaut, litellm si `AI_PROVIDER=litellm`). +- [x] Créer `synthesis/__init__.py` : factory `get_synthesis_provider(settings)` (OpenAI par défaut, litellm si `AI_PROVIDER=litellm`, `openai-compatible` si `AI_PROVIDER=openai-compatible` avec validation d'URL). - [x] Mode dégradé : clé absente / timeout / exception → retour `None` (le pipeline continue sans synthèse). - [x] Respecter les contraintes (3-5 phrases, ton sobre, pas d'emoji dans le texte IA). @@ -185,6 +185,17 @@ Générer une synthèse optionnelle via un fournisseur IA, avec mode dégradé s - Clé absente ou erreur réseau → `None` (aucune exception propagée). - La factory renvoie le bon provider ; litellm derrière l'extra optionnel. +> **Évolution FEAT_M9 — Provider `openai-compatible`** : +> Le provider `openai-compatible` a été ajouté à `get_synthesis_provider` (commit `13e058f` sur `feat/m9-custom-endpoint`). +> Il réutilise `OpenAISynthesisProvider` avec un `base_url` validé (HTTPS obligatoire, HTTP via `AI_ALLOW_INSECURE_HTTP=true`). +> Configuration incomplète → `None` + warning (mode dégradé). Aucun appel réseau à la factory. +> Couverture synthesis : 91,57 % (13 tests factory ajoutés). +> +> **Corrections FIXME_M9 — Audit synthèse IA** : +> Cinq points d'audit corrigés (commit `19cbf8f` sur `fix/m9-fixme`, mergé en `2a27225`) : +> `redact_secrets(extra_secrets=...)`, `SecretStr` préservé dans les providers, contenu du message dans `_build_prompt`, +> `_validate_output` (rejet emoji/titre/liste/HTML), `importorskip` pour les tests litellm. + --- ## M10. Canal XMPP — Priorité : Haute