⚡ Swarm Architecture

Design — Compléments fonctionnels par IA & croisement ventes

# Design — Compléments fonctionnels par IA & croisement ventes

Date : 2026-06-02 Auteur : Pierre Samson + Claude Périmètre : nouvelle brique « Compléments IA » adossée au panel cross-family-baskets (FR) Statut : design validé (Pierre, 2026-06-02), en attente de relecture du spec écrit

---

1. Contexte & objectif

Le panel Cross-Family Baskets actuel (core/basket_bundles.py + scripts/refresh_cross_family_baskets.py) mine ce qui est acheté ensemble : règles d'association (Apriori, confiance + lift) sur les paniers réels. C'est un signal piloté par les ventes : il capture les co-achats observés, avec leur bruit, et rate les complémentarités fonctionnelles qui ne se sont pas encore matérialisées dans les ventes.

On veut le signal opposé et complémentaire : déduire, uniquement à partir des produits eux-mêmes (IA, sales-blind), quels produits vont fonctionnellement ensemble (souris→piles, chaussures→lacets, imprimante→cartouche), puis croiser avec les ventes pour isoler les paires complémentaires mais rarement achetées ensemble = opportunités de reco/bundle que les ventes seules ne voient pas.

Question métier cible : *« Quels produits devraient logiquement être achetés ensemble (selon leur fonction) mais ne le sont pas encore — donc où placer une reco ou un bundle ? »*

Contrainte structurante : l'IA ne doit voir que le produit (titre + marque), jamais les ventes. Sinon le croisement IA↔ventes devient circulaire.

---

2. Décisions verrouillées

| Décision | Choix | |---|---| | Approche | A — Typage fonctionnel → graphe de compléments par type → croisement ventes | | Signal IA | Titre web + Marque uniquement (sales-blind) | | Grain de complémentarité | niveau type fonctionnel (souris ↔ pile), pas SKU-exact | | Périmètre IA (graphe) | les 18 197 SKU du CSV imports/marie/Datas_pour_produits_associes.csv | | Périmètre co-achat (ventes) | tout l'assortiment actif en paniers (~31k SKU typés) — option (a) | | Fenêtre / canal | 90 j / ALL (alignés sur le refresh existant) | | Garde-fou volume | ≥ 25 acheteurs par type (aligné min_support) | | Exécution | offline / snapshot (passe IA + croisement trop lourds pour le live) | | Moteur LLM | SDK anthropic (modèle Haiku), comme core/labeling.py ; température 0 | | Livrables | Excel pour Marie (principal) + mapping/graphe persistés (réutilisable). Section panel = phase 2 optionnelle |

---

3. Données

3.1 Univers IA — le CSV

imports/marie/Datas_pour_produits_associes.csv
  • 18 197 SKU, colonnes : SAP Code (.), SAP Code, Titre web, Marque,
Code fabricant, GTIN 1..5.
  • Encodage cp1252 (accents). À lire avec encoding="cp1252".
  • Signal exploitable : Titre web + Marque. Pas de description longue ni de
catégorie → tout repose sur le titre.
  • Couverture : tout l'assortiment Lyreco (bureau, EPI/sécurité, encres,
mobilier, informatique…), 566 marques.
  • Le SKU du CSV (SAP Code, sans points) correspond à
ecom_order_lines.product_reference / ecom_products.product_reference.

3.2 Côté ventes — réutilisation de l'existant

Le pull order_baskets de scripts/refresh_cross_family_baskets.py (ecom_order_lines, FR, fenêtre, HAVING COUNT(DISTINCT product_reference) >= 2, ARRAY_AGG(DISTINCT product_reference) AS skus) couvre déjà tout l'actif (~31k SKU distincts sur 90 j). On le réutilise tel quel, ainsi que clean_baskets (filtre anti-réassort taille>20 OU univers≥5).

3.3 Enrichissement produit

prod_meta (nom, section, world, famille) via `ecom_products × section_taxonomy × product_family_taxonomy` (postgres), même requête batchée que l'existant. Sert à afficher des SKU représentatifs lisibles par type.

3.4 Bridge cross-DB (règle mémoire)

Paniers en timescale :5434 ; produits/section/famille en postgres :5433. Jamais de join inter-DB dans une requête : on tire les SKU d'un côté, on requête l'autre avec ANY(%s). (Pattern déjà en place dans le refresh existant.)

---

4. Méthode (pipeline offline)

Phase 1 — Chargement & normalisation

Lire le CSV (cp1252) → csv_products: {sku: {title, brand}} (sku = SAP Code sans points, normalisé string). Charger prod_meta pour enrichissement.

Phase 2 — Typage fonctionnel IA (sales-blind)

Attribuer à chaque SKU un type fonctionnel normalisé depuis titre + marque.
  • Vocabulaire contrôlé incrémental : à chaque batch, on passe au LLM la liste
des types déjà créés et la consigne *« réutilise un type existant si possible, n'en crée un nouveau que si rien ne colle »*. Limite la prolifération (souris/souris sans-fil/souris ergonomique → un seul souris).
  • Batché ~40-60 produits / appel, entrée/sortie JSON ({sku: type}),
température 0.
  • Cache disque clé = hash(title + brand) → re-runs quasi gratuits ; seuls
les SKU nouveaux/modifiés sont retypés.
  • Périmètre du typage : union des SKU du CSV (18k, pour le graphe) et des
SKU actifs en paniers (~31k, pour le co-achat). Overlap gratuit via le cache.
  • Sortie : sku_type_map: {sku: {type_id, type_label}}. Attendu ~400-800 types.

Phase 3 — Graphe de compléments par type (sales-blind)

Pour chaque type issu du CSV, le LLM propose ses types compléments, choisis dans le vocabulaire de types existant (pour que chaque arête retombe sur de vrais produits du catalogue) :
  • sortie par type : liste de {type_complement, relation, force_ia, justification}
où `relation ∈ {consommable-de, accessoire-de, EPI-associé, recharge-de, protection-de, …}, force_ia ∈ [0,1], justification` = une ligne.
  • Batché par type-ancre. Le LLM ne voit jamais les ventes.
  • Symétrisation : a→b et b→a fusionnés en une arête non orientée (type1,
type2) — on garde max(force_ia) et on concatène les justifications/relations.
  • Sortie : complement_edges: [{t1, t2, relation, ai_strength, rationale}].

Phase 4 — Croisement ventes (co-achat au grain type)

Sur les paniers nettoyés (clean_baskets), mapper chaque SKU → son type (sku_type_map), puis agréger au grain paire de types :
  • n(t) = nb de paniers contenant ≥1 SKU de type t ;
  • co(t1,t2) = nb de paniers contenant à la fois ≥1 SKU de t1 et ≥1 de t2 ;
  • N = nb total de paniers nettoyés ;
  • lift(t1,t2) = (co/N) / ((n(t1)/N)·(n(t2)/N)) ;
  • cond(t1,t2) = co / min(n(t1), n(t2)) (taux de co-achat conditionnel).

Fonction pure, testable, dans core/functional_complements.py (pas d'I/O).

Phase 5 — Score & buckets

Pour chaque arête IA (t1,t2), jointer le co-achat mesuré :
  • volume = min(n(t1), n(t2)) (les deux types doivent avoir assez d'acheteurs) ;
  • copurchase_norm = normalisation de cond (ou de lift) sur [0,1] ;
  • opportunité = ai_strength × log1p(volume) × (1 − copurchase_norm).

Garde-fou : ne garder que les paires où n(t1) ≥ min_type_support et n(t2) ≥ min_type_support (défaut 25).

Trois seaux : 1. Sous-exploité — IA fort + co-achat faible (p. ex. lift < ~1 ou cond sous un seuil) → la cible : candidat reco/bundle. 2. Confirmé — IA fort + co-achat fort → valide le moteur (peut alimenter un « fréquemment complémentaires »). 3. IA-seule, jamais ensemble — co ≈ 0 → vraie lacune ou erreur de typage → à vérifier (sert aussi de détecteur de bruit de typage).

Phase 6 — Sorties

  • Excel exports/complements_ia_FR__d.xlsx (pour Marie) :
  • Sous-exploités : t1, t2, relation, ai_strength, justification,
n(t1), n(t2), co, lift, cond, score opportunité, + 1-3 SKU représentatifs par type (les plus vendus, à défaut les premiers).
  • Confirmés (même structure).
  • IA-seule à vérifier (même structure).
  • Typage : sku → type (audit/correction par Marie).
  • Graphe types : toutes les arêtes IA.
  • Snapshot JSON exports/complements_ia_FR__d.json
(machine-readable, alimente une future section panel).
  • Persistance légère (réutilisable) : tables postgres
`product_functional_type (source_country, product_reference, type_id, type_label) et type_complement_edge (source_country, t1, t2, relation, ai_strength, rationale, co_baskets, lift, cond, bucket, computed_at)`. *(Alternative au moment du plan : se contenter des fichiers snapshot si on veut éviter une migration DB — à trancher dans le plan.)*

---

5. Architecture & isolation (calquée sur l'existant)

5.1 core/functional_complements.py — fonctions pures, testables (TDD)

`python def load_csv_products(path, encoding="cp1252") -> dict: """{sku: {"title": str, "brand": str}} depuis le CSV de Marie."""

def type_copurchase(baskets, sku_type_map, min_type_support=25) -> dict: """Agrège n(t), co(t1,t2), lift, cond au grain type sur paniers nettoyés. Pur : pas d'I/O. baskets = list[frozenset[str]] (sortie de clean_baskets)."""

def score_opportunities(complement_edges, copurchase, min_type_support=25) -> list: """Jointe arêtes IA × co-achat → score + bucket (sous-exploité / confirmé / ia-seule). Pur."""

def build_complement_outputs(scored, sku_type_map, prod_meta, type_volume, top_per_type=3) -> dict: """Assemble les sections d'affichage : 3 seaux + typage + graphe, enrichis (SKU représentatifs lisibles par type). Pur.""" `

5.2 core/ai_typing.py — wrapper LLM injectable

Réutilise le pattern core/labeling.py (SDK anthropic, ANTHROPIC_API_KEY, fallback gracieux, modèle Haiku, température 0). `python def type_products(products, existing_vocab, llm_call=None, batch_size=50, cache_path=None) -> dict: """{sku: {type_id, type_label}} ; batché ; cache disque ; vocab contrôlé. llm_call injectable → tests sans réseau."""

def propose_complements(types, llm_call=None) -> list: """Pour chaque type, types compléments (relation, force_ia, justification), contraints au vocab. llm_call injectable.""" ` Les appels LLM sont isolés ici ; tout le reste (core/functional_complements) est pur. Les tests injectent un llm_call factice → aucun appel réseau en test.

5.3 scripts/refresh_functional_complements.py — orchestration

  • Réutilise database.timeseries_db.TimeseriesDatabase (paniers) +
database.crm_db.CRMDatabase (produits) + clean_baskets.
  • Pull order_baskets (même requête que le refresh existant) → typage (Phase 2)
→ graphe (Phase 3) → co-achat (Phase 4) → score (Phase 5) → écrit XLSX + JSON + (option) upsert DB (Phase 6).
  • CLI : `--days 90 --channel ALL --min-type-support 25 --perimeter active
--limit N (--limit` pour dev : sous-échantillonne le CSV).
  • Logs step() + runtime de la passe IA mesuré et loggé (1er run vs caché).

5.4 tests/test_functional_complements.py — TDD, LLM stubbé

Jeu synthétique :
  • load_csv_products : cp1252 + colonnes correctes.
  • type_copurchase : n(t), co, lift, cond exacts sur paniers connus ;
respect min_type_support.
  • score_opportunities : bucketing correct (sous-exploité vs confirmé vs
ia-seule) ; score monotone en ai_strength↑, co-achat↓.
  • build_complement_outputs : SKU représentatifs présents ; enrichissement OK.
  • ai_typing : llm_call factice → vocab réutilisé avant création ; cache hit.

---

6. Paramètres & défauts

| Paramètre | Défaut | Rôle | |---|---|---| | days | 90 | fenêtre ventes | | channel | ALL | ALL / WEB | | perimeter | active | co-achat sur tout l'actif (a) vs csv (b) | | min_type_support | 25 | plancher acheteurs par type | | batch_size | 50 | produits par appel LLM | | max_basket_size | 20 | filtre anti-réassort (réutilisé) | | max_worlds | 5 | filtre anti-réassort (réutilisé) |

---

7. Risques & mitigations

  • Qualité du typage = pièce maîtresse. Mitigations : vocabulaire contrôlé
(réutiliser avant créer), cache déterministe (température 0), feuille Typage pour audit/correction par Marie, et le seau « IA-seule » sert de détecteur d'erreurs de typage (un complément « jamais acheté ensemble » est souvent un mauvais type).
  • Titre seul comme signal : pas de description → certains titres ambigus.
Accepté ; la marque aide ; les erreurs ressortent dans IA-seule à vérifier.
  • Coût/runtime LLM : ~600-1000 appels Haiku (batch 50) au 1er run, mesuré +
loggé, quasi nul ensuite (cache). Modèle Haiku bon marché.
  • Cross-DB : bridge 2 étapes (règle mémoire) — déjà respecté par réutilisation.
  • Encodage cp1252 : lecture explicite ; sinon accents corrompus.
  • Déterminisme du snapshot : température 0 + cache → reproductible.

---

8. Hors périmètre

  • Recalcul live des compléments dans le panel (coût) → snapshot uniquement.
  • GB (FR d'abord).
  • Section panel dans _cross_family_baskets.html : phase 2 optionnelle
(lit le snapshot ; à brancher après validation du livrable Excel).
  • Optimisation du moteur de reco en aval (cette brique produit les
candidats, elle ne les pousse pas en prod).
  • On ne casse pas l'existant : core/basket_bundles.py,
product_co_occurrence et le refresh cross-family restent intacts ; cette brique est additive.